-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathprogress.txt
More file actions
382 lines (353 loc) · 36 KB
/
progress.txt
File metadata and controls
382 lines (353 loc) · 36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
## 2026-03-06 - US-041
- Updated `internal/pipeline/runner.go`: added nil guard for `r.gen` (returns clear error when AI not configured); added `log.Printf("ERROR ...")` with raw output when JSON parse fails; `ErrInvalidAIOutput` already existed
- Updated `internal/connector/connector.go`: added `ErrCredentialsMissing` typed error struct with VarName field and `NewErrCredentialsMissing(varName)` constructor
- Updated all 5 connectors (github, notion, grafana, posthog, signoz) to return `connector.NewErrCredentialsMissing(...)` instead of `fmt.Errorf(...)` for missing credentials
- Added `TestFirstRunEndpointsReturn200` to `internal/api/edge_cases_test.go`: verifies all 5 data endpoints return 200 before any sync
- All tests pass with race detector: `go test -race ./...`
- Files changed: `internal/pipeline/runner.go`, `internal/connector/connector.go`, all connector packages, `internal/api/edge_cases_test.go`
- **Learnings for future iterations:**
- `ErrCredentialsMissing` in `internal/connector/connector.go` — use `errors.As` to check for this type in error chains
- Pipeline `generate()` now guards against nil `r.gen` — safe to call `pipeline.New(nil, st)` but all pipelines will return errors
- Raw AI output is logged at ERROR level in pipeline generate() when JSON parse fails
- `go test -race ./...` is the full race condition test command
---
## 2026-03-06 - US-040
- Fixed `internal/api/teams.go`: handleTeamSprint now defaults start_date_missing=true; clears to false when valid start_date found; avoids 500 when no sprint meta exists
- Fixed `internal/tui/views/sprint.go`: total_sprints (M) rendered in amber (warningAmberStyle) when > 4
- Updated `internal/api/sync.go`: 409 error message changed to "sync already running for this scope"
- Fixed `internal/pipeline/runner_test.go`: skip integration test when CLAUDECODE env var is set (prevents failure inside nested Claude Code session)
- Added `internal/api/edge_cases_test.go`: TestTeamSprintMissingStartDate (verifies 200 + start_date_missing:true when no sprint meta); TestPostSyncConflict (verifies 409 with correct error message)
- All tests pass: `go test ./...`
- Files changed: `internal/api/teams.go`, `internal/tui/views/sprint.go`, `internal/api/sync.go`, `internal/pipeline/runner_test.go`, `internal/api/edge_cases_test.go`
- **Learnings for future iterations:**
- Default `start_date_missing = true` in the sprint response and clear when found — safer than checking at the end
- Tests that use chi URL params must go through the full router (not direct handler calls) so URLParam extraction works
- `CLAUDECODE` env var is set inside Claude Code sessions — use to skip integration tests that would run the claude binary
---
## 2026-03-06 - US-039
- Created `scripts/smoke_test.sh`: starts server in background, logs in (saves token), calls GET /teams, GET /org/overview, POST /sync (org), polls sync run until done/timeout, GET /teams/{id}/sprint; prints PASS/FAIL summary; exits 1 on failure
- Script is chmod +x
- Files changed: `scripts/smoke_test.sh`
- **Learnings for future iterations:**
- Smoke test uses `BASE_URL`, `ADMIN_USER`, `ADMIN_PASS`, `CONFIG` env vars for configuration
- SyncRun status field is `Status` (capital S) in JSON — matches store.SyncRun struct without json tags
- Script skips sprint test gracefully if no teams exist
---
## 2026-03-06 - US-038
- Created `internal/sync/autotag.go`: Engine.AutoTag iterates active github_project catalogue items, parses source_meta for owner/repo/project_id, builds teamLabelMap from ListTeams, calls github.AutoTagIssues per project
- Created `internal/api/admin.go`: GET /admin/autotag (edit role) calls engine.AutoTag and returns {status: "ok"}
- Updated `internal/api/router.go`: added AutoTag(ctx) to SyncEngine interface; registered GET /admin/autotag route
- Updated `cmd/server/main.go`: graceful shutdown with signal.NotifyContext + http.Server.Shutdown(30s); wg for autotag ticker + server goroutines; PruneStaleCache(30 days) on startup; AutoTag ticker every 12h; full Engine wiring (connectors, AI, pipeline, sync engine)
- Files changed: `cmd/server/main.go`, `internal/sync/autotag.go`, `internal/api/admin.go`, `internal/api/router.go`
- **Learnings for future iterations:**
- `pipeline.New(cachedGen, st)` is the constructor (not `NewRunner`); takes CachedGenerator and Store
- `signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)` is the Go 1.16+ idiom for graceful shutdown
- `_ = wg` on a sync.WaitGroup causes vet error (copies lock value); remove the blank assignment
- AutoTag source_meta for github_project: `{owner, repo, project_id}` — all three required; skip if any missing
- `pipeline.New(nil, st)` would panic on first use; guard with `if gen != nil` before creating CachedGenerator
---
## 2026-03-06 - US-037
- Created `internal/tui/views/config/config_annotations.go`: ConfigAnnotationsView loads all annotations via GetConfigAnnotations; team-level then item-level sections; archived entries show "[Archived]" in dim; e edits content (textarea, Ctrl+S to save); d hard-deletes with y/N confirmation
- Created `internal/tui/views/config/config_users.go`: ConfigUsersView with multi-step input flow for n (username→password→role); e edits role then optional password; d deletes with y/N confirm; role normalized to view/edit
- Updated `internal/tui/views/config/config_root.go`: wired case 3 (Annotations) and case 4 (Users) in pushSubView
- Files changed: `internal/tui/views/config/config_annotations.go`, `internal/tui/views/config/config_users.go`, `internal/tui/views/config/config_root.go`
- **Learnings for future iterations:**
- `textarea.Blink` is the Init cmd for textarea.Model in edit mode
- `textinput.Blink` is the Init cmd for textinput.Model in focus mode
- Multi-step user creation: use mode progression (username → password → role) with `advanceInputMode()` pattern
- `PutConfigUser(id, role, password)` — pass empty string for password to keep existing
---
## 2026-03-06 - US-036
- Added PostConfigTeam/PutConfigTeam/DeleteConfigTeam/PostConfigMember/PutConfigMember/DeleteConfigMember to `internal/tui/client/client.go` with TeamConfigResponse and MemberConfigResponse types
- Created `internal/tui/views/config/config_root.go`: ConfigRootView sub-menu with 5 items; Teams & Members and Sources are wired; others return nil (stubs)
- Created `internal/tui/views/config/config_teams.go`: ConfigTeamsView with 7-mode state machine (normal/input-new-team/edit-team/new-member/edit-member/confirm-delete-team/confirm-delete-member); j/k nav, Enter expand, n/e/d CRUD; teamsMutatedMsg reloads teams after any mutation
- Created `internal/tui/views/config/config_sources.go`: ConfigSourcesView with status filter (f cycles all/active/pending/ignored); Enter pushes configTagView; D pushes configDiscoverView; inline tag view with AI suggestion + team selector (Tab) + purpose input
- Files changed: `internal/tui/client/client.go`, `internal/tui/views/config/*.go`
- **Learnings for future iterations:**
- Config views are in `internal/tui/views/config/` package (separate from `views`)
- Config views still use `views.PushViewMsg` and `views.PopViewMsg` from the `views` package to push/pop views
- `cfgSelectedStyle`/`cfgDimStyle` are defined locally in config_root.go and shared via same package
- textinput Update needs the full `tea.Msg` passed (not just `tea.KeyMsg`) for character-by-character editing to work
- `configTagView` uses `views.PopViewMsg{}` to pop itself after saving
---
## 2026-03-06 - US-035
- Created `internal/tui/components/banner.go`: Banner struct with Active/Label/Failed/FailedMsg fields; Render() prepends banner to view output; in-progress=yellow, failed=red bold + "Press Enter to dismiss"
- Updated `internal/tui/app.go`: added SyncStartedMsg/SyncFailedMsg types; refactored SyncDoneMsg (now only for success); SyncPoller.Poll() returns SyncFailedMsg on error; App.banner updated on each msg; App.View() prepends banner.Render(); Enter dismisses failed banner
- Files changed: `internal/tui/components/banner.go`, `internal/tui/app.go`
- **Learnings for future iterations:**
- `components` package lives at `internal/tui/components/`; import it from `tui` package (not from `views` — would be circular)
- `SyncStartedMsg{RunID, Label}` triggers App-level banner + polling; views can emit this to hook into global banner
- `SyncDoneMsg` = success (banner clears); `SyncFailedMsg{Err}` = failure (red banner stays until Enter)
- `SyncPoller.Poll()` now returns SyncFailedMsg on status="error", SyncDoneMsg on "done"
---
## 2026-03-06 - US-034
- Created `internal/tui/views/metrics.go`: MetricsView loads GetMetrics on Init; lists configured panels (title + latest value); nil value shown as dim "—"; footer with last synced timestamp; no-data placeholder
- Updated `internal/tui/views/team.go`: wired case 4 (MetricsView) in pushSubView — all 5 team menu items are now wired
- Files changed: `internal/tui/views/metrics.go`, `internal/tui/views/team.go`
- **Learnings for future iterations:**
- All 5 team menu items (sprint=0, goals=1, workload=2, velocity=3, metrics=4) are now wired in pushSubView
- MetricsPanel.Value is *string (nullable); display "—" when nil
---
## 2026-03-06 - US-033
- Created `internal/tui/views/workload.go`: WorkloadView loads GetWorkload on Init; tabular per-member display (name, estimated days, label); j/k navigation through members; HIGH=red, NORMAL=default, LOW=dim; `a` pushes AnnotateView with team-level tier pre-selected; footer with last synced timestamp
- Created `internal/tui/views/velocity.go`: VelocityView loads GetVelocity on Init; sparkline using unicode block chars (▁▂▃▄▅▆▇█) normalized to max score; tabular breakdown per sprint (label, score, issues, PRs, commits); footer with last synced timestamp; no-data placeholder
- Updated `internal/tui/views/team.go`: wired case 2 (WorkloadView) and case 3 (VelocityView) in pushSubView
- Files changed: `internal/tui/views/workload.go`, `internal/tui/views/velocity.go`, `internal/tui/views/team.go`
- **Learnings for future iterations:**
- Workload annotation uses team-level tier (not item-level) with empty itemRef — pass `"team"` as tier and `""` as itemRef to NewAnnotateView
- Sparkline normalization: divide each score by max, multiply by (len(blocks)-1), clamp to block index range
- `riskNormalStyle` (color "7") used for NORMAL workload label; HIGH=riskHighStyle (red), LOW=dimStyle (gray)
---
## 2026-03-06 - US-032
- Created `internal/tui/views/goals.go`: GoalsView loads GetGoals on Init; two sections (Goals + Concerns) separated by a unicode divider; j/k navigation through combined list; concerns colored by severity (HIGH=red, MEDIUM=yellow, LOW=gray); stale_annotation_ keys prefixed [STALE ANNOTATION] in amber (warningAmberStyle); `a` pushes AnnotateView; footer with last synced timestamp
- Created `internal/tui/views/annotate.go`: AnnotateView with textarea.Model (charmbracelet/bubbles); Tab toggles tier between Item-level and Team-level (default item); Ctrl+Enter submits via POST /annotations; success returns PopViewMsg to pop the view; error displayed inline
- Added `PopViewMsg` type to `internal/tui/views/annotate.go`
- Updated `internal/tui/app.go`: handles `views.PopViewMsg` to pop top view from stack
- Updated `internal/tui/views/team.go`: case 1 in pushSubView now pushes NewGoalsView
- Files changed: `internal/tui/views/goals.go`, `internal/tui/views/annotate.go`, `internal/tui/views/team.go`, `internal/tui/app.go`
- **Learnings for future iterations:**
- `PopViewMsg` is defined in `internal/tui/views/annotate.go`; App handles it to pop the top view (complement to PushViewMsg)
- `textarea.Blink` is the Init cmd for textarea.Model (same pattern as textinput.Blink)
- `warningAmberStyle` (color "214") is defined in `sprint.go` and reused in `goals.go` for stale annotation prefix — no need to redefine
- `AnnotateView` accepts teamID (int64), tier (string), itemRef (string), label (string) — push it from any view using `PushViewMsg`
- For team-tier annotations, itemRef is set to nil (only passed for item-tier); tier is toggled by Tab
---
## 2026-03-06 - US-031
- Updated `internal/tui/views/team.go`: full sub-menu view with Sprint & Plan Status, Goals & Concerns, Resource/Workload, Velocity, Business Metrics; j/k + Enter to drill in; r triggers team sync via PostSync("team", &teamID); poll loop mirrors org_overview pattern
- Created `internal/tui/views/sprint.go`: SprintView loads GetSprint on Init; shows Week N of M; amber warning (color "214") for start_date_missing; red warning (errorStyle) for next_plan_start_risk; lists goals; footer with last synced timestamp; no-data placeholder
- Only Sprint & Plan Status drills into SprintView; other menu items are stubs (return nil) for future stories
- Files changed: `internal/tui/views/team.go`, `internal/tui/views/sprint.go`
- **Learnings for future iterations:**
- `warningAmberStyle` uses lipgloss color "214" (orange/amber) — distinct from yellow ("11") used for sync banner
- Team sync uses lowercase "r" (not uppercase "R" like org sync) since we're scoped to a team
- `pushSubView()` returns nil for unimplemented menu items — App.Update ignores nil tea.Cmd safely
- Other sub-views (goals, workload, velocity, metrics) are wired in US-032, US-033, US-034; update `pushSubView()` switch in team.go when implementing those
---
## 2026-03-06 - US-030
- Created `internal/tui/views/org_overview.go`: OrgOverviewView loads org overview via GetOrgOverview on Init
- Team cards show `[Team Name] Sprint N/M | Risk: HIGH | Focus: ...` with Lip Gloss risk colors (HIGH=red, MEDIUM=yellow, LOW=green)
- j/k navigation through team cards; Enter pushes TeamView stub
- R triggers org sync via PostSync("org", nil), polls status, reloads data on done
- Shows sync banner during sync; "No data yet" state when data is empty
- Footer shows last synced timestamp or "Never synced"
- Created `internal/tui/views/team.go`: stub TeamView (US-031 will flesh out)
- Moved `PushViewMsg` from `internal/tui/app.go` to `internal/tui/views/org_overview.go` to break import cycle
- Updated `internal/tui/app.go`: handles `views.PushViewMsg`; LoginDoneMsg now pushes OrgOverviewView
- Updated `cmd/tui/main.go`: pushes OrgOverviewView when token is valid (was empty stack before)
- Files changed: `internal/tui/views/org_overview.go`, `internal/tui/views/team.go`, `internal/tui/app.go`, `cmd/tui/main.go`
- **Learnings for future iterations:**
- `PushViewMsg` lives in `internal/tui/views` (not `internal/tui`) to avoid import cycle; App handles `views.PushViewMsg`
- Views implement their own sync poll loop using internal msg types (orgSyncPollMsg, etc.) — no shared poller needed
- Lip Gloss color codes: 9=red, 11=yellow, 10=green, 8=dim gray, 12=bright blue
- `NewTeamView(c, teamID, name)` is the stub for US-031; update it in place in team.go
---
## 2026-03-06 - US-029
- Created `internal/tui/views/login.go`: LoginView with two textinput.Model fields (username + password) using `github.com/charmbracelet/bubbles/textinput`
- Submit on Enter: calls client.Login; on success emits LoginDoneMsg; on failure displays error below the form
- Password field uses EchoPassword mode
- Added `IsTokenExpired()` to `internal/tui/client/client.go`: base64url-decodes JWT payload, checks `exp` claim without signature verification
- Added `ShowLoginMsg` to `internal/tui/app.go`: App pushes a new LoginView when received
- App handles `LoginDoneMsg` by popping the login view
- Updated `cmd/tui/main.go`: LoadToken + IsTokenExpired on startup; pushes LoginView if no valid token
- Added `github.com/charmbracelet/bubbles v1.0.0` to go.mod (plus transitive deps: atotto/clipboard, clipperhouse/displaywidth, clipperhouse/uax29)
- Files changed: `internal/tui/views/login.go`, `internal/tui/client/client.go`, `internal/tui/app.go`, `cmd/tui/main.go`, `go.mod`, `go.sum`
- **Learnings for future iterations:**
- `charmbracelet/bubbles` is now available; use `textinput.New()` / `textinput.Blink` for text inputs in TUI views
- `client.IsTokenExpired()` decodes JWT exp without needing the JWT library; safe to call after `LoadToken()`
- `ShowLoginMsg{}` triggers login from any view; `LoginDoneMsg{}` pops the login view from the App stack
- Views package: `internal/tui/views/` — all TUI view models live here; import cycle safe (views import client, not tui)
---
## 2026-03-06 - US-028
- Created `internal/tui/client/client.go`: typed HTTP client with token file storage at `~/.dashboard/token`
- Client handles 401 by attempting token refresh via POST /auth/refresh; on failure clears token file and returns `ErrUnauthenticated`
- Defined all response mirror types in the client package (OrgOverviewResponse, TeamItem, SprintResponse, GoalsResponse, WorkloadResponse, VelocityResponse, MetricsResponse, SyncRunResponse, AnnotationResponse, SourceItemResponse, GroupedAnnotationsResponse, UserResponse)
- Implemented all required methods: Login, GetOrgOverview, GetTeams, GetSprint, GetGoals, GetWorkload, GetVelocity, GetMetrics, PostSync, GetSyncRun, PostAnnotation, PutAnnotation, DeleteAnnotation, GetConfigSources, PutConfigSource, PostDiscover, GetConfigAnnotations, GetConfigUsers, PostConfigUser, PutConfigUser, DeleteConfigUser
- Created `internal/tui/app.go`: App struct with views []tea.Model stack, client *client.Client, syncPoller *SyncPoller; Enter (via PushViewMsg) pushes view; Esc/Backspace pops; q quits when stack depth ≤ 1
- SyncPoller polls GET /sync/{run_id} every 2s via tea.Cmd loop (syncPollMsg → Poll → SyncDoneMsg)
- Updated `cmd/tui/main.go` to launch Bubble Tea program with App as root model
- Files changed: `internal/tui/client/client.go`, `internal/tui/app.go`, `cmd/tui/main.go`
- **Learnings for future iterations:**
- Client mirror types live in `internal/tui/client` — do NOT import `internal/api` from TUI (would create import cycle potential)
- `SyncRunResponse` uses capitalized field names (ID, Status, Scope, Error) since `store.SyncRun` has no json tags and is serialized as-is by `writeJSON`
- App handles `PushViewMsg` — views push new views by returning `tea.Cmd` that returns `PushViewMsg{View: newView}`
- SyncPoller polling loop: Poll() returns a cmd that sleeps 2s, checks status; if still running returns `syncPollMsg`; App.Update re-dispatches with `a.syncPoller.Poll(m.RunID)`
---
## 2026-03-06 - US-027
- All rollover logic was already implemented in US-025 (config_sources.go, store/annotations.go, pipeline/runner.go)
- Added explicit `TestRolloverAnnotations` unit test to `internal/store/store_test.go`
- Test creates team, creates item-tier and team-tier annotations, calls `ArchiveItemAnnotationsForPlan`, then explicitly verifies each annotation's archived state
- Pipeline filtering already confirmed: `Runner.activeAnnotations()` in `runner.go` filters `Archived == 0` Go-side after `ListAnnotations` query
- Files changed: `internal/store/store_test.go`
- **Learnings for future iterations:**
- US-025 already implemented the full rollover flow; US-027 just needed an explicit targeted unit test
- `ListAnnotations` does NOT filter by archived=0 in SQL — it returns all; Go-side filter in `activeAnnotations` compensates for pipeline inputs
- `TestAnnotationCRUD` implicitly tests rollover but `TestRolloverAnnotations` makes it explicit with per-annotation ID verification
---
## 2026-03-06 - US-026
- Implemented `GET /config/users`: returns list of users (without password_hash)
- Implemented `POST /config/users`: accepts {username, password, role}, bcrypt-hashes password, stores user
- Implemented `PUT /config/users/{id}`: accepts {role, password}; both optional; fetches existing user to preserve current values if fields omitted
- Implemented `DELETE /config/users/{id}`: checks target user's role; returns 409 if deleting the last edit-role user via `CountUsersByRole`
- Added `CountUsersByRole(ctx, role) (int, error)` to `internal/store/users.go`
- `/annotations` POST/PUT/DELETE already delegated to shared helpers in `annotations.go` (done in US-025)
- Files changed: `internal/api/config_users.go`, `internal/store/users.go`
- **Learnings for future iterations:**
- `auth.HashPassword` and `auth.CheckPassword` are in `internal/auth/password.go` — use these instead of calling bcrypt directly
- `UpdateUser` takes both `passwordHash` and `role`; if only one changes, fetch existing user first to preserve the other
- DELETE /config/users/{id} returns 409 (not 403) when trying to remove the last edit-role user
---
## 2026-03-06 - US-025
- Implemented `GET /config/sources`: lists all catalogue items with their source configs, grouped by catalogue_id; includes ai_suggested_purpose from ai_suggestion field
- Implemented `PUT /config/sources/{id}`: updates catalogue status and upserts source_config; handles current_plan rollover (archive item annotations + delete old config before inserting new)
- Implemented `GET /config/annotations`: returns all annotations grouped by tier (`item` vs `team`) including archived
- Implemented `POST /config/annotations`, `PUT /config/annotations/{id}`, `DELETE /config/annotations/{id}`
- Implemented shared helpers `sharedCreateAnnotation`, `sharedUpdateAnnotation`, `sharedDeleteAnnotation` used by both `/config/annotations` and `/annotations` routes
- Added migration `0010_source_configs_meta` to add `config_meta TEXT` to `source_configs`
- Updated `SourceConfig` model and all store methods to include `config_meta` (6-field scan)
- Added `GetSourceConfigsByItemID`, `FindCurrentPlanForTeam`, `ListAllAnnotations` to store
- Updated `UpsertSourceConfig` signature to include `configMeta sql.NullString` (4th param); updated `store_test.go` call sites
- Files changed: `internal/store/migrations/0010_*`, `internal/store/models.go`, `internal/store/source_configs.go`, `internal/store/annotations.go`, `internal/store/store_test.go`, `internal/api/config_sources.go`, `internal/api/config_annotations.go`, `internal/api/annotations.go`
- **Learnings for future iterations:**
- `UpsertSourceConfig` now takes `configMeta sql.NullString` as 4th param; update any future call sites
- Rollover logic: `FindCurrentPlanForTeam` → `ArchiveItemAnnotationsForPlan` → `DeleteSourceConfig` → `UpsertSourceConfig`
- `ListAllAnnotations` returns all annotations regardless of team (used by config panel); `ListAnnotations` filters by team_id
- Shared annotation mutation helpers in `config_annotations.go` are used by both `/config/annotations` and `/annotations` routes
---
## 2026-03-06 - US-024
- Implemented all 6 config teams/members CRUD handlers in `internal/api/config_teams.go`
- Added `GetTeam(ctx, id)` to `internal/store/teams.go` (needed to return updated team after PUT)
- Extended `AddMember` and `UpdateMember` signatures to include `notionUserID sql.NullString` parameter
- Updated `store_test.go` call sites with the new 5-arg signatures
- Files changed: `internal/api/config_teams.go`, `internal/store/teams.go`, `internal/store/store_test.go`
- **Learnings for future iterations:**
- `UpdateTeam` returns only error (not the updated record); use `GetTeam` after update to return the full record
- `AddMember`/`UpdateMember` now take `notionUserID` as 4th param (before `role`): `(ctx, teamID, name, githubLogin, notionUserID, role)`
- DELETE handlers return 204 No Content (no body); no need to call `DeleteTeam`/`DeleteMember` — cascade is handled by FK in schema
---
## 2026-03-06 - US-022
- Implemented `GET /org/overview`: loads all teams, reads sprint_parse/concerns/goal_extraction/workload ai_cache by pipeline+team, computes cross-team workload aggregate, loads org-level alignment cache, returns full org overview JSON
- Implemented `GET /teams`: returns teams with members including notion_user_id (null until set)
- Added `store.GetLastCompletedSyncRun(ctx, scope, teamID)` to `internal/store/sync_runs.go`
- Added `store.GetLatestCacheByPipeline(ctx, pipeline, teamID)` to `internal/store/ai_cache.go`
- Added migration `0009_team_members_notion` to add `notion_user_id TEXT` column to `team_members`
- Updated `TeamMember` model and `scanMember` in `teams.go` to include `notion_user_id`
- Files changed: `internal/api/org.go`, `internal/api/teams.go`, `internal/store/sync_runs.go`, `internal/store/ai_cache.go`, `internal/store/models.go`, `internal/store/teams.go`, `internal/store/migrations/0009_team_members_notion.{up,down}.sql`
- **Learnings for future iterations:**
- `GetLatestCacheByPipeline` queries by pipeline+team_id without needing the hash — useful for read endpoints that load the most recent AI output
- `org.go` imports `internal/pipeline` for pipeline name constants and result structs; this is fine (no import cycle)
- `notion_user_id` is now in `team_members` (via migration 0009); `AddMember`/`UpdateMember` signatures unchanged — new field defaults to NULL until US-024 adds write support
- `workloadLabel` and `highestSeverity` helpers in `org.go` compute aggregated labels server-side
---
## 2026-03-06 - US-021
- Implemented `POST /sync`: validates scope ('team'/'org'), requires team_id when scope='team', checks store for existing running sync run (409 if busy), calls engine.Sync, returns {sync_run_id: N}
- Implemented `GET /sync/{run_id}`: parses run_id from URL, fetches from store, returns SyncRun as JSON (404 if not found)
- Implemented `POST /config/sources/discover`: validates scope + target, calls engine.Discover, returns {sync_run_id: N}
- Updated `SyncEngine` interface in `router.go` to include `Sync` and `Discover` methods with proper context signatures
- Files changed: `internal/api/sync.go`, `internal/api/config_sources.go`, `internal/api/router.go`
- **Learnings for future iterations:**
- `SyncEngine` interface is in `router.go`; `Deps.Engine` field is type `SyncEngine`
- `cmd/server/main.go` does not yet wire up Engine — it will need to be updated in a future story
- chi URL params use `chi.URLParam(r, "run_id")` matching the route pattern `{run_id}`
- The handler checks `store.GetRunningSyncRun` directly for the 409 check before calling engine.Sync (avoids race and gives clean 409 semantics)
---
## 2026-03-06 - US-020
- Implemented `(e *Engine) Sync(ctx, scope string, teamID *int64) (syncRunID int64, err error)` in `internal/sync/sync.go`
- Checks for existing 'running' sync run via new `store.GetRunningSyncRun`; returns existing ID without creating a new run
- Inserts sync_run with status='running', launches goroutine
- Goroutine: for team scope, calls `fetchTeamData` (loads source_configs, fetches notion pages/dbs and github PRs/issues/commits incrementally with `updated_at` as `since`, updates `updated_at` on success via `store.TouchCatalogueItem`)
- Runs pipelines in order: sprint_parse → goal_extraction → concerns → workload_estimation → velocity_analysis
- For org scope: runs all team pipelines, then fetches org_goals/org_milestones sources, calls `RunGoalAlignment` with all teams' extracted goals + org goals text
- Partial source failures collected in error map; run marked 'error' with JSON if any errors, 'done' on full success
- Files changed: `internal/sync/sync.go`, `internal/store/sync_runs.go`, `internal/store/catalogue.go`
- **Learnings for future iterations:**
- sync_runs status CHECK constraint is `('running', 'done', 'error')` — NOT 'completed'/'failed'; existing discover.go uses wrong values (bug in US-019 code, errors discarded)
- `GetSourceConfigsForScope(ctx, sql.NullInt64{})` returns org-level configs (team_id IS NULL); pass a valid NullInt64 for team-scoped configs
- source_catalogue.updated_at serves as the `since` timestamp for incremental fetching
- github FetchIssues requires a non-empty label; skip issue fetch if no label in source_meta
- `store.TouchCatalogueItem` added to update updated_at after successful fetch
---
## 2026-03-06 - US-017
- Implemented `VelocityResult`/`SprintVelocity`/`VelocityBreakdown`/`VelocitySprint` structs and `RunVelocity` in `internal/pipeline/velocity.go`: takes sprints (up to 4) with closed_issues, merged_prs, commit_count; outputs normalized score + breakdown per sprint
- Implemented `AlignmentResult`/`TeamAlignment` structs and `RunAlignment` in `internal/pipeline/alignment.go`: org-level pipeline (nil teamID), takes org_goals_text + team_goals map; outputs per-team aligned bool + notes, plus flags
- Implemented `DiscoverySuggestionResult` struct and `RunDiscoverySuggestion` in `internal/pipeline/discovery.go`: no teamID (item not yet tagged), takes title + excerpt; outputs suggested_purpose, confidence, reasoning
- Files changed: `internal/pipeline/velocity.go`, `internal/pipeline/alignment.go`, `internal/pipeline/discovery.go`
- **Learnings for future iterations:**
- `alignment` and `discovery_suggestion` use nil teamID in CachedGenerator (org-level / untagged item scope)
- `velocity` pipeline name is already in ai_cache CHECK constraint — no migration needed
- All three follow the same buildPrompt pattern as previous pipelines
---
## 2026-03-06 - US-016
- Implemented `ConcernsResult`/`Concern` structs and `RunConcerns` in `internal/pipeline/concerns.go`: takes open_issues, merged_prs, sprint_plan_text, extracted_goals, sprint_meta + active annotations; outputs concerns with key, summary, explanation, severity
- Implemented `WorkloadResult`/`MemberWorkload`/`WorkloadMember`/`SprintWindow` structs and `RunWorkload` in `internal/pipeline/workload.go`: takes members, sprint_window, standard_sprint_days; outputs per-member estimated_days and LOW/NORMAL/HIGH label
- Files changed: `internal/pipeline/concerns.go`, `internal/pipeline/workload.go`
- **Learnings for future iterations:**
- Both pipelines follow the same pattern: buildPrompt → inputs["prompt"] → CachedGenerator.Generate → json.Unmarshal
- Workload labels (LOW/NORMAL/HIGH) are determined by the AI based on thresholds described in the schema string; no Go-side label logic needed
- `concerns` and `workload` are valid pipeline names in ai_cache CHECK constraint
---
## 2026-03-06 - US-015
- Implemented shared `buildPrompt` in `internal/pipeline/shared.go`: wraps schema + inputs + optional annotations in natural language prompt with XML tags
- Implemented `SprintParseResult` struct and `RunSprintParse` in `internal/pipeline/sprint_parse.go`: calls CachedGenerator, parses JSON output, upserts sprint_meta with planType="current"
- Implemented `GoalExtractionResult`/`ExtractedGoal` structs and `RunGoalExtraction` in `internal/pipeline/goal_extraction.go`
- Modified `internal/ai/cached_generator.go`: if `inputs["prompt"]` is a non-empty string, pass it to the inner generator instead of canonical JSON (canonical JSON still used as cache key)
- Files changed: `internal/ai/cached_generator.go`, `internal/pipeline/shared.go`, `internal/pipeline/sprint_parse.go`, `internal/pipeline/goal_extraction.go`
- **Learnings for future iterations:**
- Pipeline convention: build prompt via `buildPrompt(schema, rawInputs, annotations)`, put result in `inputs["prompt"]`, pass `nil` annotations to CachedGenerator (annotations are embedded in the prompt string)
- CachedGenerator now uses `inputs["prompt"]` as the actual prompt to the inner generator; canonical JSON (which includes the prompt) is still the cache key
- `GoalExtractionPipeline = "goal_extraction"` — this name is in the ai_cache CHECK constraint; check constraint list before adding new pipeline names
- `UpsertSprintMeta` called with `planType = "current"` for current sprint plans; endDate left null since schema does not include it
---
## Codebase Patterns
- `ai_cache.pipeline` has a CHECK constraint: must be one of `sprint_parse`, `concerns`, `goal_extraction`, `workload`, `velocity`, `alignment`, `discovery_suggestion`
- `ai_cache.team_id` has a FOREIGN KEY constraint to teams table; tests must create a real team before using a team ID
- Go's `encoding/json` sorts map keys alphabetically automatically — no custom sort needed for canonical JSON
- All connectors follow the same pattern: `New(credentials...) *Client`, `checkCredentials() error`, `Discover(ctx, target) ([]DiscoveredItem, error)`
- Connectors return clear errors for missing credentials rather than panicking
- `connector.DiscoveredItem` is the shared type from `internal/connector/connector.go`
- Use `net/http` directly (no external HTTP libraries) for metrics connectors
- Grafana dashboard URLs follow `/d/{uid}/{slug}` pattern
- PostHog pagination uses `next` field (absolute URL) in list responses
- SigNoz uses `SIGNOZ-API-KEY` header (not `Authorization: Bearer`)
- Always use `strings.TrimRight(baseURL, "/")` to normalize base URLs
- `go build ./...` and `go vet ./...` are the quality checks
- `charmbracelet/bubbles v1.0.0` is in go.mod; use `textinput.New()` for TUI text input fields
- TUI views live in `internal/tui/views/`; they import `internal/tui/client` but NOT `internal/tui` (avoids import cycle)
- `PushViewMsg` is defined in `internal/tui/views` (not `tui`); App handles `views.PushViewMsg` to push new views
- Pipeline prompt pattern: `buildPrompt(schema, rawInputs, annotations)` → put in `inputs["prompt"]` → call `CachedGenerator.Generate(ctx, pipelineName, &teamID, inputs, nil)`
- sync_runs status CHECK is `('running', 'done', 'error')` — use 'done'/'error', NOT 'completed'/'failed'
- Always use `IF NOT EXISTS` for migrations
- Config uses `gopkg.in/yaml.v3` for YAML loading
---
## 2026-03-06 - US-013
- Implemented `Generator` interface in `internal/ai/generator.go`
- Implemented `AnthropicProvider` in `internal/ai/anthropic.go`: posts to Anthropic REST API, respects `ANTHROPIC_API_KEY` env override
- Implemented `ClaudeCodeProvider` in `internal/ai/claude_code.go`: runs claude CLI subprocess, streams stdout line-by-line, collects text from `result` and `content_block_delta` events
- Implemented `New(cfg config.AIConfig) (Generator, error)` factory in `internal/ai/factory.go`
- Files changed: `internal/ai/generator.go`, `internal/ai/anthropic.go`, `internal/ai/claude_code.go`, `internal/ai/factory.go`
- **Learnings for future iterations:**
- `content_block_delta` stream-json events have `delta.text` at the top level (not nested under `content`)
- `result` stream-json events carry the full response as a top-level `result` string field
- `ANTHROPIC_API_KEY` env var overrides config api_key in AnthropicProvider
---
## 2026-03-06 - US-012
- Implemented Grafana connector: `New`, `Discover` (extracts UID from URL, fetches dashboard panels), `FetchPanel` (re-fetches dashboard for targets, POSTs to `/api/ds/query`)
- Implemented PostHog connector: `New`, `Discover` (paginates `/api/projects/:id/dashboards/`, one item per insight tile), `FetchInsight` (GET `/api/projects/:id/insights/:id/`)
- Implemented SigNoz connector: `New`, `Discover` (lists all dashboards, one item per widget), `FetchPanel` (fetches dashboard, returns panel query config)
- Files changed: `internal/connector/grafana/grafana.go`, `internal/connector/posthog/posthog.go`, `internal/connector/signoz/signoz.go`
- **Learnings for future iterations:**
- Grafana `/api/ds/query` is a POST endpoint requiring datasource + targets from the dashboard definition
- SigNoz uses `SIGNOZ-API-KEY` header (not `Authorization: Bearer`) — check their API docs
- PostHog pagination: `next` field is an absolute URL; strip the host prefix before reusing as path
- SigNoz FetchPanel returns the panel's query definition (not executed results) since executing `/api/v3/query_range` requires time range from the caller
---
## 2026-03-06 - US-014
- Implemented `CachedGenerator` in `internal/ai/cached_generator.go`
- `NewCachedGenerator(inner Generator, s *store.Store) *CachedGenerator`
- `Generate(ctx, pipeline, teamID *int64, inputs map[string]any, annotations []store.Annotation)`: serializes to canonical JSON (Go json.Marshal sorts map keys), SHA-256 hashes for cache key, checks store, on miss calls inner generator and stores result
- Unit tests in `internal/ai/cached_generator_test.go`: cache hit, cache miss, different inputs → different hashes, teamID affects cache key
- Files changed: `internal/ai/cached_generator.go`, `internal/ai/cached_generator_test.go`
- **Learnings for future iterations:**
- `ai_cache.pipeline` has a CHECK constraint — use a valid pipeline name in tests (e.g., `concerns`, `workload`)
- `ai_cache.team_id` has a FOREIGN KEY to teams — must call `s.CreateTeam` before using a team ID in tests
- Go's `encoding/json` sorts map[string]any keys alphabetically automatically — canonical JSON is free
---