Skip to content

feat: Jira in the app, WP0 contract - #245

Merged
selluxhenner merged 1 commit into
mainfrom
feat/jira-contract
Oct 29, 2026
Merged

selluxhenner merged 1 commit into
mainfrom
feat/jira-contract

Conversation

@selluxhenner

Copy link
Copy Markdown
Collaborator

WP0 of docs/plans/2026-10-29_jira-in-app.md: the contract the Jira engine, the admin page and the lead UI are built against. No sync engine, no UI.

Needs Kevin's eye (CLAUDE.md "stop and ask" list: schema, new event type; the plan was approved on 29 Oct):

  • prisma/schema.prisma + migration 20261029100000_integrations: model Integration, @@unique([companyId, system]), cascade with the company. Integration is added to TENANT_MODELS (lib/db/scope.ts).
  • New human event project.sync {system:"jira", on}, lead or manager, project open only; ReducedProject.sync.
  • project.step.edited joins SYSTEM_EVENT_TYPES, text only. JIRA_ACTOR = "system:jira", JIRA_SOURCE = "jira".
  • A second exception to "no credentials in the app database": the Jira token and webhook secret are stored sealed, context integration:<companyId>:<system> (documented in docs/INTEGRATIONS.md).

What is in

  • features/integrations/jira/{types,settings,view}.ts: JiraSettings, Job, JiraApi, raw Jira JSON types, JiraStatusView, TestJira; the settings parser, labels, issue URLs; the status view.
  • lib/db/integrations.ts: get / save / secrets / on-off / lease / recordRun helpers.
  • connectors/vault.ts: integrationContext.
  • Docs: plan doc, INTEGRATIONS.md ("Jira in the app", secrets exception, stale "allow-list lacks the four" line removed), events table and data model.

Checked

  • tsc, eslint (no new warnings), vitest run (817 tests) pass.
  • Migration applies on a throwaway Postgres 17; prisma migrate diff shows nothing for Integration (only the old Document.search drift).
  • DB suite (ops/test-db.sh) passes, incl. the new tests/db/integrations.test.ts (lease is atomic: 4 parallel claims, 1 wins).
  • features/projects/brain.ts already skips system:* authors and only reads case comments; a test now pins it for system:jira.

No new dependencies. .github/ untouched.

@selluxhenner
selluxhenner merged commit 17f62c3 into main Oct 29, 2026
5 checks passed
@selluxhenner
selluxhenner deleted the feat/jira-contract branch October 29, 2026 01:37
selluxhenner added a commit that referenced this pull request Oct 29, 2026
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 added a commit that referenced this pull request Oct 29, 2026
WP-C of `docs/plans/2026-10-29_jira-in-app.md`: the project lead's Jira
UI. Builds on the WP0 contract (#245, merged).

**Jira row in the tools panel** (`jiraRowOf` in
`features/projects/views.ts`, pure and unit-tested):
- company Jira off: label **Link**, hint "Your admin can switch the Jira
sync on" (fixes the false "Two-way sync" claim)
- on, project not sent: **Send to Jira** ("Creates an epic in NUP and N
issues"), appends `project.sync {system:"jira", on:true}`
- synced: chip "Sending…", "Synced · NUP-1 · checked 1 min ago" or "Sync
problem: …", plus **Stop syncing** (`on:false`)
- **Link an existing issue**: key field (`isIssueKey`) + step picker,
appends human `project.linked` with the URL from `jiraIssueUrl`
- step cards and the step sheet show the key chip, or "waiting for Jira"
- buttons only for lead or manager (same `mayAppendProject` rule as the
server)
- the top "Sync" chip now only shows for a project that is actually
synced

**Data**: the `[company]` project page reads `getIntegration` +
`jiraStatusView` on the server and passes the plain `JiraStatusView` (no
secrets) to `ProjectView`. Demo / no database gives `null`.

**Demo**: with `null` in the local demo the row shows Jira as on (`NUP`,
host `jira.demo.invalid`), and `act.project.sync` plays the connector:
it appends the same `project.linked` events (epic, then one per step),
no network.

**Tests**: `tests/unit/jira-row.test.ts` (new), two new demo e2e tests
in `projects.spec.ts` (send, link, stop; employee gets no buttons).
Typecheck, lint, 855 unit tests, and the full demo suite (30 tests, run
on Edge) pass.

No dependency, schema, `.github/` or engine/admin file touched.
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