Skip to content

feat(admin): /admin/integrations, the Jira setup page - #246

Merged
selluxhenner merged 5 commits into
mainfrom
feat/jira-admin
Oct 29, 2026
Merged

selluxhenner merged 5 commits into
mainfrom
feat/jira-admin

Conversation

@selluxhenner

Copy link
Copy Markdown
Collaborator

Part of the Jira-in-app plan (docs/plans/2026-10-29_jira-in-app.md), WP-B: the admin setup page. #245 (the contract) is merged. This branch also carries the engine's public surface (feat/jira-engine, a2ab924: testJira, kickJira), because the page imports them; those files drop out of the diff once the engine PR lands.

What it adds

/admin/integrations (company stack only; the platform admin gets a 404, like /admin/assistant):

  • Connection: Cloud/DC choice (DC disabled, "later"), site URL, integration email, write-only API token (the page only learns "saved on …", the token is never sent back), project key.
  • Test connection: runs testJira and lists each JiraCheck. The epic and step type dropdowns fill from its answer; Epic/Epos and Task/Aufgabe are preselected by name.
  • What goes where: autoEpic / pullNewIssues / pullComments, and a status row per column (optional Jira status id; blank = by category).
  • Status: on/off (sets enabledAt), Sync now, last round, last error (401/403 shown as "token expired"), linked projects and steps, and the projects/steps still without a Jira twin, read from the log.
  • Webhook: URL, a generated secret shown once (sealed with the vault), remove.
  • Remove: forgets settings, token and secret (confirm step).
  • Nav: "Integrations" in the company admin, with a badge only when a running sync fails ("sync error" / "token expired").

Notes for review

  • Outside the file list I was given, two small edits were needed: components/admin/AdminNav.tsx (an icon for the new AdminPage id; the type forces it) and server/admin-context.ts (reads the Jira row for the nav badge; failure is swallowed so the layout never breaks).
  • The pure parts live in features/admin/jira-admin.ts: form to settings, webhook URL, link summary, headline. Tests are in tests/unit/admin-jira.test.ts, and admin-nav.test.ts covers the new nav entry and badge rules.
  • The token and the webhook secret are sealed with integrationContext; the actions are admin-only and limited to the stack's own company (same guard as actions/m365.ts).
  • Status ids per column are plain text fields: JiraApi has no "list statuses" call, so there is nothing to fill a dropdown from.
  • "Saved on" is the row's updatedAt: settings and token are saved together.

Checks

tsc --noEmit, eslint, stylelint and vitest run (837 tests) pass. Not run: next build, and the page has not been looked at in a browser (needs a company stack with a database).

@selluxhenner
selluxhenner merged commit 4033455 into main Oct 29, 2026
8 checks passed
@selluxhenner
selluxhenner deleted the feat/jira-admin branch October 29, 2026 01:56
selluxhenner added a commit that referenced this pull request Oct 29, 2026
WP-A of `docs/plans/2026-10-29_jira-in-app.md`: the Jira sync engine,
moved out of n8n into the app. Builds on the contract (#245), the admin
page (#246) and the lead UI (#247), which are all on main.

**What it does**
- `server/jira-sync.ts`: a loop once a minute (`JIRA_SYNC_INTERVAL_MS`
overrides it, the e2e run uses 2000) for each company with Jira on. It
takes the lease, pushes, then pulls, and records the result on the
Integration row. An hourly reconcile reads every linked issue.
`kickJira(slug)` starts a round early; it is called after a person's
project event (and a `case.decided`), and from the webhook.
- `server/jira/round.ts`: one round. It only knows a `RoundStore`, so
the unit tests run whole rounds against the fake Jira with an in-memory
log.
- Pure core in `features/integrations/jira/`: `map` (issue key to
project/step, sync mode), `plan` (state and events to jobs), `pull`
(Jira JSON to events), `status` (transition picking, hopping through In
Progress when the workflow has no direct way), `adf`.
- `server/jira/cloud.ts` (API v3, Basic auth, `/search/jql` with
`nextPageToken`, one inline wait on a short 429), `dc.ts` (stub that
says "not supported yet"), `test.ts` (`testJira`, a failed call is a
failed check).
- `POST /api/[company]/integrations/jira/webhook`: HMAC check, then only
`kickJira`. The header name (`x-hub-signature`) is marked TBD in
`server/jira/webhook.ts`; confirm it on the live site.
- One-line hooks in `instrumentation.ts` and `server/actions/events.ts`.

**Echo-loop rules, as built**
1. Push acts only on human actors (`isSystemActor`), and the last move
of a step in a batch decides.
2. Pull skips changes whose latest history is by the integration
account, and writes nothing when NextUp already shows the state (Blocked
against In Progress counts as the same).
3. idemKeys `jira:epic:<projectId>`, `jira:issue:<stepId>`,
`jira:<key>:status:<historyId>`, `jira:<key>:summary:<historyId>`,
`jira:added:<key>`, `jira:comment:<id>`. Creates look for the label
first and adopt an existing issue.
4. At most 20 creates per round. A round with nothing to do writes no
events and makes no write call to Jira (`jira-sync.test.ts` runs 5
silent rounds after each scenario).

**Decisions worth a look**
- The first round after switch-on does not replay old events (`pulledAt`
null and `pushedSeq` 0); creates come from the state, not the cursor.
- Imported comments read `Jira NUP-2: <text>` (cut to 2000), with no
author and mentions as `@someone`.
- A failed job that Jira refused for good (no transition, a 4xx) is
reported as `lastError` but does not hold the cursor; transient failures
(401, 429, 5xx, no answer) end the round and move nothing.
- Added beyond the file list: `server/jira/round.ts` and
`server/jira/webhook.ts`, so the engine and the HMAC check are testable
without a database.
- Not covered here: the e2e-db spec and the live test on
`nextup-test.atlassian.net`; and the Labels check in "Test connection"
can only prove the create/edit permission and that JQL on labels parses,
not that the create screen has the labels field.

**Run the fake Jira:** `node tests/fake-jira/server.mjs --port 4010`
(Node 22.18 or newer). It also takes `POST
/__fake/{move,rename,comment,child,429}`.

Checked: typecheck, eslint, `npm test` (88 files, 945 tests). `next
build` was not run.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant