docs(SERVICE): correct event-stream contract and document preview.html

Audited docs against the fork-salvage API surface (PR #17). SERVICE.md fixes:

- Event stream: events are the compiler's granular {t, ...} log stream plus
  service phases, not a fixed capture/generate/verify enum. Corrected the
  "{seq, t, ...payload}" claim — returned bodies carry `t` but NOT `seq`.
  Documented that `after` is a seq cursor (DB filters seq > N) and that the
  in-memory backend ignores `after` and returns the full list each poll.
- preview.html: new Preview section — the flat, self-contained generate-time
  artifact (file-map path `preview.html`, manifest `preview_html`, always
  present), fetched via /v1/clones/:id/files/preview.html. Documented its
  arrival on the `generated` event, relative `public/assets/...` resolution
  through the files route, the authenticated-iframe caveat + proxy
  recommendation, and the original→preview.html→deployed staging. Stated
  plainly that it is static/runtime-free: frozen animations, no interactions.
- Added the `preview` option and clarified app-preview (built export) vs the
  flat preview.html.

DEPLOY.md migration list (0000/0001/0002) verified accurate — no change.
README.md / docs/README.md make no stale API claims — no change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Samraaj Bath
2026-07-06 10:27:53 -07:00
co-authored by Claude Fable 5
parent 01e59d96bd
commit b66a993b35
+67 -10
View File
@@ -31,12 +31,27 @@ Two run modes, same HTTP surface:
**worker** process consumes the queue, runs the clone, stores artifacts, and the **worker** process consumes the queue, runs the clone, stores artifacts, and the
client polls to completion. This is the production mode. client polls to completion. This is the production mode.
In both modes the job emits structured progress events (stage transitions such as In both modes the job emits structured progress events as it runs — the compiler's
`capture`, `generate`, `verify`) as it runs. Poll `GET /v1/clones/:id/events?after=N` granular log stream (`{ event: "captured", ... }`, `{ event: "generated", ... }`,
with the last sequence number you have seen; each event is `{seq, t, ...payload}`. `{ event: "build_start", ... }`, etc.) plus service phases (`{ event: "clone_done" }`,
In production mode events persist in the `job_events` table (migration `{ event: "clone_error", error }`). Every event carries a millisecond wall-clock
`0002_productive_wallflower.sql`), so they survive worker restarts and remain timestamp `t`; the rest of each object is the payload the emitter logged, so field sets
readable after completion. vary by event. There is no fixed enum of event types — treat unknown `event` strings as
opaque progress and drive UI off the coarse job `status` from `GET /v1/clones/:id`.
Poll `GET /v1/clones/:id/events?after=N``{ jobId, events: [{ t, ... }] }`. `after` is a
**sequence cursor** (a 1-based event count, not a timestamp): pass the number of events
you have already consumed to get only newer ones. Caveats:
- The returned event bodies do **not** include the `seq` value, so track the cursor
yourself as a running count of events received.
- The in-memory backend **ignores `after`** and returns the full list on every poll — so
in that mode you always get all events and must de-duplicate by count. The DB backend
honors `after` (`seq > N`).
In production (DB) mode events persist in the `job_events` table (migration
`0002_productive_wallflower.sql`) with a per-job `seq`, so they survive worker restarts
and remain readable after completion.
## Local development ## Local development
@@ -92,10 +107,10 @@ POST /v1/signup/request { email } → 202 {message} (s
POST /v1/signup/verify { token } → 201 {apiKey,message} (consume email token) POST /v1/signup/verify { token } → 201 {apiKey,message} (consume email token)
GET /v1/clones → list (metadata) GET /v1/clones → list (metadata)
GET /v1/clones/:id → status + metadata (fileCount, totalBytes, capture, timings) GET /v1/clones/:id → status + metadata (fileCount, totalBytes, capture, timings)
GET /v1/clones/:id/events?after=N → structured progress events since seq N (poll while running) GET /v1/clones/:id/events?after=N → { jobId, events } — progress events after cursor N (poll while running)
GET /v1/clones/:id/result → the eager CloneResult (text files inline; binaries by URL) GET /v1/clones/:id/result → the eager CloneResult (text files inline; binaries by URL)
GET /v1/clones/:id/files/* → stream one generated file (e.g. .../files/preview.html — see Preview)
GET /v1/clones/:id/app-preview/* → serve the clone's built static export when present (404 until built) GET /v1/clones/:id/app-preview/* → serve the clone's built static export when present (404 until built)
GET /v1/clones/:id/files/* → stream one file
GET /v1/clones/:id/bundle?format=tgz|zip → the whole app as one archive (302 → S3 when configured) GET /v1/clones/:id/bundle?format=tgz|zip → the whole app as one archive (302 → S3 when configured)
DELETE /v1/clones/:id → purge artifacts DELETE /v1/clones/:id → purge artifacts
GET /healthz → { ok: true } (unauthenticated) GET /healthz → { ok: true } (unauthenticated)
@@ -116,8 +131,10 @@ one-time email link, and `POST /v1/signup/verify` consumes the token, stores
only the API key's SHA-256 hash in Postgres, stores the verified email in the only the API key's SHA-256 hash in Postgres, stores the verified email in the
key label for attribution, and returns the raw key once. key label for attribution, and returns the raw key once.
Normal product `options` are `{ mode?: "single" | "multi", styling?: "tailwind" | "css", framework?: "next" | "vite" }`. Normal product `options` are `{ mode?: "single" | "multi", styling?: "tailwind" | "css", framework?: "next" | "vite", preview?: boolean }`.
`mode` defaults to `"single"`, `styling` defaults to `"tailwind"`, and `framework` defaults to `"next"`. Operational options `mode` defaults to `"single"`, `styling` defaults to `"tailwind"`, and `framework` defaults to `"next"`. `preview`
controls the browsable built export served at `/v1/clones/:id/app-preview/` (see **Preview** below); it is on by
default for single-page clones, so pass `preview:true` to force it for a multi-page clone. Operational options
remain `{ verify?, asyncVerify?, maxRoutes?, maxCollection?, captureConcurrency?, validationConcurrency?, viewportConcurrency?, noCache? }`; `noCache` is service-level and remain `{ verify?, asyncVerify?, maxRoutes?, maxCollection?, captureConcurrency?, validationConcurrency?, viewportConcurrency?, noCache? }`; `noCache` is service-level and
bypasses the cache. Deprecated aliases (`multiPage`, `humanizeMode`) and dev-only escape bypasses the cache. Deprecated aliases (`multiPage`, `humanizeMode`) and dev-only escape
hatches are still accepted for compatibility, but are not part of the normal product surface. hatches are still accepted for compatibility, but are not part of the normal product surface.
@@ -132,6 +149,46 @@ attaches the verify report afterward while the worker still has the run artifact
current async QA path; a post-hoc verify endpoint would require persisting full capture artifacts, current async QA path; a post-hoc verify endpoint would require persisting full capture artifacts,
not just the generated app bundle. not just the generated app bundle.
## Preview
Every clone emits a flat, self-contained **`preview.html`** at generate time — it is a
regular entry in the result file map (path `preview.html`, at the app root), and the
manifest records it as `preview_html: "preview.html"` (always present). It exists as soon
as the compiler's `generate` step finishes emitting the file map — well before any Next
build or deploy — so a consumer can show the cloned page within seconds. Its arrival is
visible in the event stream as a `{ event: "generated", ... }` event.
Fetch it through the ordinary files route:
```bash
curl -sS -H "authorization: Bearer $DITTO_API_KEY" \
"$DITTO_API_URL/v1/clones/$JOB_ID/files/preview.html" -o preview.html
```
**What it is — and isn't.** `preview.html` is a single HTML file with inline CSS and **no
JavaScript** (no runtime scripts). It renders the captured page statically: animations are
frozen at their captured start frame, Lottie/`<video>` mounts show their captured
poster/first-frame still, and nothing is interactive — no menus, no hover, no scroll
effects. It is a faithful *picture* of the page, not a working app. For interaction, use the
built export (`/v1/clones/:id/app-preview/`) or the deployed app.
**Relative assets.** `preview.html` references its images and fonts with relative paths
(`public/assets/cloned/...`), so those sub-paths resolve against the same files route the
HTML itself came from (`/v1/clones/:id/files/public/assets/cloned/...`). Serve or proxy the
file under that mount and the assets load with no rewriting.
**Iframe auth caveat.** Because the files route is authenticated (when `API_KEYS`/DB keys
are set), you cannot drop `.../files/preview.html` straight into an `<iframe src=...>` — the
browser can't attach your `Authorization` header to the iframe's asset sub-requests, so
images and fonts 404. Front it with a small server-side proxy that injects the key and
re-serves both the HTML and its `public/assets/...` sub-paths from your own origin, then
point the iframe at the proxy.
**Intended staging.** The preview is the middle rung of a three-step reveal for a consumer
UI: show the **original site** first (instant), swap to **`preview.html`** the moment
`generate` finishes (seconds — a static, frozen likeness), then swap to the **deployed app**
once the build + deploy completes (fully interactive).
**Incremental clone (single → multi, for speed).** Clone one URL single-page first **Incremental clone (single → multi, for speed).** Clone one URL single-page first
(fast app back), then POST the **same URL** with `{ mode: "multi" }` — the second call (fast app back), then POST the **same URL** with `{ mode: "multi" }` — the second call
reuses the first's entry capture (no re-capture of page 1), crawls + captures only the reuses the first's entry capture (no re-capture of page 1), crawls + captures only the