docs: realign blueprint with shipped state + add feature/journey/ideas docs
- PROJECT.md, README.md, TEST_GUIDE.md: status line refreshed; rate-limiter doc-vs-code drift fixed; HTML export section rewritten for the SvelteKit- static viewer; SSE event names + new events documented; config seed block extended with planned toggles + privacy_note; decision log entries added. - docs/CONCEPT_HTML_VIEWER.md, docs/CONCEPT_MOBILE_UI.md: banner the design intent as shipped; point at the source-of-truth code paths. - docs/CONCEPT_DIASHOW.md: planned-then-shipped design for the live diashow (two-queue policy, pluggable transitions, data-mode aware). - docs/FEATURES.md: capability matrix by role (Guest / Host / Admin) plus prose per area (auth, posting, feed, moderation, admin, export, gestures, data mode, quotas, privacy note, extensibility). - docs/USER_JOURNEYS.md: step-by-step flows for every supported scenario, including PIN reset by host, data mode, privacy note, gestures, and the admin toggles. - docs/IDEAS.md: speculative extensions (global diashow, reactions, multi-tenancy, animation pack, etc.) — explicitly out of v0.16 scope. - backend/migrations/README.md, frontend/src/lib/README.md: codify the "never edit a shipped migration" rule and the lib/ conventions (one store per concern, gestures via actions, sheets via ContextSheet, transitions as drop-in components). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
196
docs/CONCEPT_DIASHOW.md
Normal file
196
docs/CONCEPT_DIASHOW.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# Live Diashow Concept
|
||||
|
||||
> **Status: SHIPPED.** Implementation lives at
|
||||
> [frontend/src/lib/diashow/](../frontend/src/lib/diashow/) and
|
||||
> [frontend/src/routes/diashow/+page.svelte](../frontend/src/routes/diashow/+page.svelte).
|
||||
> Treat this doc as the design reference; code is the source of truth.
|
||||
|
||||
## Goal
|
||||
|
||||
A fullscreen, auto-advancing slideshow that any user can start from their device. Suitable
|
||||
for a venue projector or TV running off a single phone/laptop. Two behaviours combine in one
|
||||
view:
|
||||
|
||||
1. **Live drain** — when a new post is uploaded mid-event, it appears on the next slide
|
||||
transition.
|
||||
2. **Shuffle fallback** — between new uploads (and they will be rare in quiet stretches), the
|
||||
diashow rotates through everything posted so far, in shuffled order.
|
||||
|
||||
The user does **not** need to be Host or Admin. Any guest can start the diashow on their own
|
||||
device. There is no global "the room's diashow" — each device runs its own (though they will
|
||||
look very similar if started at the same time).
|
||||
|
||||
---
|
||||
|
||||
## Behavioural model
|
||||
|
||||
Two FIFO queues live in the client:
|
||||
|
||||
| Queue | Source | Drain priority |
|
||||
|----------------|-------------------------------------------|----------------|
|
||||
| `liveQueue` | SSE events for new processed uploads | First |
|
||||
| `shuffleQueue` | Snapshot of all known uploads, shuffled | When live empty |
|
||||
|
||||
### Slide-advance algorithm
|
||||
|
||||
```ts
|
||||
function nextSlide(): Slide | null {
|
||||
// 1. Drain live posts first (FIFO).
|
||||
if (liveQueue.length) return liveQueue.shift()!;
|
||||
|
||||
// 2. Refill shuffle queue from `allKnown` if drained.
|
||||
if (!shuffleQueue.length) {
|
||||
shuffleQueue = shuffle(
|
||||
[...allKnown.values()].filter(s => !recentlyShown.has(s.id))
|
||||
);
|
||||
}
|
||||
return shuffleQueue.shift() ?? null;
|
||||
}
|
||||
```
|
||||
|
||||
A small ring buffer `recentlyShown` (last ~5 IDs) prevents the same picture coming back
|
||||
within seconds when the shuffle queue is rebuilt.
|
||||
|
||||
### Live insertion
|
||||
|
||||
```ts
|
||||
sseClient.on('upload-processed', (msg) => {
|
||||
if (allKnown.has(msg.upload_id)) return;
|
||||
const slide = await fetchUpload(msg.upload_id); // or use payload directly
|
||||
allKnown.set(slide.id, slide);
|
||||
liveQueue.push(slide);
|
||||
});
|
||||
```
|
||||
|
||||
Listen on **`upload-processed`**, not `new-upload` — the preview/thumbnail must exist before
|
||||
we try to display the slide.
|
||||
|
||||
### Deletion / hiding
|
||||
|
||||
```ts
|
||||
sseClient.on('upload-deleted', ({ upload_id }) => {
|
||||
allKnown.delete(upload_id);
|
||||
liveQueue = liveQueue.filter(s => s.id !== upload_id);
|
||||
shuffleQueue = shuffleQueue.filter(s => s.id !== upload_id);
|
||||
if (currentSlide?.id === upload_id) advanceImmediately();
|
||||
});
|
||||
```
|
||||
|
||||
Hidden uploads (banned user with `uploads_hidden=true`) need either a new SSE event or to
|
||||
piggyback `upload-deleted` for diashow purposes. Simplest path: emit `upload-deleted` for
|
||||
hidden posts to all subscribers (the live feed already filters them via `v_feed`, so this is
|
||||
a backwards-compatible signal).
|
||||
|
||||
---
|
||||
|
||||
## Initial pool
|
||||
|
||||
On start:
|
||||
|
||||
1. Call `GET /api/v1/feed?limit=200` (or paginate-and-drain in the background while the
|
||||
diashow runs).
|
||||
2. Push every returned upload into `allKnown`.
|
||||
3. Build the first `shuffleQueue` from it.
|
||||
4. Open the SSE stream and route `upload-processed` / `upload-deleted` into the queues.
|
||||
|
||||
If the event is empty, show a friendly placeholder:
|
||||
*"Noch keine Beiträge — neue erscheinen automatisch."*
|
||||
|
||||
---
|
||||
|
||||
## Frontend surface
|
||||
|
||||
### Entry point
|
||||
|
||||
A small **Diashow / "Präsentation starten"** action visible:
|
||||
|
||||
- In the feed header (icon next to the list/grid toggle) on tablet/desktop layouts.
|
||||
- In the Account page on mobile (less prominent — diashow is primarily a venue-screen
|
||||
feature).
|
||||
|
||||
Tapping it navigates to the `/diashow` route (full-screen, bottom nav hidden).
|
||||
|
||||
### Route: `/diashow`
|
||||
|
||||
- Fullscreen request via `element.requestFullscreen()` after first user gesture.
|
||||
- **Screen Wake Lock**: `navigator.wakeLock.request('screen')` to keep the screen on during
|
||||
long shows. Renew on `visibilitychange` if needed.
|
||||
- Default dwell: **6 seconds** per slide. Configurable via overlay control: 3 / 6 / 10 s.
|
||||
- Tap or `Escape` reveals an overlay with: pause/resume, dwell selector, **transition
|
||||
picker**, exit.
|
||||
- Transitions: crossfade (≈400 ms) by default; Ken Burns, zoom, slide, etc. available as
|
||||
pluggable components — see "Pluggable transitions" below.
|
||||
- Videos: autoplay muted, fit-to-screen, advance on `ended` or after `max(dwell, 12 s)`,
|
||||
whichever first.
|
||||
- Preload the next slide's media into a hidden `<img>`/`<video>` to avoid flashing.
|
||||
- **Media source respects the user's data mode**
|
||||
(see [FEATURES.md §2.5](FEATURES.md)). In Saver mode the diashow loads `preview_url`;
|
||||
in Original mode it loads the original. The data-usage warning is shown once when the
|
||||
mode is toggled in My Account — the diashow itself stays silent.
|
||||
|
||||
### Pluggable transitions
|
||||
|
||||
Each transition is a **drop-in Svelte component** under
|
||||
`frontend/src/lib/diashow/transitions/` (path finalised at implementation time). The
|
||||
interface is intentionally tiny:
|
||||
|
||||
```ts
|
||||
// pseudocode — the real shape lands with the feature
|
||||
export interface SlideTransition {
|
||||
id: string; // 'crossfade', 'kenburns', ...
|
||||
label: string; // shown in the dwell/transition picker
|
||||
durationMs: number; // default; can be overridden per-event
|
||||
// The actual motion is implemented by mounting the component with `from` / `to` slides.
|
||||
}
|
||||
```
|
||||
|
||||
A small registry maps `id → component`; the settings popover renders that registry as a
|
||||
dropdown. **Adding a new animation is one new file plus one line in the registry — no
|
||||
other changes required.** This is the maintainability target called out in
|
||||
[FEATURES.md §2.9](FEATURES.md) and [IDEAS.md](IDEAS.md) ("Animation pack").
|
||||
|
||||
The same pattern is a candidate for whole-event "themes" later — a bundle of (transition
|
||||
+ dwell + optional background-music defaults).
|
||||
|
||||
### Edge cases
|
||||
|
||||
| Case | Behaviour |
|
||||
|--------------------------------------------|--------------------------------------------------------|
|
||||
| Empty event | Placeholder card; live SSE will trigger the first show |
|
||||
| All known uploads are still compressing | Same placeholder — wait for `upload-processed` |
|
||||
| Network drop / SSE reconnect | EventSource auto-reconnects; queues survive |
|
||||
| Current slide gets deleted | Advance immediately |
|
||||
| Event is closed (no new uploads possible) | Diashow keeps running on shuffle queue indefinitely |
|
||||
| Banned user's content (`uploads_hidden`) | Removed via `upload-deleted` signal (see Deletion) |
|
||||
|
||||
---
|
||||
|
||||
## Backend changes
|
||||
|
||||
**Essentially none.** The diashow reuses:
|
||||
|
||||
- `GET /api/v1/feed` (initial pool)
|
||||
- `GET /api/v1/stream` SSE (`upload-processed`, `upload-deleted`)
|
||||
|
||||
Optional small additions:
|
||||
|
||||
1. Emit `upload-deleted` (or a new `upload-hidden`) when a host bans a user with
|
||||
`hide_uploads=true`, so that diashow clients can scrub the relevant slides without
|
||||
reloading.
|
||||
2. Consider raising the cap on `GET /api/v1/feed?limit=` for diashow clients (or paginate
|
||||
the initial pool in the background — preferred, no API change needed).
|
||||
|
||||
---
|
||||
|
||||
## Future extensions (not in scope for v1)
|
||||
|
||||
The big ones live in [IDEAS.md](IDEAS.md) under "Diashow extensions" — most notably the
|
||||
**global / synchronised diashow** where multiple screens share one server-side cursor.
|
||||
Short list of others kept here for context:
|
||||
|
||||
- **Curated highlights mode** — only show uploads tagged with a specific hashtag, or
|
||||
Host-pinned "Story" uploads (depends on the story-highlights feature).
|
||||
- **Audio bed** — host can pick a background track; mute videos so they don't fight the
|
||||
music.
|
||||
- **Slide caption / uploader chyron** — small lower-third with the uploader's name and
|
||||
caption. Out by default to keep the visual clean.
|
||||
Reference in New Issue
Block a user