One-time trouble-ticket timeline backfill + runbook - #297
Merged
Merged
Conversation
…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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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: buildsTroubleTicketCreatedtimeline rows from unimported cache rows — dedup against the read model and within the batch — withrecordedAtoverridden to the ticket'ssubmittedAt, then hands them to the existing generic drift-guarded rebuild executor (deps.rebuildEventTimeline, reused as-is).POST /api/trouble-tickets/backfill-timeline(bearer viaconstantTimeEqual):?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}).recordedAt, in-batch dedup of byte-identical rows,beforescoping, 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