|
| 1 | +# 🧱 ScrapeWizard — Application Build Steps (Backend Foundation → Frontend) |
| 2 | + |
| 3 | +> The execution sequence for building the GUI app. Feature detail lives in |
| 4 | +> [FRONTEND_PLAN.md](FRONTEND_PLAN.md); the *order of operations* lives here. |
| 5 | +> |
| 6 | +> **Golden rule:** the backend API is built **first** because every screen needs it. Then |
| 7 | +> screens are added as **vertical slices** (one screen + the endpoints it uses), each shippable. |
| 8 | +> |
| 9 | +> **Locked:** local web app (`scrapewizard start`) · launch-alongside recorder · Stage 2 engine. |
| 10 | +
|
| 11 | +--- |
| 12 | + |
| 13 | +# PART A — Backend API Skeleton + SQLite (the foundation) |
| 14 | + |
| 15 | +The backend is a **thin HTTP layer over the working Stage 2 engine**. It does three jobs: |
| 16 | +expose the engine over REST/WS, persist tests & runs in SQLite, serve the built frontend. |
| 17 | + |
| 18 | +**Engine functions it wraps (all exist, verified working):** |
| 19 | +- `InteractiveRecorder(output_path, screenshots_dir, headless=False).start(url)` → writes `flow.json` |
| 20 | +- `TestGenerator(flow_path).generate()` → `{url, steps:[...]}` · `.export_pytest(path)` → file |
| 21 | +- `SandboxRunner(artifacts_dir, baselines_dir, headless).run(test_def)` → `RunResult` |
| 22 | +- `LLMClient(provider, api_key, model)` → for Settings test-connection |
| 23 | +- keyring via `scrapewizard/utils/security.py` → for storing the API key |
| 24 | + |
| 25 | +### Step A1 — Dependencies & structure |
| 26 | +- **Do:** add `sqlmodel` (and confirm `fastapi`, `uvicorn`, `websockets`) to `requirements.txt` |
| 27 | + + `pyproject.toml`. Create the backend module layout under `studio/backend/`: |
| 28 | + ``` |
| 29 | + studio/backend/ |
| 30 | + main.py # FastAPI app, CORS, static mount, router registration (exists — extend) |
| 31 | + db.py # SQLite engine + session, create_all, schema_version |
| 32 | + models.py # SQLModel tables |
| 33 | + routes_settings.py |
| 34 | + routes_tests.py |
| 35 | + routes_runs.py |
| 36 | + run_executor.py # background run worker + WS hub |
| 37 | + deps.py # shared dependencies (DB session, paths) |
| 38 | + ``` |
| 39 | +- **Done-when:** `uvicorn studio.backend.main:app` boots; `GET /health` returns ok. |
| 40 | + |
| 41 | +### Step A2 — Database setup (`db.py`) |
| 42 | +- **Do:** |
| 43 | + 1. SQLite at `~/.scrapewizard/studio.db` (create dir if missing). |
| 44 | + 2. SQLModel `engine` + a `get_session()` dependency. |
| 45 | + 3. `init_db()` → `SQLModel.metadata.create_all()`; store a `schema_version` row. |
| 46 | + 4. Artifacts root at `~/.scrapewizard/artifacts/`, baselines at `~/.scrapewizard/baselines/` |
| 47 | + (matches the sandbox default — keep them consistent). |
| 48 | +- **Done-when:** first boot creates the `.db` file; `GET /health` reports `schema_version`. |
| 49 | + |
| 50 | +### Step A3 — Models (`models.py`) *(minimal set for v1 — expand later per PLATFORM_PLAN §12)* |
| 51 | +``` |
| 52 | +Setting key (PK), value # provider, model, ai_mode, thresholds (NOT the key) |
| 53 | +Test id, name, url, created_at, updated_at |
| 54 | +Step id, test_id(FK), order, action, value, selectors(JSON), assertions(JSON), fingerprint(JSON) |
| 55 | +Run id, test_id(FK), status, started_at, finished_at, duration_ms, ai_calls, ai_cost_usd |
| 56 | +StepResult id, run_id(FK), step_name, status, duration_ms, screenshot_path, |
| 57 | + visual_diff_score, console_errors(JSON), network_errors(JSON), |
| 58 | + a11y_violations(JSON), healed, error_message |
| 59 | +``` |
| 60 | +- **Note:** `Step.selectors/assertions/fingerprint` mirror `TestGenerator.generate()` output, and |
| 61 | + `StepResult` mirrors the sandbox dataclass → storage = direct dump, no translation. |
| 62 | +- The **API key is NOT a DB column** — it lives in keyring (`Setting` only records `has_key`). |
| 63 | +- **Done-when:** tables create cleanly; a manual insert/select round-trips. |
| 64 | + |
| 65 | +### Step A4 — Settings router (`routes_settings.py`) → unblocks the Settings screen |
| 66 | +- **Endpoints:** |
| 67 | + - `GET /settings` → `{provider, model, ai_mode, has_key, visual_threshold, retention}` |
| 68 | + (read from `Setting` rows; `has_key` = does keyring hold one). |
| 69 | + - `PUT /settings` → upsert `Setting` rows; if `api_key` present, store it in keyring |
| 70 | + (`security.py`) and **never store/echo it in the DB or response**. |
| 71 | + - `POST /settings/test-connection` → build `LLMClient(provider, key, model)`, do a tiny |
| 72 | + probe call; return `{ok, message}`. |
| 73 | +- **Done-when:** `curl PUT /settings` saves; `GET` shows `has_key:true`; bad key → test fails clearly. |
| 74 | + |
| 75 | +### Step A5 — Tests router (`routes_tests.py`) → unblocks New Test + Step Manager |
| 76 | +- **Endpoints:** |
| 77 | + - `POST /tests {url, name?}` → create `Test`, return `{id}`. |
| 78 | + - `GET /tests` → list with `step_count` + `last_run`. |
| 79 | + - `GET /tests/{id}` → test + ordered `Step`s. |
| 80 | + - `PUT /tests/{id}` → update name / steps (edits from Step Manager). |
| 81 | + - `DELETE /tests/{id}`. |
| 82 | + - `POST /tests/{id}/record` → **launch the alongside recorder as a background task** |
| 83 | + (see A6 note); returns `{status:"started"}`. |
| 84 | + - `GET /tests/{id}/record/status` → `{recording: bool, step_count}`. |
| 85 | + - `POST /tests/{id}/generate {mode:"local"|"ai"}` → run `TestGenerator` (local) or AI path |
| 86 | + (Stage 5); persist resulting `Step`s. |
| 87 | + - `POST /tests/{id}/export` → `TestGenerator.export_pytest(...)` → return file download. |
| 88 | + - `POST /tests/{id}/run` → enqueue a run (A7), return `{run_id}`. |
| 89 | +- **Done-when:** create a test, record against it, `GET /tests/{id}` shows captured steps. |
| 90 | + |
| 91 | +### Step A6 — Recording as a background task (the one tricky bit) |
| 92 | +- **Why:** `InteractiveRecorder.start()` opens a **headed browser and blocks until the user |
| 93 | + closes it** — you cannot await it inside a request handler. |
| 94 | +- **Do:** |
| 95 | + 1. `POST /tests/{id}/record` spawns an `asyncio` task running the recorder, writing |
| 96 | + `flow.json` into the test's working dir; set an in-memory `recording[test_id]=True`. |
| 97 | + 2. Frontend polls `record/status` (or subscribe via WS `/tests/{id}/record/live`). |
| 98 | + 3. When the browser closes → recorder finishes → backend reads `flow.json`, runs |
| 99 | + `TestGenerator.generate()`, and **persists the steps** to the `Step` table; flips status off. |
| 100 | +- **Done-when:** click record (via curl trigger), perform actions, close browser → steps land in DB. |
| 101 | + |
| 102 | +### Step A7 — Runs router + executor (`routes_runs.py`, `run_executor.py`) → unblocks Run view |
| 103 | +- **Do:** |
| 104 | + 1. `POST /tests/{id}/run` → create `Run(status="queued")`, spawn a background task. |
| 105 | + 2. Executor: build `test_def` from the test's `Step`s → `SandboxRunner(...).run(test_def)` |
| 106 | + → write `StepResult` rows → update `Run` to passed/failed + duration; emit progress over WS. |
| 107 | + 3. `GET /runs/{run_id}` → `Run` + its `StepResult`s (= the sandbox `RunResult` shape). |
| 108 | + 4. `GET /runs?test_id=&status=` → history. |
| 109 | + 5. `WS /runs/{run_id}/live` → push each `StepResult` as it completes. |
| 110 | +- **Simpler v1 fallback:** if WS is too much at first, make `POST /run` block and return the |
| 111 | + full `RunResult`; add live WS later. (Frontend can poll `GET /runs/{id}` meanwhile.) |
| 112 | +- **Done-when:** trigger a run → `GET /runs/{id}` returns per-step results + screenshots paths. |
| 113 | + |
| 114 | +### Step A8 — Artifacts static mount |
| 115 | +- **Do:** mount `~/.scrapewizard/artifacts/` at `GET /artifacts/{run_id}/{file}` so the frontend |
| 116 | + can load screenshots and diff images by URL. |
| 117 | +- **Done-when:** a screenshot path from a run loads in the browser. |
| 118 | + |
| 119 | +### Step A9 — `scrapewizard start` command |
| 120 | +- **Do:** new CLI command that (1) `init_db()`, (2) runs uvicorn on `127.0.0.1`, (3) serves the |
| 121 | + **built** frontend (`studio/frontend/dist`) as static files at `/`, (4) opens the browser. |
| 122 | + Bind localhost only (security, already set). Keep CORS for the Vite dev origin during dev. |
| 123 | +- **Done-when:** `scrapewizard start` opens the app shell in the browser. |
| 124 | + |
| 125 | +### Step A10 — Verify the backend before touching the frontend |
| 126 | +- **Do:** a smoke script (or REST client) that walks: `PUT /settings` → `POST /tests` → |
| 127 | + (`record` manually) → `GET /tests/{id}` → `POST /run` → `GET /runs/{id}`. |
| 128 | +- **Done-when:** the whole loop works over HTTP with **no frontend** — the API is real and trusted. |
| 129 | + |
| 130 | +> ✅ **End of Part A:** the engine is fully drivable over HTTP + persisted in SQLite. Now the |
| 131 | +> frontend is "just" a client of a working API — exactly the position you want to build a UI from. |
| 132 | +
|
| 133 | +--- |
| 134 | + |
| 135 | +# PART B — Frontend, step by step |
| 136 | + |
| 137 | +Build as vertical slices; each slice ends with something you can click. Detail per screen is in |
| 138 | +[FRONTEND_PLAN.md](FRONTEND_PLAN.md) §5 — this is the sequence + setup. |
| 139 | + |
| 140 | +### Slice 0 — Scaffold & plumbing |
| 141 | +- **Do (in `studio/frontend/`):** |
| 142 | + 1. Confirm Vite + React; add TypeScript if not present. |
| 143 | + 2. Install: `react-router-dom`, `@tanstack/react-query`, `tailwindcss`, `shadcn/ui` (init), |
| 144 | + `react-hook-form`, `zod`, `lucide-react`. |
| 145 | + 3. Set up Tailwind + design tokens + **dark mode** (semantic colors: passed/failed/healed/ |
| 146 | + running/queued). |
| 147 | + 4. Create `src/lib/api.ts` — a typed fetch client pointing at `http://127.0.0.1:8000` |
| 148 | + (env var for prod vs dev). One place for all API calls. |
| 149 | + 5. Create `QueryClientProvider`, `RouterProvider`. |
| 150 | +- **Done-when:** `npm run dev` shows a themed blank app; dark mode toggles. |
| 151 | + |
| 152 | +### Slice 1 — App shell + the four state components |
| 153 | +- **Do:** |
| 154 | + 1. Layout: sidebar (Dashboard / Tests / Runs / Settings) + top bar (logo, **AI status pill**). |
| 155 | + 2. Define routes (FRONTEND_PLAN §4) with placeholder pages. |
| 156 | + 3. Build the reusable primitives **now** (everything depends on them): `EmptyState`, |
| 157 | + `LoadingSkeleton`, `ErrorState` (with "copy details"), `StatusPill`. |
| 158 | +- **Done-when:** navigation works; every placeholder renders an EmptyState. |
| 159 | + |
| 160 | +### Slice 2 — Settings screen *(simplest real screen; uses A4)* |
| 161 | +- **Do:** provider select (OpenAI/Anthropic/OpenRouter/Local), API key field, model field, |
| 162 | + AI mode radio (Off/Creation/Full), threshold slider. "Test connection" button → |
| 163 | + `POST /settings/test-connection`. Save → `PUT /settings`. Show `has_key` as "•••• set". |
| 164 | +- **Done-when:** save a key, test connection passes, AI pill updates. |
| 165 | + |
| 166 | +### Slice 3 — The spine: New Test → Record → Step Manager *(uses A5 + A6)* |
| 167 | +- **Do:** |
| 168 | + 1. **New Test** (`/tests/new`): URL form (Zod), name optional → `POST /tests` → then |
| 169 | + `POST /tests/{id}/record`. |
| 170 | + 2. **Record state:** waiting panel polling `record/status` (or WS), showing live step count + |
| 171 | + ⚠ warnings; on finish → navigate to `/tests/{id}`. |
| 172 | + 3. **Step Manager** (`/tests/{id}`): render ordered `StepCard`s (action, **editable selector |
| 173 | + ladder**, value, assertions, screenshot thumb). Edit/reorder/delete → `PUT /tests/{id}`. |
| 174 | + Buttons: **Run ▶** (`POST /run`), **Generate** (local/AI), **Export pytest**. |
| 175 | +- **Done-when:** enter URL → record 3 actions → see them as editable steps → edit one → save. |
| 176 | + |
| 177 | +### Slice 4 — Run view (live + result) *(uses A7 + A8)* |
| 178 | +- **Do:** on Run ▶ → navigate to `/runs/{run_id}`; subscribe to WS (or poll `GET /runs/{id}`); |
| 179 | + render `RunTimeline` (steps green/red) + an evidence panel per step: screenshot (from |
| 180 | + `/artifacts/...`), `VisualDiffViewer`, `A11yViolationList`, `ConsoleNetworkList`, error msg. |
| 181 | + Status banner deep-links to the first failing step. |
| 182 | +- **Done-when:** run a test, watch steps update, open evidence; failed run highlights the failure. |
| 183 | + |
| 184 | +### Slice 5 — Export button polish |
| 185 | +- **Do:** wire **Export pytest** in Step Manager to `POST /tests/{id}/export` → download the |
| 186 | + standalone file. (Engine already produces it.) |
| 187 | +- **Done-when:** downloaded file runs as a standalone `pytest` outside the app. |
| 188 | + |
| 189 | +### Slice 6 — Dashboard + Run history *(uses /stats, /runs)* |
| 190 | +- **Do:** Dashboard stat cards + recent runs + empty state ("Record your first test" / "Try the |
| 191 | + demo"). Run history table with filters → click → Run view. |
| 192 | +- **Done-when:** dashboard shows real numbers; history filters to a test's failed runs. |
| 193 | + |
| 194 | +### Slice 7 — Polish & a11y |
| 195 | +- **Do:** ensure **all four states** on every screen; keyboard shortcuts (`r`, `/`, `Esc`); |
| 196 | + run the portal through axe-core in CI (we ship an a11y checker — it must pass its own). |
| 197 | +- **Done-when:** clean empty/loading/error states everywhere; portal passes its own a11y scan. |
| 198 | + |
| 199 | +--- |
| 200 | + |
| 201 | +## First demo-able product = end of Slice 4 |
| 202 | +Enter a URL → record the task → see/edit the generated steps → run → watch it pass/fail live → |
| 203 | +read the report. That is the exact application you described, working end to end, no API key |
| 204 | +required (AI optional via Settings). |
| 205 | + |
| 206 | +## Suggested commit checkpoints |
| 207 | +- After **A10** (backend drivable over HTTP) — a clean foundation commit. |
| 208 | +- After each frontend slice (0→7) — each is independently shippable. |
| 209 | + |
| 210 | +## Dependency note |
| 211 | +Part B Slice N assumes the Part A endpoints it lists are done. If building solo, do |
| 212 | +**A1–A5 → Slice 0–2 → A6/A7 → Slice 3–4 → rest** so backend and frontend interleave naturally. |
0 commit comments