We launched the operator portal for the AI video ad generation platform on a Thursday. By Saturday morning, real users had surfaced twelve distinct issues — some broken, some confusing, some quietly leaking the wrong data into the UI. I shipped twenty pull requests in forty-eight hours, batched by theme, and recorded every fix in a living roadmap document with shipped/planned status flips.
This post is the playbook for that sprint: how to triage post-launch feedback without drowning in one-off patches, and why the "honest default" pattern matters as much as the reliability fixes.
Day zero: what launch actually reveals
Pre-launch testing covered happy paths. Launch exposed:
- Reliability regressions — jobs that restored on prod but not staging, navigation state lost on refresh
- Data leaks in the UI — the reference (competitor) frame appearing in every model column of the scene grid
- Honesty gaps — thumbnails falling back silently to wrong assets, status chips implying success when generation partially failed
- UX friction — run-detail pages that hid real failures, run-history tables where long names destroyed column layout
- Recovery gaps — no way to reopen old runs from cloud storage after a state wipe
The instinct is to fix everything immediately in random order. That produces twenty PRs that are hard to review, hard to rollback, and impossible to summarize for stakeholders. Instead, I sorted every issue into three buckets and batched fixes by bucket.
| Bucket | Question | Examples from launch |
|---|---|---|
| Broken | Does it fail or lose data? | Reliability regressions, old runs won't reopen, verbatim script not preserved |
| Confusing | Does it work but mislead? | Reference frame in model columns, dishonest default chips, jargon-heavy create form |
| Missing | Does the user need something we didn't build? | Folder-driven run history, run-detail charts, breadcrumb navigation |
Batch 1: reliability regressions + persistent navigation
The first batch targeted failures that made the product unusable, not merely ugly.
Persistent navigation. Operators lost their place when refreshing mid-run — the portal reset to the run list instead of the run-detail view they were inspecting. The fix stored the active run ID and tab selection in URL search params, not React state alone. Refresh became idempotent: same URL, same view.
// URL-driven nav state — survives refresh and share
const runDetailUrl = (runId: string, tab: RunDetailTab) =>
`/portal/runs/${runId}?tab=${tab}`;
function RunDetailPage({ runId }: { runId: string }) {
const [searchParams, setSearchParams] = useSearchParams();
const activeTab = (searchParams.get('tab') as RunDetailTab) ?? 'overview';
const setTab = (tab: RunDetailTab) => {
setSearchParams({ tab }, { replace: true });
};
// ...
}
Reliability regressions. Two launch-week changes had introduced subtle breakage: restore-on-retry consulted stale checkpoint metadata, and cancellation signals weren't propagating to in-flight vendor polls. Both were fixed before touching any portal UX — users can't evaluate a redesign if jobs silently fail.
Batch 2: the reference frame leak
The most embarrassing launch bug: the scene comparison grid showed the reference (competitor) video's keyframe in every model column, not just the reference column.
Root cause was a shared thumbnail resolver that defaulted to referenceKeyframeUrl when a model-specific generated frame wasn't cached yet. During poll-in-progress states, every column rendered the competitor's frame. Operators thought all models had produced identical output — or worse, that our platform was just repackaging the reference video.
// BEFORE — wrong default poisons every column
function resolveSceneThumbnail(scene: SceneGridCell): string {
return scene.generatedFrameUrl
?? scene.referenceKeyframeUrl // leak: competitor frame everywhere
?? PLACEHOLDER;
}
// AFTER — honest per-column state
function resolveSceneThumbnail(scene: SceneGridCell, column: GridColumn): string {
if (column.role === 'reference') {
return scene.referenceKeyframeUrl ?? PLACEHOLDER;
}
if (scene.generationStatus === 'pending') {
return PLACEHOLDER; // spinner, not competitor frame
}
return scene.generatedFrameUrl ?? PLACEHOLDER;
}
This is the honest default pattern: when data isn't available, show a placeholder or explicit status — never silently substitute data from a different context. Users assume the UI is truthful. Violating that assumption erodes trust faster than a loading spinner.
Batch 3: verbatim scripts and prod restore
Two data-integrity fixes shipped together:
- Verbatim provided-script — when an operator pasted an exact legal script, the planner was paraphrasing it during pacing auto-fit. The fix added a
verbatim: trueflag that bypasses rewrite and routes overflow to pitch-preserving time-compression instead. - Old runs reopen on prod — production had migrated to state-only restore (metadata in DB, artifacts in Google Drive) but the restore path still expected local disk paths from dev. The fix implemented on-demand Drive streaming for artifact hydration on reopen.
async function restoreRun(runId: string): Promise<RestoredRun> {
const meta = await db.runs.findById(runId);
if (!meta) throw new RunNotFoundError(runId);
// State-only: metadata from DB, blobs streamed from Drive on demand
const artifacts = await driveArtifacts.list(meta.driveFolderId);
return {
...meta,
clips: artifacts.clips.map(a => ({
sceneId: a.sceneId,
streamUrl: `/api/media/stream?fileId=${a.fileId}`, // no full download
})),
plan: await driveArtifacts.readJson(meta.driveFolderId, 'plan.json'),
};
}
Operators who had completed runs before the migration could finally reopen them in production without a manual re-import.
Batch 4: thumbnails, chips, and breadcrumbs
This batch was pure honesty and navigation polish:
- Reference-thumbnail Drive fallback — when the reference keyframe wasn't locally cached, fetch from Drive instead of showing a broken image
- Honest default chip — status chips now distinguish "generating," "partial failure," and "complete" instead of a green "done" badge when two of twelve scenes failed
- Breadcrumbs — run list → run detail → scene drill-down, each level linked
The default chip change was small in code but large in operator trust:
| Run state | Old chip | New chip |
|---|---|---|
| 12/12 scenes succeeded | Complete ✓ | Complete ✓ |
| 10/12 succeeded, 2 failed | Complete ✓ | Partial — 2 failed |
| Still generating scene 8 | Complete ✓ (premature) | Generating… 7/12 |
| Cancelled mid-run | Complete ✓ | Cancelled at scene 5 |
Batch 5: run-detail redesign mid-flight
Operators needed to diagnose failures, not just see a red badge. The run-detail page got a tabbed redesign:
- Overview — run metadata, honest status chip, timing summary
- Scenes — grid with per-model columns (reference frame leak fixed)
- Timeline — Recharts visualization of stage durations and vendor calls
- Reference — embedded reference clip for side-by-side comparison
- Failures — real error messages with vendor, scene, and retry history — not a generic "something went wrong"
Restore speedup shipped in the same PR: lazy-loading scene artifacts instead of blocking the detail page on a full Drive folder scan. The overview tab renders in under 500ms from DB metadata; thumbnails stream in as they're resolved.
Batch 6: run-history table and folder recovery
Bound cells. Long run names and folder paths were blowing out the run-history table, hiding the status and date columns entirely. Fixed with text-overflow: ellipsis, max-width constraints, and full-name tooltips on hover.
Folder-driven run history. Before this fix, run history came only from the application database — runs created before a DB migration or wiped by a bad deploy were gone. The recovery path indexed Google Drive folders directly: each run's artifact folder name encodes the run ID and timestamp. A background indexer reconciled Drive folders against DB records and surfaced "orphan" runs in the history table with a "recovered from storage" badge.
async function indexRunHistory(): Promise<RunHistoryEntry[]> {
const dbRuns = await db.runs.listRecent({ limit: 100 });
const driveFolders = await drive.listRunFolders({ parentId: RUNS_ROOT });
const dbIds = new Set(dbRuns.map(r => r.id));
const orphans = driveFolders
.filter(f => !dbIds.has(parseRunId(f.name)))
.map(f => ({
id: parseRunId(f.name),
source: 'drive-recovery' as const,
recoveredAt: new Date(),
// metadata parsed from folder manifest if present
}));
return mergeAndSort(dbRuns, orphans);
}
Nav declutter shipped alongside: removed duplicate links to the same run-list view and consolidated "create" and "history" into a single sidebar section.
Batch 7: editor-facing form copy
The create-run form used internal jargon — "swipe iteration," "challenger pool," "feel regen" — labels that made sense to engineers but not to the video editors who were the primary operators.
A docs pass rewrote every form field description in plain language:
| Old label | New label | Old description | New description |
|---|---|---|---|
| Swipe iteration | Revision pass | Increment swipe counter for VIL loop | How many automatic review-and-fix passes to run on each scene |
| Challenger pool | Alternate versions | Max challengers per scene slot | Number of backup versions to generate per scene (for comparison) |
| Feel regen | Energy matching | Re-run feel-regen on sub-threshold clips | Re-render scenes that don't match the reference video's pacing and energy |
The roadmap doc discipline
Every fix was recorded in a roadmap markdown doc before the PR merged. Each entry has:
- Issue description (user-visible symptom)
- Root cause (one sentence)
- PR link
- Status:
planned→shipped - Batch tag for grouping
When batch 4 shipped, five items flipped from planned to shipped in the same commit as the roadmap update. Stakeholders could read one doc and see exactly what launch feedback produced — no archaeology through twenty PR titles.
The post-launch audit isn't a fire drill. It's the first real user test. Treat it like a structured sprint, not a panic.
Numbers from the 48-hour sprint
| Metric | Value |
|---|---|
| Pull requests merged | 20 |
| Distinct user-visible fixes | 12 |
| Batches (themed PR groups) | 7 |
| Reliability fixes | 4 |
| UX / honesty fixes | 5 |
| Recovery / infrastructure fixes | 3 |
What I'd repeat next launch
- Pre-write the roadmap doc template before launch day, with bucket categories ready
- Assign a "honest defaults" reviewer — one person whose only job is asking "what does the user think this UI element means?"
- Batch by theme, not by reporter — three users reporting navigation issues becomes one batch, not three PRs
- Fix data leaks before polish — the reference frame bug eroded trust; breadcrumb styling can wait
- Flip shipped status in the same commit — roadmap drift is how you lose track of what's actually in prod
Launch is not the finish line. It's the start of the audit where real operators tell you what you actually built — and the forty-eight hours after determine whether they trust it enough to come back Monday.