feat(web): stream narrative via SSE, wire Instagram carousel button, polish privacy#45
Merged
Merged
Conversation
…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>
2b96d0a to
915c42b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
POST /generatenow does just the deterministic prepphase (parse GPX, load photos, persist
pending.json) and returns agenerating page that opens an
EventSourceto the newGET /generate/{slug}/stream. The SSE endpoint runs the LLM viaAnthropicClient.complete_streamand emitschunk/status/done/errorevents. One parse-failure retry flips the page to a"regenerating" state; double failure lands an
error. Pending state isunlinked on success so the SSE endpoint cannot be re-triggered (and
cannot pay the bill twice).
/memory/{slug}/carousel, fetches each slide as aBlob, and eithercalls
navigator.share({files})(iOS Safari path → "Save N Images" sharesheet → two-tap to Instagram) or renders desktop fallback download links.
The slide route now sets
Content-Disposition: attachmentwith aslug-namespaced filename so desktop saves land cleanly in
~/Downloads.lifecycle that links to specific line ranges in
web/storage.py(
cleanup_inputs,sweep_expired) andweb/app.py(_run_sweeper) onGitHub. The form's privacy link reads "How we handle your photos →" and
opens in a new tab.
The unused synchronous
web.pipeline.run_pipelinewas removed — thestreaming flow has fully replaced it.
Test plan
make ci— 286 tests pass, ruff clean, mypy clean, 95% totalcoverage.
tests/test_web.pycases: generating-page rendering, fullSSE handshake (
chunk→status→done), retry path, double-failure error path, 404-after-consume, content-type header,
end-to-end retention sweep.
tests/test_instagram_button.py: button renders in all threestyles (parametrized),
navigator.canShare+ fallback both wired,carousel returns N+2 slides, slide downloads carry attachment
Content-Dispositionwith slug, idempotent POSTs.tests/test_client.pyandtests/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.
TestClient:landing → POST → SSE drain → memory page → carousel POST → slide
download. Passes.
mobile emulation at 375px) the full upload → streaming page →
memory → "Save for Instagram" → share sheet flow.
🤖 Generated with Claude Code