diff --git a/.phase20-state.json b/.phase20-state.json index f2f5c46..461e135 100644 --- a/.phase20-state.json +++ b/.phase20-state.json @@ -1,9 +1,10 @@ { "completed_features": [], - "current_feature": "slack-integration-stub", + "current_feature": "github-link-on-entries", "started_at": "2026-05-23T07:57:43.412201", "attempted_features": [ "time-budget-per-project", - "recurring-time-entries" + "recurring-time-entries", + "slack-integration-stub" ] } \ No newline at end of file diff --git a/GENERATION_LOG.md b/GENERATION_LOG.md index 490441b..1ce83c4 100644 --- a/GENERATION_LOG.md +++ b/GENERATION_LOG.md @@ -2422,3 +2422,22 @@ src/index.ts(27,25): error TS2769: No overload matches this call. Overload 2 of 3, '(plugin: FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error. Argument of type 'Promise' is not assignable to parameter of type 'FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'. Type 'Promise' provides no match for the signature '(instance: FastifyInstance, FastifyBaseLogger, FastifyTy +- `08:03:40` **INFO** Committed feature slack-integration-stub +- `08:03:41` **INFO** Pushed: rc=0 + +## Phase-3 Feature: github-link-on-entries (2026-05-23 08:03:41) + +- `08:03:41` **INFO** Description: GitHub-Link-Feld pro TimeEntry (z.B. PR-URL) +- `08:03:41` **INFO** Generating apps/api/src/db/schema.ts (WICHTIG: BEHALTE alle bestehenden Tabellen. Füge nur Spalte `externalL…) +- `08:04:16` **INFO** wrote 4130 chars in 35.2s (attempt 1) +- `08:04:16` **INFO** Generating apps/web/src/pages/TimeEntries.tsx (ERWEITERT — behalte alles. Füge im Create-Form optional 'GitHub/Link'-…) +- `08:06:11` **INFO** wrote 15363 chars in 115.2s (attempt 1) +- `08:06:11` **INFO** Running tsc --noEmit on api… +- `08:06:13` **WARN** tsc errors: +src/index.ts(27,25): error TS2769: No overload matches this call. + Overload 1 of 3, '(plugin: FastifyPluginCallback<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error. + Argument of type 'Promise' is not assignable to parameter of type 'FastifyPluginCallback<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'. + Type 'Promise' provides no match for the signature '(instance: FastifyInstance, FastifyBaseLogger, FastifyTypeProvider>, opts: { ...; }, done: (err?: Error | undefined) => void): void'. + Overload 2 of 3, '(plugin: FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error. + Argument of type 'Promise' is not assignable to parameter of type 'FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'. + Type 'Promise' provides no match for the signature '(instance: FastifyInstance, FastifyBaseLogger, FastifyTy diff --git a/apps/api/src/db/schema.ts b/apps/api/src/db/schema.ts index c6ed11a..3a998e9 100644 --- a/apps/api/src/db/schema.ts +++ b/apps/api/src/db/schema.ts @@ -66,6 +66,7 @@ export const timeEntries = pgTable("time_entries", { projectId: uuid("project_id").references(() => projects.id, { onDelete: "cascade" }), description: text("description").notNull(), notes: text("notes"), + externalLink: text("external_link"), startTime: timestamp("start_time").notNull(), endTime: timestamp("end_time"), createdAt: timestamp("created_at").notNull().defaultNow() @@ -97,57 +98,40 @@ export const timeEntryTemplates = pgTable("time_entry_templates", { defaultDurationMinutes: integer("default_duration_minutes"), createdAt: timestamp("created_at").notNull().defaultNow() }) - -export const appSettings = pgTable("app_settings", { +export const apiKeys = pgTable("api_keys", { id: uuid("id").primaryKey().defaultRandom(), - key: text("key").notNull().unique(), - value: text("value").notNull(), - updatedAt: timestamp("updated_at").notNull().defaultNow() -}) - -export const auditLog = pgTable("audit_log", { - id: uuid("id").primaryKey().defaultRandom(), - userId: uuid("user_id").references(() => users.id), - action: text("action").notNull(), - entityType: text("entity_type").notNull(), - entityId: uuid("entity_id"), - oldValue: text("old_value"), - newValue: text("new_value"), - createdAt: timestamp("created_at").notNull().defaultNow() -}) - -export const documents = pgTable("documents", { - id: uuid("id").primaryKey().defaultRandom(), - projectId: uuid("project_id").references(() => projects.id, { onDelete: "cascade" }), + userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }), name: text("name").notNull(), - fileData: bytea("file_data").notNull(), - mimeType: text("mime_type").notNull(), - createdAt: timestamp("created_at").notNull().defaultNow() -}) - -export const webhooks = pgTable("webhooks", { - id: uuid("id").primaryKey().defaultRandom(), - url: text("url").notNull(), - secret: text("secret").notNull(), - events: text("events").array().notNull(), - active: boolean("active").notNull().default(true), - createdAt: timestamp("created_at").notNull().defaultNow() + keyHash: text("key_hash").notNull(), + createdAt: timestamp("created_at").notNull().defaultNow(), + lastUsedAt: timestamp("last_used_at"), + revokedAt: timestamp("revoked_at"), }) export const savedViews = pgTable("saved_views", { id: uuid("id").primaryKey().defaultRandom(), userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }), name: text("name").notNull(), - config: text("config").notNull(), - isDefault: boolean("is_default").notNull().default(false), - createdAt: timestamp("created_at").notNull().defaultNow() + entityType: text("entity_type").notNull(), + filters: text("filters").notNull(), + createdAt: timestamp("created_at").notNull().defaultNow(), }) -export const apiKeys = pgTable("api_keys", { +export const webhooks = pgTable("webhooks", { id: uuid("id").primaryKey().defaultRandom(), - userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }), - keyHash: text("key_hash").notNull().unique(), - name: text("name").notNull(), - expiresAt: timestamp("expires_at"), - createdAt: timestamp("created_at").notNull().defaultNow() -}) \ No newline at end of file + url: text("url").notNull(), + event: text("event").notNull(), + active: boolean("active").notNull().default(true), + createdAt: timestamp("created_at").notNull().defaultNow(), + createdBy: uuid("created_by").references(() => users.id, { onDelete: "set null" }), +}) + +export const auditLog = pgTable("audit_log", { + id: uuid("id").primaryKey().defaultRandom(), + userId: uuid("user_id").references(() => users.id, { onDelete: "set null" }), + action: text("action").notNull(), + resourceType: text("resource_type"), + resourceId: text("resource_id"), + metadata: text("metadata"), + createdAt: timestamp("created_at").notNull().defaultNow(), +}) diff --git a/apps/web/src/pages/TimeEntries.tsx b/apps/web/src/pages/TimeEntries.tsx index b2721b2..e567837 100644 --- a/apps/web/src/pages/TimeEntries.tsx +++ b/apps/web/src/pages/TimeEntries.tsx @@ -29,7 +29,8 @@ export default function TimeEntries() { startTime: "", endTime: "", projectId: "", - notes: "" + notes: "", + externalLink: "" }) const [filters, setFilters] = useState({ @@ -86,7 +87,7 @@ export default function TimeEntries() { mutationFn: (data: Partial) => api.createTimeEntry(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["time-entries"] }) - setFormData({ description: "", startTime: "", endTime: "", projectId: "", notes: "" }) + setFormData({ description: "", startTime: "", endTime: "", projectId: "", notes: "", externalLink: "" }) } }) @@ -120,21 +121,7 @@ export default function TimeEntries() { queryClient.invalidateQueries({ queryKey: ["time-entries"] }) }, onError: (error: any) => { - alert(`Import failed: ${error.message || "Unknown error"}`) - } - }) - - const saveViewMutation = useMutation({ - mutationFn: (view: { name: string, filters: any }) => api.createSavedView(view), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["saved-views", "time-entries"] }) - } - }) - - const deleteViewMutation = useMutation({ - mutationFn: (id: string) => api.deleteSavedView(id), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["saved-views", "time-entries"] }) + alert(`Import failed: ${error.message}`) } }) @@ -142,7 +129,7 @@ export default function TimeEntries() { if (!entries) return [] return entries.filter(e => { const matchesSearch = !filters.search || - e.description.toLowerCase().includes(filters.search.toLowerCase()) || + e.description.toLowerCase().includes(filters.search.toLowerCase()) || e.notes?.toLowerCase().includes(filters.search.toLowerCase()) return matchesSearch }) @@ -173,209 +160,234 @@ export default function TimeEntries() {
-
-
-

New Entry

- +
+

New Entry

+ +
- +
setFormData({...formData, description: v})} - suggestions={descriptionSuggestions} + label="Description" + value={formData.description} + onChange={v => setFormData({...formData, description: v})} + suggestions={descriptionSuggestions} /> - +
- + setFormData({...formData, startTime: e.target.value})} + className="w-full p-2 text-sm border rounded" + value={formData.startTime} + onChange={e => setFormData({...formData, startTime: e.target.value})} />
- + setFormData({...formData, endTime: e.target.value})} + className="w-full p-2 text-sm border rounded" + value={formData.endTime} + onChange={e => setFormData({...formData, endTime: e.target.value})} />
- + setFormData({...formData, projectId: e.target.value})} + className="w-full p-2 text-sm border rounded" + value={formData.projectId} + onChange={e => setFormData({...formData, projectId: e.target.value})} />
- + + setFormData({...formData, externalLink: e.target.value})} + /> +
+ +
+