Skip to content

One-time trouble-ticket timeline backfill + runbook - #297

Merged
JamesMoulang merged 9 commits into
mainfrom
feature/tt-backfill
Sep 21, 2026
Merged

JamesMoulang merged 9 commits into
mainfrom
feature/tt-backfill

Conversation

@JamesMoulang

Copy link
Copy Markdown
Collaborator

Stacked on #296. The one-time historical catch-up for the trouble-ticket → event-log migration, mirroring the quiz migration's #276.

  • src/trouble-tickets/backfill-timeline.ts: builds TroubleTicketCreated timeline rows from unimported cache rows — dedup against the read model and within the batch — with recordedAt overridden to the ticket's submittedAt, then hands them to the existing generic drift-guarded rebuild executor (deps.rebuildEventTimeline, reused as-is).
  • POST /api/trouble-tickets/backfill-timeline (bearer via constantTimeEqual):
    • ?dryRun=true → {totalCandidates, wouldInsert, alreadyImported, sample} with no writes. This replaces the quiz migration's dry-run page; ticket volume is small enough that a JSON summary suffices.
    • ?before=<ISO date> → canary run scoped to submissions strictly before the date (the natural scope for a single-sheet dataset).
  • docs/trouble-ticket-migration.md: the prod runbook — snapshot, quiet window, dry-run, one curl (expect Fly's proxy to time out; don't re-run), verify via idempotent re-run ({"rewrote":false,"inserted":0}).
  • Tests: weaving a 2021 ticket ahead of newer events at its true recordedAt, in-batch dedup of byte-identical rows, before scoping, dry-run writes nothing, idempotent re-runs.

Ordering gate: the going-forward poller PR must not merge until this backfill has run and been verified on prod — the append path claims row hashes at recordedAt = now, irreversibly.

🤖 Generated with Claude Code

JamesMoulang and others added 2 commits September 18, 2026 14:48
…driver

First stage of migrating trouble tickets from the Google Sheets poll into
the event log, mirroring the training-quiz migration (#275). Salvaged from
the stale feature/trouble-tickets-board branch and aligned to the quiz
conventions:

- TroubleTicketCreated event storing raw sheet facts only; member and
  equipment resolution happen downstream.
- record command with dedup by rowHash; codec stricter than the event.
- troubleTickets read-model table (unique rowHash) projecting status Todo,
  title from the issue text, and equipment resolved by name (null =
  Unassigned).
- Candidate reader + stable rowHash over parsed answer *values* (not the
  cached JSON string, whose keys are the sync worker's question strings and
  would otherwise become a frozen wire format).
- Sequential ingest driver attributed to the system actor. Deliberately not
  wired into the sync worker and not exposed over HTTP: appending claims
  each row's hash at recordedAt=now, which would prevent the one-time
  timeline backfill from weaving rows in at their historical submittedAt.
  The poller is wired up only after the backfill has run on prod.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Weaves cached trouble-ticket rows into the event log at their historical
submission times, reusing the generic drift-guarded rebuild executor from
the quiz migration. Exposed as POST /api/trouble-tickets/backfill-timeline
(bearer-checked), with ?dryRun=true reporting counts and a sample without
writing (replacing the quiz migration's dry-run page - ticket volume is
small) and ?before=<ISO date> scoping a canary run by date.

docs/trouble-ticket-migration.md is the prod runbook, adapted from the
quiz one: snapshot, quiet window, dry-run, one curl (expect the proxy
timeout), verify by idempotent re-run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
JamesMoulang and others added 7 commits September 18, 2026 15:10
Review finding: after the timeline weave, historical tickets replay before
the EquipmentAdded events for the machines they name, so name resolution
at TroubleTicketCreated time found nothing and every backfilled ticket
landed permanently in the Unassigned bucket. When equipment is added, link
any still-unresolved tickets whose submitted string matches its name.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Without this, a scoped dry-run shows wouldInsert < totalCandidates with no
explanation, which reads as a problem mid-runbook.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Audit finding: soft-deleting a TroubleTicketCreated event (e.g. for a
data-removal request) removed its rowHash from the read model, so the
ingest dedup saw the cached sheet row as new and silently re-imported the
same PII as a fresh event on the next sync cycle. Deleted ticket events
now record their rowHash in a suppression table that hasRowHash also
consults.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Validate INSERTS before rewriting: each backfill payload must round-trip
  the DomainEvent codec (one undecodable event in the log takes down every
  getAllEvents call) and carry a finite recordedAtMs (a NaN makes the sort
  comparator inconsistent, which can silently permute EXISTING events). The
  generic executor also now refuses non-finite insert timestamps.
- Refuse to rebuild when deleted_events references a non-existent event
  index: the previous fallback kept the old number, which after renumbering
  belongs to a different event - silently soft-deleting it.
- Drift guard also compares the events row count, closing the theoretical
  gap where index gaps exactly offset a concurrent rewrite's inserts.
- Refresh the read model before planning (dedup reads it; it is eventually
  consistent), and reject dryRun values other than exactly 'true' instead
  of silently running the destructive path.
- Runbook: name the real PR numbers, add the post-backfill process restart
  step (incremental refresh is index-based and wrong after renumbering
  until restart), and advise against re-running once the poller is live.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@JamesMoulang
JamesMoulang changed the base branch from feature/tt-events to main September 18, 2026 14:52
@JamesMoulang
JamesMoulang merged commit ca562ff into main Sep 21, 2026
11 of 12 checks passed
@github-project-automation github-project-automation Bot moved this from Options to Done in Makespace Members App Sep 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

1 participant