The final-mile plan: take the working app (engine ✅, backend API ✅, all 7 screens ✅) from "functional draft" to market-standard, deployable product. Grounded in a verified audit (2026-06-11) — every issue below was confirmed by execution or grep, not guesswork. Companion docs: FRONTEND_PLAN.md (feature spec), APP_BUILD_STEPS.md (original build order), PLATFORM_PLAN.md (architecture).
| Item | Status |
|---|---|
alert() error popups |
gone (0 usages) |
| Record endpoint event-loop crash | fixed (async def record_test) |
DB session leak (next(get_session())) |
fixed |
Deprecated @app.on_event |
migrated to lifespan |
datetime.utcnow() deprecations |
fixed (0 left) |
.db files in git |
ignored (*.db, -shm, -wal) |
display-font undefined class |
now defined in CSS |
| Crop screenshots static mount | /projects mount added |
| # | Issue | Evidence | Phase |
|---|---|---|---|
| 1 | Invalid Tailwind shades slate-650/750/850 render as nothing → flat, surface-less UI |
77 usages | P1 |
| 2 | React Query installed, unused — manual fetch/useEffect/setInterval in all 8 pages | grep: 0 usages | P2 |
| 3 | Zustand installed, unused — settings re-fetched per page on a 10s interval | grep: 0 usages | P2 |
| 4 | react-router-dom installed, unused — hand-rolled hash routing, string parsing | grep: 0 usages | P2 |
| 5 | TanStack Table installed, unused — hand-built tables, no sort/filter | grep: 0 usages | P3 |
| 6 | No component primitives — Tailwind class-soup duplicated across pages | no components/ dir |
P1 |
| 7 | TS strict off, any everywhere |
tsconfig | P4 |
| 8 | No error boundary — render error = white screen | grep | P2 |
| 9 | No toast system — banners ad-hoc per page | — | P1 |
| 10 | Brand/copy drift — "Record Scraper Flow", "scraper plugin" in a test tool | NewTest.tsx etc. | P3 |
| 11 | Theme tokens defined but unused — hardcoded bg-[#192233] everywhere |
index.css vs pages | P1 |
| 12 | No frontend tests (no Vitest/RTL) | — | P4 |
| 13 | CI has no frontend job — broken build can merge | ci.yml is Python-only | P5 |
| 14 | No env-based API config — API_BASE='' breaks Vite dev for downloads/WS |
api.ts | P2 |
| 15 | Icon-only buttons lack aria-labels; portal never run through its own a11y check | TestDetail.tsx | P3 |
| 16 | Light/dark half-scaffolding — invalid .dark :root selector, no toggle |
index.css | P1 |
| 17 | No deployment packaging — frontend dist not bundled; no scrapewizard start |
— | P5 |
| 18 | Stray test_studio.db in repo root (untracked but present) |
ls | P5 |
Goal: the app looks designed. One source of truth for color/spacing/type; reusable primitives so every later fix is one edit, not 77.
- In
studio/frontend/src/index.css@theme, define the missing shades and semantic tokens:Defining@theme { --color-slate-650: #3e4c66; /* legitimize existing usage instantly */ --color-slate-750: #2c3a52; --color-slate-850: #16202f; --color-surface: #192233; /* card/panel background */ --color-surface-2:#1e2a40; /* raised surface */ --color-edge: #324467; /* borders */ --color-accent: #135bec; --color-pass: #10b981; --color-fail: #f43f5e; --color-warn: #f59e0b; --color-run: #3b82f6; }
slate-650/750/850makes all 77 broken classes render immediately with zero page edits — then migrate to semantic names (bg-surface,border-edge) opportunistically as pages are touched in Phase 2. - Remove the dead
.dark :rootblock; commit to dark-only for v1 (decision, not accident). - ✔️ Every card/panel has a visible surface + border; no
bg-[#hex]hardcodes left in pages.
Create studio/frontend/src/components/ui/:
| Component | Notes |
|---|---|
Button |
variants: primary / secondary / ghost / danger; sizes; loading prop; auto aria-busy |
Card |
surface + border + padding; header/footer slots |
Input, Select, Label |
consistent focus rings, error state |
Badge / StatusPill |
semantic statuses: passed/failed/running/queued/healed |
EmptyState |
icon + message + CTA (used by every list) |
LoadingSkeleton |
shimmer blocks for lists/cards |
ErrorState |
human message + retry + "copy details" |
Toast + useToast() |
success/error/info; replaces all ad-hoc banners (see 2.2 store) |
PageHeader |
title + breadcrumb + actions slot (fills the empty top bar) |
ConfirmDialog |
for deletes (currently un-confirmed) |
- ✔️ One Storybook-style demo route (
/dev/ui, dev-only) rendering all primitives — your visual regression target later.
Goal: actually use the stack you ship. Server state via React Query, client state via Zustand, real routing, resilient errors.
- Wrap app in
QueryClientProvider(sane defaults:staleTime: 15s,retry: 1). - Create
src/hooks/— one typed hook per resource, delete all manual fetch/useEffect:useSettings()/useUpdateSettings()/useTestConnection()useTests()/useTest(id)/useCreateTest()/useUpdateTest(id)/useDeleteTest(id)useRecordStatus(id)—refetchInterval: 1000only while recording (replaces hand-rolledsetInterval)useTriggerRun(id)/useRun(runId)/useRuns(filters)/useStats()
- Mutations invalidate their queries (
onSuccess: invalidate(['tests'])) → no manual reloads. - Keep the WebSocket in RunDetail (it's correct); use it to patch the
useRuncache viaqueryClient.setQueryDataso live + fetched state stay consistent. - ✔️ grep shows zero raw
fetch(outsidelib/api.ts; zerosetIntervaloutside the WS fallback.
src/store/app.ts:{ settings, setSettings, toasts, pushToast, dismissToast }.- Settings loaded once via
useSettings(), mirrored to the store for the AI pill — delete the 10-second polling inApp.tsx. - ✔️ AI pill updates instantly after saving Settings (cache invalidation), no polling.
- Replace hand-rolled hash state with
createHashRouter(react-router v7) — keeps compatibility with the FastAPI static mount (no server rewrites needed), but givesuseParams,useNavigate, layout routes, and a 404 route. - Layout route renders Sidebar +
PageHeader+<Outlet/>. - ✔️ Deep links (
#/tests/3,#/runs/7) load correctly on hard refresh from the prod mount.
ErrorBoundarycomponent at the layout level → rendersErrorState, not a white screen.src/lib/config.ts:export const API_BASE = import.meta.env.VITE_API_BASE ?? ''—.env.developmentsetsVITE_API_BASE=http://127.0.0.1:8000; prod stays''. Use it for fetch, file downloads (export), artifact image URLs, and the WS URL (ws://derived from API_BASE orlocationin prod).- Vite dev proxy alternative documented in
vite.config.js(either works; pick one). - ✔️
npm run devagainst a running backend: every feature works, including pytest export download and screenshots.
- Settings (simplest — proves the pattern)
- Tests list (+
EmptyState,ConfirmDialogfor delete) - NewTest / recording (uses
useRecordStatuspolling hook) - TestDetail / Step Manager (largest; keep local edit state, save via mutation)
- RunDetail (React Query + WS cache patching)
- Dashboard + RunHistory (swap hand tables for TanStack Table — sort/filter free)
- ✔️ After each page: it works, it's shorter, and it uses zero ad-hoc fetch/state.
- Sweep all UI strings: "Record Scraper Flow" → "Record Test Flow"; "scraper plugin" → "test"; placeholder "Scrape products list" → "Login flow smoke test"; sidebar "Test Suite" ✓ (already right). Keep "ScrapeWizard" as brand until the §18 name decision.
- ✔️ grep for "scrap" in
studio/frontend/srcreturns only the brand name.
- Step crop thumbnails in Step Manager via the new
/projectsmount (already exists backend-side) — render<img>with lightbox; placeholder if missing. - Visual diff viewer in RunDetail: baseline / current / diff tabs when
visual_diff_score > 0. - ✔️ A recorded step shows its element crop; a visually-changed run shows the diff.
aria-labelon all icon-only buttons (move/delete/etc.); focus-visible rings (primitives give this for free).- Keyboard:
r= run (on TestDetail),/= focus search (Tests),Esc= close dialogs. - Self-a11y check: Playwright script loads the built portal and runs our own
perform_a11y_checkagainst each route → wire into CI (Phase 5). We ship an a11y checker; we must pass it. - ✔️ Portal passes its own axe scan with 0 serious/critical violations.
- Walk every page against the checklist: empty / loading / error / partial (FRONTEND_PLAN §2).
Primitives make this mostly drop-in. Partial = mid-run refresh of RunDetail recovers from
GET /runs/{id}+ WS. - ✔️ Kill the dev server mid-action; every page degrades to a designed state, never a blank div.
"strict": truein tsconfig; fix fallout (mostlyerr: any→unknown+ narrow,interval: any→ReturnType<typeof setInterval>).- Shared API types: one
src/lib/types.tsmatching backend models (Test,Step,Run,StepResult,Settings) — the API client and hooks are fully typed end-to-end. - ✔️
tsc --noEmitclean.
- Vitest + React Testing Library + MSW (mock the API):
- primitives render-tests (Button states, EmptyState, Toast)
- Settings: load → edit → save (mutation called, toast shown)
- Tests list: empty state CTA, delete confirm flow
- TestDetail: edit selector → save payload shape correct
- RunDetail: WS message updates a step to failed
- ✔️
npm testgreen; meaningful coverage on the 3 critical pages (not a % target — the flows above).
- Extend
tests/integration/test_studio_backend.py: export endpoint returns a runnable file;/statsshape; run executor error path writesstatus="error". - ✔️ pytest suite green (currently 4 studio tests → ~8).
Add to .github/workflows/ci.yml:
frontend:
runs-on: ubuntu-latest
defaults: { run: { working-directory: studio/frontend } }
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: npm, cache-dependency-path: studio/frontend/package-lock.json }
- run: npm ci
- run: npm run lint
- run: npx tsc --noEmit
- run: npm test -- --run
- run: npm run buildPlus the self-a11y job (3.3) after build. ✔️ A broken frontend can no longer merge.
- Build pipeline: CI builds
studio/frontend/dist→ included in the wheel as package data ([tool.setuptools.package-data]+ move/copy dist underscrapewizard/studio_static/or addstudio*to packages). Users need no Node. scrapewizard startcommand: init DB → uvicorn on127.0.0.1:<free port>→ serve the bundled dist →webbrowser.open(). (Backend already mounts dist when present.)scrapewizard doctorextended: check Playwright browsers; offerplaywright install chromium.- Versioning: single source (
pyproject.toml) → exposed at/healthand in the sidebar (currently hardcoded "1.2.0" in three places). - ✔️ On a clean machine:
pipx install <wheel>→scrapewizard start→ browser opens → record → run → report. No Node, no manual uvicorn.
- Extend the existing tag-triggered
release.yml: build frontend → build wheel → attach to GitHub Release → (when named/ready) publish to PyPI. CHANGELOG.mdstarted; Conventional Commits already in use.- Delete stray
test_studio.dbfrom the working tree (issue #18); point tests at tmp DBs. - ✔️ Tagging
v1.3.0produces an installable artifact with the UI bundled.
- README "Run the Studio" section:
pipx install … && scrapewizard start+ screenshot/GIF. SECURITY.mdnote: server binds localhost-only by design; not a hosted service.- ✔️ A stranger can install and reach the dashboard in < 5 minutes from README alone.
- Fresh-machine install → record → edit → run → report on Win/macOS/Linux (CI matrix + one manual)
- All Phase 1–5 acceptance checks green
- Portal passes self-a11y; zero invalid Tailwind classes (
grep slate-[678]50= only @theme defs) -
tsc --noEmit, lint, vitest, pytest, build — all green in CI - Copy sweep done (no "scraper" strings in the test UI)
- Demo GIF recorded (record → break page → run → failure evidence)
- Version bumped once in pyproject; visible in UI +
/health
| Phase | What | Effort | Visible result |
|---|---|---|---|
| P1 | Color system + primitives | 1–2 d | App suddenly looks designed |
| P2 | React Query + Zustand + Router + page migrations | 2–3 d | Modern, resilient, fast |
| P3 | Copy rebrand + evidence UI + a11y + states | 1–2 d | Feels like a product |
| P4 | TS strict + tests | 1–2 d | Trustworthy to change |
| P5 | CI + packaging + start |
1–2 d | Deployable |
| P6 | Launch gate | ½ d | Ship |
Total: ~7–11 focused days. P1 → P2 are sequential (primitives before migrations); P3/P4 can interleave; P5 last (packages whatever exists).
Single highest-leverage first step: Phase 1.1 — the three
@themelines that fix all 77 invalid color classes at once. Fifteen minutes, transforms the entire app's appearance.