Skip to content

feat(web): stream narrative via SSE, wire Instagram carousel button, polish privacy#45

Merged
ditvor merged 1 commit into
developfrom
feat/streaming-and-carousel
Apr 30, 2026
Merged

feat(web): stream narrative via SSE, wire Instagram carousel button, polish privacy#45
ditvor merged 1 commit into
developfrom
feat/streaming-and-carousel

Conversation

@ditvor
Copy link
Copy Markdown
Owner

@ditvor ditvor commented Apr 30, 2026

Summary

Phase 5 polish on top of the FastAPI web builder. Three features in one PR
because they share the rendered-memory route surface and would otherwise
need each other's plumbing back-ported.

  • SSE streaming. POST /generate now does just the deterministic prep
    phase (parse GPX, load photos, persist pending.json) and returns a
    generating page that opens an EventSource to the new
    GET /generate/{slug}/stream. The SSE endpoint runs the LLM via
    AnthropicClient.complete_stream and emits chunk / status / done /
    error events. One parse-failure retry flips the page to a
    "regenerating" state; double failure lands an error. Pending state is
    unlinked on success so the SSE endpoint cannot be re-triggered (and
    cannot pay the bill twice).
  • Instagram carousel button on every style template. Posts to
    /memory/{slug}/carousel, fetches each slide as a Blob, and either
    calls navigator.share({files}) (iOS Safari path → "Save N Images" share
    sheet → two-tap to Instagram) or renders desktop fallback download links.
    The slide route now sets Content-Disposition: attachment with a
    slug-namespaced filename so desktop saves land cleanly in ~/Downloads.
  • Privacy polish. The privacy page is now an explicit six-step
    lifecycle that links to specific line ranges in web/storage.py
    (cleanup_inputs, sweep_expired) and web/app.py (_run_sweeper) on
    GitHub. The form's privacy link reads "How we handle your photos →" and
    opens in a new tab.

The unused synchronous web.pipeline.run_pipeline was removed — the
streaming flow has fully replaced it.

Test plan

  • make ci — 286 tests pass, ruff clean, mypy clean, 95% total
    coverage
    .
  • New tests/test_web.py cases: generating-page rendering, full
    SSE handshake (chunkstatusdone), retry path, double-
    failure error path, 404-after-consume, content-type header,
    end-to-end retention sweep.
  • New tests/test_instagram_button.py: button renders in all three
    styles (parametrized), navigator.canShare + fallback both wired,
    carousel returns N+2 slides, slide downloads carry attachment
    Content-Disposition with slug, idempotent POSTs.
  • New streaming tests in tests/test_client.py and
    tests/test_narrative.py: rate-limit retry before first chunk,
    mid-stream error surfaces without retry, status-error translation,
    JSON-parse retry, schema-validation failure surfaced cleanly.
  • End-to-end smoke test against the running app via TestClient:
    landing → POST → SSE drain → memory page → carousel POST → slide
    download. Passes.
  • Manual mobile QA pending. Test on a real phone (or DevTools
    mobile emulation at 375px) the full upload → streaming page →
    memory → "Save for Instagram" → share sheet flow.

🤖 Generated with Claude Code

…polish privacy

POST /generate now does prep only (parse GPX, load photos, persist
pending.json) and returns a generating page that opens an EventSource
to a new GET /generate/{slug}/stream endpoint. The SSE endpoint runs
the LLM via AnthropicClient.complete_stream and emits chunk / status /
done / error events; one parse-failure retry flips the page UI to a
"regenerating" state. Pending state is unlinked on success so the
endpoint cannot be re-triggered.

A "Save for Instagram" button is wired into all three style templates.
It POSTs to /memory/{slug}/carousel, then either calls
navigator.share({files}) for the iOS Safari two-tap path or renders
desktop fallback download links. The carousel slide route now sets
Content-Disposition: attachment with a slug-namespaced filename so
desktop saves land cleanly in Downloads.

The privacy page is now an explicit six-step lifecycle and links to
specific line ranges in web/storage.py (cleanup_inputs, sweep_expired)
and web/app.py (_run_sweeper) on GitHub. The form's privacy link
opens in a new tab and reads "How we handle your photos →".

Tests cover the full SSE handshake (chunk/done/error/retry/404 paths),
the carousel button across all three styles, slide download headers,
and the streaming variants of both AnthropicClient and the narrative
orchestrator. 286 tests pass; coverage 95%.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@ditvor ditvor force-pushed the feat/streaming-and-carousel branch from 2b96d0a to 915c42b Compare April 30, 2026 09:33
@ditvor ditvor changed the title feat(web): SSE streaming, Instagram carousel, privacy polish feat(web): stream narrative via SSE, wire Instagram carousel button, polish privacy Apr 30, 2026
@ditvor ditvor merged commit 3a2bf39 into develop Apr 30, 2026
6 checks passed
@ditvor ditvor deleted the feat/streaming-and-carousel branch April 30, 2026 09:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant