GLOOK-38: configurable per-project Jira boards - #66
Conversation
Adds LanguageAI Research (RND) to the Glooker Projects board via a nullable per-team board_config, so SPS behaviour is unchanged. Four decisions settled against the mockup: group flat epics by researcher, swap the Rollout tab for Backlog, keep a 30-day Done window including rejected hypotheses, and render a Jira-only progress ring. The 30-day window deliberately differs from the two weeks requested on the ticket; doneWindowDays is config so it can be reversed without a code change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nine TDD tasks: BoardConfig parse/validate, the teams.board_config column across all three schema locations, parentless-epic retention plus provenance attribution, project source resolution and the team JQL builder, API wiring, mock and seed data, ProgressRing extraction with a jira-only mode, board-config-driven columns and tabs, and the Settings form. Corrects the spec on deep-linking: /projects?team= already works via useUrlState; what was missing is passing the team through to the API. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ProjectSources This test verifies that two teams both declaring project keys are correctly filtered by name, catching regressions in the team selection logic. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review finding: the 400 mapping for BoardConfigError in teams/route.ts and teams/[id]/route.ts had no automated test, so a future refactor of validateBoardConfig or a reordering of the catch blocks could silently break it. Adds route-level tests for both handlers: BoardConfigError -> 400 with the real message, and TeamDuplicateError/TeamNotFoundError still mapping to 409/404 unchanged, guarding catch-block ordering.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…board config Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… pin column parity A failed /api/projects fetch renders an error banner instead of the tab bar, so the tab-reset effect returning early on !tabData left the user with no way out. Newly reachable: ?status=Backlog is a legal URL since ALL_TABS, and the route 404s on it once no team declares jiraProjectKeys — a bookmarked ?team=Research&status=Backlog dead-ends after that config is cleared. Reset to visibleTabs(null) on error, since without a response there is no config to trust. Also gate the footer untracked-work count on !boardConfig, matching the rows and the trigger button — untrackedTeams persists across team switches, so the count advertised work a configured board never renders. Also assert widths/headers length parity in both columnLayout cases. Verified the assertion has teeth: a 7-element owner widths array still summing to 100 passes the sum check and is caught only by the parity check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ck mode Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…, drop dead code Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reframes the feature from "Research is a special team" to "point the board at any Jira project". Config moves from teams.board_config to a jira_projects table; attribution reverts to assignee-only; rings, the Done window and rejected-work handling all unify to what SPS already does. Each project names its own tab statuses rather than inferring them from status categories. Measured against live Jira: SPS In Progress is 46 epics by status but 71 by category, the extra 25 being Discovery, Rollout, Specs & Design and Ready for Dev - so category inference would double-list Rollout epics and surface pre-development work the board excludes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Eleven TDD tasks: JiraProject types and validation, the jira_projects table and service, per-project JQL generation, self-migration from the legacy env var, provenance removal, the single-project API, CRUD routes, layout retyping with the ring mode deleted, the board selector, the Settings tab plus the board-config deletions, and mock/seed. Carries a git workaround in its constraints: git commit and git status hang in this checkout, so every task commits via write-tree/commit-tree/update-ref. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
JQL literals escape with backslash, so a trailing one escapes the closing quote. Records why the guard stays a two-character denylist rather than a whitelist.
… jira-projects API
…sweep stale comments Final-review fix wave for the project-boards branch: - Seed the legacy JIRA_PROJECTS_JQL project from GET /api/jira-projects too, not only GET /api/projects, so configuring a project via Settings before anyone loads the board no longer permanently strands the SPS seed. - Gate ensureSeedProject's write on the org existing in teams or reports, so an unauthenticated GET /api/projects?org=<anything> can no longer mint jira_projects rows for arbitrary org strings. - Reject interior \n/\r in status names at save time and at point-of-use in buildProjectJql, so a broken status surfaces as a 400 on save instead of a 500 from the board. - Resolve /api/projects' requested project from the already-fetched configured list instead of a second DB round trip. - Sweep stale comments referencing deleted buildTeamJql/provenance concepts; revert a no-op reflow in teams/service.ts back to its merge-base form. - Add admin-denial tests for PUT/DELETE /api/jira-projects/[id]. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
🔁 review-loop — 1 loop, 2 personasAutomated multi-lens review (
Cross-cutting:
|
| Location | Meaning | Still legitimate? |
|---|---|---|
src/app/projects/page.tsx:10 |
page-level feature gate | ❌ |
src/lib/env-validation.ts:127 |
required-var warning when JIRA_ENABLED=true |
❌ |
src/lib/projects/untracked.ts:116 |
untracked-work source | ✅ |
src/lib/jira-projects/seed.ts:9 |
one-shot migration source | ✅ |
Reducing it to the last two removes C1 below and makes the feature self-describing. Patching page.tsx alone fixes the symptom and leaves the boundary undrawn.
Related: there are two independent sources of truth for "the default project" — the server picks configured[0] (api/projects/route.ts:43), the client independently picks projectList[0] (projects-content.tsx:599). They agree today only because both order by position, project_key. An isDefault/isLegacy flag on the response project object collapses this to one server-side decision and fixes I4 as a side effect.
🔴 Critical
[C1] /projects still hard-gates on the env var this PR replaces — src/app/projects/page.tsx:10
The page renders "JIRA_PROJECTS_JQL is not configured. Set this environment variable to enable the projects view." before ProjectsContent ever mounts. The API layer moved to the jira_projects table; this gate didn't move with it.
A deployment that configures boards entirely through Settings → Projects — the stated point of GLOOK-38 — still sees the env-var error page. Conversely the route's own new 404 copy, 'No Jira projects configured. Add one in Settings → Projects.', is unreachable in exactly the state it describes. An operator who migrates and then unsets the now-legacy var kills a working board.
Gate on
JIRA_ENABLEDonly and letProjectsContent/GET /api/projectsown the "no projects configured" state, which they already render.
Found independently by both code reviewers.
🟡 Important
[I1] Deleting the last configured project silently resurrects it — src/app/api/jira-projects/route.ts:18, src/lib/jira-projects/seed.ts:36
GET /api/jira-projects runs ensureSeedProject(org) on every call, and that re-seeds whenever listJiraProjects(org) is empty. ProjectsTab.del() calls load() immediately after the DELETE — which is that same GET.
On the current SPS deployment an admin deletes the only row and watches it reappear before the click finishes. Worse: an admin with one manually-created RND (owner hierarchy) row deletes it to reconfigure and gets an SPS row they never asked for — different key, different hierarchy, middleStatus: 'Rollout'. There is no way to reach a zero-project state while the env var is set. No test covers delete-then-list.
Make the migration one-shot rather than state-derived — gate on a marker row or config key instead of
existing.length === 0, or runensureSeedProjectonly fromGET /api/projectsand never from the Settings list endpoint.
[I2] Transitioning to any unnamed status pins the epic to the Done tab indefinitely — src/app/projects/projects-content.tsx:186
targetTab falls back to 'done' for anything that is not activeStatus or middleStatus. The comment asserts Done-category is "the only other kind of transition the board offers", but GET /api/projects/[key]/status returns client.getTransitions(key) unfiltered (route.ts:19), so the dropdown lists every transition in the workflow.
On SPS (active = "In Progress", middle = "Rollout") a user moves an epic to Ready for Dev or Blocked. targetTab resolves to 'done', the entry lands in pendingTransitionsRef, and applyPendingTransitions prepends that epic to the Done tab on every subsequent fetch — Jira will never return it there, so it's re-injected indefinitely, showing status "Blocked" at the top of Done until reload.
This is a regression. Pre-diff, const targetTab = toStatus as StatusTab produced a value matching no tab, so the epic was correctly stripped from all tabs and injected into none.
Resolve to
'done'only when the target is known Done-category — the transitions payload can carrystatusCategory— and otherwise record "strip from all tabs, inject nowhere" (e.g. atargetTab: nullcase inapplyPendingTransitions).
[I3] The legacy-JQL migration is an unverified regex against a production-only value, with no fallback and no diagnostics — src/lib/jira-projects/seed.ts:9
GET /api/projects no longer reads JIRA_PROJECTS_JQL at all. The entire upgrade path runs through parseLegacyJql, which requires exactly project = KEY and status = "Name". Anything else — project in (SPS, TCM), status in ("In Progress","Rollout"), statusCategory = "In Progress", a single-quoted status — returns null, ensureSeedProject no-ops, and the route 404s. A board that worked yesterday returns an error banner, and nothing in the logs explains why: ensureSeedProject logs only on exception, never on a null parse.
Three further concrete cases:
'project = ABC AND issuetype = Epic AND status = "In Dev" AND component = Core'silently loses thecomponentfilter — the migrated board shows a strictly wider epic set than the operator configured, with no warning.middleStatus: 'Rollout'is hardcoded for every seeded project. A workflow with no such status getsbuildProjectJqlemittingstatus = "Rollout", Jira rejects the field value, the route 500s, and the tab-reset effect bounces the user back to the active tab on every click.env-validation.ts:127documents the example as… statusCategory = "In Progress"— a formparseLegacyJqldeliberately refuses. An operator who followed the documented example gets no migration at all.
⚠️ Verify the deployedJIRA_PROJECTS_JQLvalue before merge. Independently: seedmiddleStatus: null(a two-tab board is the honest default for an unknown workflow), log a warning when the parse returns null on a non-empty var, and either refuse to migrate a JQL carrying clauses beyondproject/issuetype/statusor name the dropped clauses in a warning.
[I4] isLegacyProject infers provenance from list position, which is not a contract — src/app/projects/projects-content.tsx:599
The guard is project.projectKey === projectList[0].projectKey && project.hierarchy === 'goal-initiative'. The property it's trying to express — "this is the project JIRA_PROJECTS_JQL names", since getUntrackedWork derives its keys from that var (untracked.ts:116, falling back to a hardcoded ['SPS']) — is recorded nowhere.
projectList is ordered by position, project_key, and nothing pins the seeded row to position 0: PUT /api/jira-projects/[id] accepts an arbitrary position, creates use position: projects.length (so delete-and-re-add reorders), and a deployment that never auto-seeded makes whatever the admin adds first "legacy". Concretely — admin adds RND, deletes SPS to fix a typo, re-adds SPS at position 1: RND is now projectList[0], so the "Not in Project" rows, the untracked footer count, and the "load untracked" button all vanish from the SPS board with no error. Untracked work stops being visible anywhere. This is precisely the failure the comment above the guard says it prevents.
Mark the row whose
projectKeymatchesparseLegacyJql(process.env.JIRA_PROJECTS_JQL)?.projectKeyserver-side and have the client key off that flag.
[I5] The project selector renders blank on every cold load — src/app/projects/projects-content.tsx:718
selectedProject defaults to '' (meaning "let the server pick"), but the <select> renders one <option> per project with value={p.projectKey} — none carries value="". React sets select.value = '', no option matches, selectedIndex becomes -1.
Every first visit to /projects without ?project= shows an empty dropdown beside a fully populated board. On single-project deployments it's blank 100% of the time until clicked. With two or more projects the user can't tell which board they're looking at, and selecting the already-displayed first project looks like a no-op. No test covers the selector.
Seed
selectedProjectfromtabData.project.projectKeyonce the response lands, or render an explicit option for the empty value.
[I6] Dropping the parentTypeName === 'Initiative' filter is a global behaviour change, not a per-project one — src/lib/projects/service.ts:55 · needs author confirmation
fetchProjectEpics used to drop every epic without an Initiative parent; it now keeps all of them unconditionally. The signature was reformatted but takes no hierarchy argument, so the goal-initiative board gets flat-board semantics too.
The existing production board's row set changes on merge — epics with no parent, and epics whose parent is some other type, now appear under the — goal bucket. The jira_projects row already carries hierarchy, the one piece of config that should decide this, and it never reaches this function.
Is the widened SPS board intended? If this is only wanted for
hierarchy: 'owner', pass the project (or akeepParentlessflag) intofetchProjectEpics.
🔵 Suggestions
[S1] DROP COLUMN board_config will fail on every startup forever, and swallows every error — src/lib/db/mysql.ts:320, src/lib/db/sqlite.ts:321
board_config does not exist at the merge base — git grep board_config 5f87682 -- src schema.sql is empty. The column was added in d4d0a54 and dropped in 0d6b78d, both inside this branch; no released deployment ever had it. Every real DB will now run a DDL that always fails, on every boot, permanently.
The MySQL variant is .catch(() => {}), unlike every other ALTER in that file, which inspects err.code before logging — so it also swallows connection and permission errors. That is the exact silent-DDL-failure mode CLAUDE.md calls out from the 2026-08-11 outage. jira-projects-schema.test.ts:26 then asserts the dead statement is present, pinning it in place.
Drop both statements and the test that pins them. If they must stay for dev machines that ran the intermediate commits, at minimum match the file's convention and log anything that is not
ER_CANT_DROP_FIELD_OR_KEY.
[S2] jira_projects mutations are keyed on a bare id and never see an org — src/lib/jira-projects/service.ts:68, :88
The table's real identity is (org, project_key), but updateJiraProject(id, …) and deleteJiraProject(id) take only the UUID and neither route passes an org — so the boundary the schema enforces can't be enforced by the service, is invisible in the type signature, and is impossible to test for. Harmless while AUTH_ADMIN_GROUP grants global admin; it becomes an authorization gap the moment admin scope goes per-org. deleteJiraProject also returns success unconditionally while PUT 404s on a missing row, and duplicate detection relies on err.message.includes('UNIQUE'), which also matches an id collision.
Thread
orgthrough both service functions and both routes,WHERE id = ? AND org = ?, and 404 the delete when no row matched.
[S3] ensureSeedProject is a migration wearing a request handler — src/lib/jira-projects/seed.ts:43
It costs a SELECT on every board request forever, and /api/projects then repeats listJiraProjects(org) immediately after — two identical queries per request. The repo already has a migration seam in db/mysql.ts / db/sqlite.ts; in the read path it never retires and there's nowhere to see whether it ran. The org-existence guard mitigating write-on-GET is thoughtful, but the shape is wrong. Acceptable as an interim — worth a follow-up.
[S4] Status colours are still hardcoded to the SPS vocabulary the PR set out to remove — src/app/projects/projects-content.tsx:1042, :1051
The status dot and transition dropdown still switch on the literals 'Done', 'Rollout', 'In Progress'. Tabs, labels and JQL now read in each project's own vocabulary; the colour mapping doesn't, so a board configured with Backlog / Rejected renders every row grey. Same hidden coupling this change otherwise removes, in the last place it survives.
Derive from
project.activeStatus/project.middleStatus/ everything-else-is-done — the same three-waytargetTabresolution already uses.
[S5] projects-api-board.test.ts asserts through a mock the route no longer calls — src/lib/__tests__/unit/projects-api-board.test.ts:5, :18, :45
The route resolves the project from the already-fetched configured list, so getJiraProject is now called by nothing in src/ outside its own tests — yet several cases still set mockGet.mockResolvedValue(RND) / mockResolvedValue(null) as their stated mechanism. it('404s for an unknown project key') passes because 'NOPE' is absent from mockList, not because mockGet returned null: swapping the route back to a getJiraProject lookup that returns a row for NOPE would leave this test green while the route 200s.
Drop the
getJiraProjectmock and drive every case frommockList. Either deletegetJiraProjectfromservice.tsor wire it to a caller.
[S6] ProjectEpic.projectKey is unused and adds a second derivation of project identity — src/lib/projects/service.ts:66
projectKey: epic.key.split('-')[0] is added to the shared ProjectEpic shape and covered by a test, but nothing consumes it — the client's own ProjectEpic doesn't even declare it, and isLegacyProject uses the jira_projects row's key instead. It establishes a string-splitting derivation of "which project is this" alongside the authoritative jira_projects.project_key, with no caller keeping the two honest.
[S7] ProjectsTab.del() ignores the response status — src/app/settings/projects-tab.tsx:294
await fetch(url, { method: 'DELETE' }).catch(() => {}) — neither res.ok nor a network error reaches the user, and the confirmation panel closes either way. An admin whose session lapsed to viewer clicks Delete, gets a 403, sees the panel close as if it worked, and the row reappears via load() — indistinguishable from the resurrection in I1, so neither failure can be diagnosed from the UI.
Check
res.okand route through the existingsetErrorpath, assave()already does.
[S8] Pre-PR tab URLs silently render the wrong tab — src/app/api/projects/route.ts:19 · defer
The status param vocabulary changed from In Progress/Rollout/Done to active/middle/done, and both the route (TABS.includes(...) ? ... : 'active') and readValue's enum branch fall back to the default instead of rejecting. A teammate opening a previously shared /projects?org=X&status=Done link sees In Progress epics while the address bar still reads status=Done, with nothing indicating the parameter was ignored.
[S9] docs/projects-page.md still documents the env var as the configuration mechanism — :12, :22, :168 · defer
It describes JIRA_PROJECTS_JQL as required and as the query the board runs; neither is true after this change. It's the only prose description of this subsystem and it's now actively misleading about how to configure it.
📉 Phase 1 minimalist gate: Bloat
Scope, not correctness. 6,871 of 9,446 lines (73%) are planning artifacts, and 46% of the total diff documents an approach this same PR deletes.
-
Superseded-design artifacts committed as-is.
docs/superpowers/plans/2026-08-14-glook-38-research-board.md(2,977 lines) plans theteams.board_configper-team design.b4c90d9supersedes that design;70d72bfdeletes its code. The plan stays. Repo convention across 38 prior plans is one plan per ticket, largest 1,787 lines — this is two, and the first is for a design that no longer exists.mockups/glook-38-research-board.html(1,338 lines) creates a new top-levelmockups/directory with no precedent in the base tree, and this PR's own spec labels it: "mockups/glook-38-research-board.html— stale, shows the superseded design." A file the author annotates as stale in the same PR that adds it isn't scope. The 08-14 spec was correctly added and deleted within the branch; the plan and mockup should have followed it. -
Migration for a column that never shipped — see S1 above.
-
Legacy-JQL auto-seed (
seed.ts, 69 lines + 131 test lines). Justified as "so an existing deployment keeps its board without operator action" — but this PR shipssettings/projects-tab.tsx, where that same row is three text fields. The second call site is justified purely on an ordering hypothetical. Cost carried forever: an extraSELECTon every board and Settings load, plus a security caveat that exists only because a one-time migration was wired into two read paths. -
Refactors that ride along.
progress-ring.tsx(83 lines) + its test (82 lines):fe32af5extracted it "and add a jira-only ring mode";a4dd5f2removed the ring mode — what remains is a no-behaviour-change extraction plus a new test, unrelated to configurable boards. TheTeamsTab.save()if/else collapse insettings/page.tsxis a drive-by in a different tab.board-layout.ts'shierarchy: 'owner'mode (111 + 132 test lines) is a second rendering mode — the load-bearing fix for flat projects is keeping parentless epics inprojects/service.ts.
Scoped to intent: schema.sql + CREATE TABLE jira_projects in both dialects (no DROP) + jira-projects/{types,service,jql}.ts + the two API routes + the /api/projects rewiring + the project selector + settings/projects-tab.tsx + keeping parentless epics — with tests on validation, JQL construction, the CRUD API, and board resolution. One plan doc, one spec. Roughly 1,900–2,200 lines instead of 9,446.
Reviewing "safe to merge" against 9,446 lines where three quarters is prose — including 4,315 lines describing a design this PR deletes — spends the reviewer's attention where the risk is not.
Verdict
| Reviewer | Verdict |
|---|---|
| Phase 1 minimalist gate | Bloat — hand back for rescope |
| Sr. Architect | With fixes |
| Senior Dev | No — with fixes |
Not safe to merge as-is. C1 makes the headline capability unreachable without the very env var it deprecates. I1 and I2 are user-visible wrong-state bugs on paths this PR adds, and I2 is a regression against base. I3 is an unverified one-shot migration against a production-only value with no diagnostics. The JQL-building, validation, and schema work is solid and well covered — including a properly closed injection surface at both write and build time — and the gaps are concentrated in the migration path and client-side board state.
🤖 Generated with Claude Code · sl-core-dev:review-loop
🔎 Smartling code reviewAutomated review using the centralized Issues🔴 Critical
// page.tsx — drop the env gate; let the API's 404 drive the empty state
export default function ProjectsPage() {
if (process.env.JIRA_ENABLED !== 'true') notFound();
return <ProjectsContent />;
}🟡 Warning
if (tabData?.project && !selectedProject) setSelectedProject(tabData.project.projectKey);
Documentation — 🔵 Suggestion
🟣 Question
Recommendations
AssessmentReady to merge? With fixes. Reasoning: The core design (per-project rows, per-project JQL, tab/column layout derivation, injection defence) is well-built and well-tested, but 🤖 Generated with Claude Code · |
…ct-board docs - Remove docs/superpowers/plans/2026-08-14-glook-38-research-board.md (per-team board_config design superseded within this branch) - Remove mockups/glook-38-research-board.html (stale Research-team mockup, and the only tracked file under mockups/) - Rewrite docs/projects-page.md: boards are configured per project via Settings -> Projects (jira_projects table), not JIRA_PROJECTS_JQL; add the exact-named-status vs statusCategory invariant (46 vs 71 SPS epics); mark JIRA_PROJECTS_JQL legacy/optional - CLAUDE.md: document the jira_projects table, buildProjectJql validation (denylist, not whitelist), and the dual-DB requirement Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…urrecting, tighten jira-projects API Removes the JIRA_PROJECTS_JQL gate from /projects (Settings-configured boards were unreachable behind it), memoizes the legacy-var seed per org per process so deleting the last configured project does not bring it back, changes the seed default to a two-tab board with a null middle status instead of a hardcoded Rollout status, adds console.warn coverage for unparseable or scope-widening legacy JQL, drops the permanently-dead ALTER TABLE teams DROP COLUMN board_config migration, exposes isLegacy on the GET /api/projects project object, removes the dead ProjectEpic.projectKey and getJiraProject, and fixes malformed-JSON and DELETE-404 handling on the jira-projects [id] route. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…cope board state per project The board's status dropdown offers every transition Jira's workflow allows — nine on SPS — while the board itself shows three tabs. `executeTransition` resolved anything that was neither the active nor the middle status to the Done tab, so five SPS destinations (Backlog, Discovery, Blocked, Specs & Design, Ready for Dev) were recorded as moves to Done. `applyPendingTransitions` prepends, and Jira's `statusCategory = "Done"` JQL never returns a Blocked epic, so it was re-injected on every subsequent fetch until reload. `getTransitions` now plumbs `to.statusCategory.key` through as `toStatusCategory`, and `resolveStatusTab` makes "maps to no visible tab" a first-class `null` outcome: strip from every tab, inject nowhere, let the next fetch decide. The same three-way resolution now drives the status dot colours, which were hardcoded to the SPS literals and rendered every dot grey on a board with any other vocabulary. Also: - clear `ringStats` alongside the pending-transitions registry on org/project change: `maxVolume` and `avgCommitsPerJira` are aggregates over the whole map, so a small board's rings were scaled against a large board's maximum - reset the team/goal/initiative/search filters on a project switch, keyed off an actual change so a `?project=…&goal=…` deep link survives - the shared SWR fetcher throws the API's `error` string instead of the bare status code, so "No Jira projects configured. Add one in Settings → Projects." reaches the user instead of "Error: 404" - Settings → Projects reports failed loads and deletes through `setError` instead of swallowing them; a failed delete keeps the confirm panel armed - import `ProjectEpic` from the service rather than remirroring it, and take legacy-project detection from the server's `isLegacy` flag - `POST /api/jira-projects` answers 400 on a malformed JSON body, matching `[id]/route.ts` JQL construction is untouched: the active and middle tabs still build an exact named status, never a status category. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Thanks — both reviews caught real defects, including one that blocked the feature outright. Addressed across four commits ( Tests: 115 suites / 1077 tests → 118 / 1136. Fixed[C1] the env-var gate — confirmed, and it was the important one. [I2] transition misclassification — you were right, and my earlier judgement was wrong. I'd previously accepted the [I1] delete-then-list resurrection. Removed [I3] silent migration. [I4] positional provenance. [S1] dead [S4] hardcoded status colours, [S5] the dead Also from the second review: ring stats no longer leak across boards (cleared alongside Scope / "Bloat" — agreed, acted on. Deleted One finding I'm pushing back on[I5] the project selector is not blank. Both reviews concluded Answered[I6] widened SPS board — intentional, and smaller than it reads. Of 41 SPS
Deferred, tracked in GLOOK-40
GLOOK-40 also carries the Jira-driven dropdowns for Settings → Projects, which would have prevented the misconfiguration that motivated several of these: Verified on a rebuilt container against production Jira: SPS 41/7/44, RND 10/46/7, unchanged from before the fixes. |
Closes GLOOK-38.
Ben Loy asked for the LanguageAI Research team (Jira project
RND) to appear in Glooker as a board like the other teams', replacing RND board 646. Rather than special-casing Research, this generalises the Projects page: any Jira project can be registered and rendered as a board.What changed
The
/projectsboard previously served exactly one Jira project, hardcoded through theJIRA_PROJECTS_JQLenv var. It's now driven by a new org-scopedjira_projectstable, with a project dropdown on the board and a Settings → Projects admin tab for CRUD.Each project carries its own display name, two named statuses, and a hierarchy mode:
goal-initiative(7 cols)owner(6 cols)goal-initiativerenders the Goal → Initiative → Epic hierarchy SPS uses;owneris for flat projects that don't nest, which is why RND previously rendered an empty table — parentless epics were being dropped. Team is derived from the epic's assignee. Progress rings are now unified on commits for every project, rather than Jira-only for some.The key design decision
The two configurable tabs generate JQL with an exact named status, never a status category:
This is load-bearing, and measured rather than assumed. On production Jira:
status = "In Progress"→ 46 SPS epicsstatusCategory = "In Progress"→ 71The extra 25 sit in Discovery, Rollout, Specs & Design and Ready for Dev. Using the category would have silently inflated the existing SPS board by 54%. The done tab is the deliberate exception — a category plus a rolling 30-day window, which also delivers Ben's request that finished work linger ~2 weeks.
Migration
Zero admin action. On first request
ensureSeedProjectparses the existingJIRA_PROJECTS_JQLinto the first row, so a deployment upgrading to this branch keeps its board unchanged. Verified live: SPS still returns exactly 46 epics after migration.The per-team
board_configcolumn from an earlier iteration is dropped in both DB backends. New table added tosqlite.ts,mysql.tsandschema.sql, with no pinned charset — verified the created table inherits the schema default collation (a pinned charset breaks FKs on dev's utf8mb3 database while passing locally; errno 3780).Security
GET /api/projectsis not admin-gated — viewers need to read the board — but it now writes via the seed. Seeding is therefore bounded to orgs already present inteams/reports, so an arbitrary?org=value can't mint rows. Verified against a running server: bogus orgs return 404 with the row count unchanged.Project keys and status names are interpolated into quoted JQL literals, so both are validated at save time and re-validated at point of use, which catches rows written directly to the DB. Keys must match
/^[A-Z][A-Z0-9_]*$/; statuses reject",\,\n,\r. That's deliberately a denylist, not a whitelist —Specs & DesignandReady for Dev (v2)are real statuses that must keep working.Admin routes are gated with
requireAdmin, and every handler is wrapped inwithRequestLog.Verification
Deployed locally against real Smartling Jira and MySQL. Every tab count independently confirmed equal to live Jira for the identical JQL:
tsc --noEmitcleannext buildgate)activeStatusandprojectKeyrejected with 400 by the running server; legitimateSpecs & DesignacceptedReviewer notes
src/lib/jira-projects/jql.tsis the highest-risk file — the named-status invariant lives there.board_configdesign that was later removed. The net diff is the thing to review;board_configsurvives only as a DROP COLUMN migration.Known limitation
A project that doesn't use the Epic issue type renders an empty board with no explanation. Found while testing with
SLIT, which has zero epics and uses Task/Security/Sub-task instead. Behaves per spec (the board is epic-based by design) but the empty state deserves a follow-up.🤖 Generated with Claude Code