- 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>
289 lines
10 KiB
Markdown
289 lines
10 KiB
Markdown
# HTML Viewer Export Concept
|
|
|
|
> **Status: IMPLEMENTED.** Viewer source: [frontend/export-viewer/](../frontend/export-viewer/).
|
|
> Pre-built output committed to [backend/static/export-viewer/](../backend/static/export-viewer/).
|
|
> Backend export pipeline: [backend/src/services/export.rs](../backend/src/services/export.rs).
|
|
>
|
|
> Outstanding follow-ups:
|
|
> - The export-viewer's `tailwind.config.js` does not yet extend the main app's config. Visual
|
|
> drift risk — see "Shared Tailwind Config" section below.
|
|
> - Service-worker (offline PWA caching) is still "Future" — fine for v1 since the ZIP is
|
|
> already fully offline by virtue of relative paths.
|
|
|
|
## Overview
|
|
|
|
The HTML Viewer export produces a **self-contained offline ZIP** that is a read-only clone
|
|
of the live EventSnap feed. Opening `index.html` in any modern browser shows the full event
|
|
gallery — list view, grid view, search, filter, lightbox — with no internet connection or
|
|
server required.
|
|
|
|
It **replaces the current HTML export job type**. The old HTML export produced a raw
|
|
minijinja-rendered template; the new viewer supersedes it entirely. The existing `html`
|
|
job variant in the backend is repurposed to run this flow instead of being kept alongside
|
|
it.
|
|
|
|
It is distinct from the ZIP archive export (which is raw media files). The viewer is a
|
|
polished, navigable web app bundled with the event's content.
|
|
|
|
---
|
|
|
|
## User-Facing Behavior
|
|
|
|
- Download `event_name_viewer.zip`, unzip, open `index.html`
|
|
- Full list view (chronological, newest first) and grid view with search/filter
|
|
- Likes, comments, and reaction counts shown (static snapshot from export time)
|
|
- Read-only: no uploads, no auth, no dashboards
|
|
- Works offline, no CDN or external resources
|
|
|
|
---
|
|
|
|
## Architecture
|
|
|
|
### Separate Static SvelteKit App
|
|
|
|
A new mini SvelteKit project lives at `frontend/export-viewer/` within the same monorepo.
|
|
It uses `adapter-static` and is kept completely independent of the main app.
|
|
|
|
**Why separate rather than a shared route:**
|
|
- The viewer must be distributable as a standalone static bundle; the main app uses
|
|
`adapter-node` and cannot be mixed
|
|
- Keeping it separate avoids auth, store, and routing dependencies leaking in
|
|
- Simpler to reason about: the viewer has exactly two concerns (list view, grid view)
|
|
|
|
**Why same repo:**
|
|
- Shares Tailwind config and design tokens → visual parity with the main app
|
|
- Single `pnpm` workspace, no separate CI needed
|
|
- Backend can reference the pre-built output by relative path
|
|
|
|
### Pre-Built Output Committed to Repo
|
|
|
|
The viewer is built once and its output committed to `backend/static/export-viewer/`.
|
|
The backend export job **does not run a Node build** at runtime — it just copies the
|
|
pre-built assets and injects event data alongside them.
|
|
|
|
When the viewer source changes, a developer rebuilds it locally (`pnpm build` in
|
|
`frontend/export-viewer/`) and commits the updated `backend/static/export-viewer/` output.
|
|
|
|
---
|
|
|
|
## ZIP Structure
|
|
|
|
```
|
|
event_name_viewer.zip/
|
|
├── index.html ← entry point; open this in any browser
|
|
├── _app/
|
|
│ └── immutable/
|
|
│ ├── viewer.[hash].js ← all Svelte/app logic, single bundle
|
|
│ └── viewer.[hash].css ← all styles including Tailwind output
|
|
├── data.json ← injected by backend at export time
|
|
└── media/
|
|
├── abc123_thumb.jpg ← ~400 px wide, used in grid cells
|
|
├── abc123_full.jpg ← original or capped (see Media Strategy)
|
|
├── def456_thumb.jpg
|
|
└── def456.mp4 ← videos included as-is
|
|
```
|
|
|
|
No external font CDN, no Google Fonts, no remote scripts. All assets are local.
|
|
|
|
---
|
|
|
|
## data.json Schema
|
|
|
|
Generated by the backend export job. The viewer fetches this file on startup via
|
|
`fetch('./data.json')` (relative path, works from filesystem).
|
|
|
|
```json
|
|
{
|
|
"event": {
|
|
"name": "Sommerfest 2025",
|
|
"exported_at": "2025-07-15T20:00:00Z"
|
|
},
|
|
"posts": [
|
|
{
|
|
"id": "abc123",
|
|
"uploader": "MaxMustermann",
|
|
"caption": "Tolle Stimmung! #party #spaß",
|
|
"tags": ["party", "spaß"],
|
|
"timestamp": "2025-07-15T18:30:00Z",
|
|
"likes": 12,
|
|
"comments": [
|
|
{
|
|
"author": "AnnaSchulz",
|
|
"text": "So schön!",
|
|
"timestamp": "2025-07-15T18:35:00Z"
|
|
}
|
|
],
|
|
"media": [
|
|
{
|
|
"type": "image",
|
|
"thumb": "media/abc123_thumb.jpg",
|
|
"full": "media/abc123_full.jpg",
|
|
"width": 1920,
|
|
"height": 1080
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
All post data (likes, comments, tags) reflects the state at export time. No live updates.
|
|
|
|
---
|
|
|
|
## Media Strategy
|
|
|
|
### Images
|
|
|
|
| Variant | Purpose | Max dimension | Format |
|
|
|---------|---------|---------------|--------|
|
|
| `_thumb` | Grid cells, list post thumbnail | 400 px wide | JPEG q75 |
|
|
| `_full` | Lightbox / full-screen view | Original, or 2000 px cap if >5 MB | JPEG q85 |
|
|
|
|
The backend applies compression only when the original exceeds a threshold (e.g. >5 MB for
|
|
images). Below that threshold the original is used as `_full` unchanged.
|
|
|
|
The full-resolution original is always available via the separate ZIP archive export.
|
|
|
|
### Videos
|
|
|
|
Included as-is (no server-side transcoding). The viewer uses a standard `<video>` element.
|
|
The `_thumb` variant for videos is a JPEG frame extracted at the 1-second mark.
|
|
|
|
---
|
|
|
|
## SvelteKit SSG Configuration
|
|
|
|
```
|
|
// frontend/export-viewer/src/routes/+layout.ts
|
|
export const prerender = true;
|
|
export const ssr = false;
|
|
```
|
|
|
|
`ssr = false` produces a pure client-side SPA: SvelteKit emits a minimal `index.html` shell
|
|
and the JavaScript bundle hydrates it entirely in the browser. This is correct for a ZIP
|
|
distribution where no server exists to handle SSR.
|
|
|
|
```
|
|
// frontend/export-viewer/svelte.config.js
|
|
import adapter from '@sveltejs/adapter-static';
|
|
|
|
export default {
|
|
kit: {
|
|
adapter: adapter({
|
|
fallback: 'index.html',
|
|
pages: '../../backend/static/export-viewer',
|
|
assets: '../../backend/static/export-viewer',
|
|
})
|
|
}
|
|
};
|
|
```
|
|
|
|
The build output is written directly into `backend/static/export-viewer/` so the backend
|
|
can reference it without a copy step.
|
|
|
|
### Shared Tailwind Config
|
|
|
|
```
|
|
// frontend/export-viewer/tailwind.config.js
|
|
import baseConfig from '../tailwind.config.js';
|
|
export default { ...baseConfig, content: ['./src/**/*.{svelte,ts}'] };
|
|
```
|
|
|
|
Imports the main app's Tailwind config to guarantee visual parity. Only the `content` glob
|
|
is overridden.
|
|
|
|
---
|
|
|
|
## Viewer Feature Set
|
|
|
|
| Feature | Included | Notes |
|
|
|---------|----------|-------|
|
|
| List view (chronological, newest first) | ✓ | Full-width cards, same layout as live app |
|
|
| Grid view (3-column) | ✓ | Square cells, video duration badge |
|
|
| List/grid toggle | ✓ | Same toggle icons as live app |
|
|
| Search bar (grid view only) | ✓ | Appears only in grid view |
|
|
| Tag filter chips | ✓ | Built from tags in data.json |
|
|
| Uploader filter | ✓ | Dropdown from uploaders in data.json |
|
|
| Autocomplete suggestions | ✓ | From data.json — no network requests |
|
|
| Lightbox (tap to expand) | ✓ | Swipe left/right navigates filtered set |
|
|
| Like counts (static) | ✓ | Snapshot from export time |
|
|
| Comment list (static) | ✓ | Expandable under each post |
|
|
| Like/comment actions | ✗ | Read-only export |
|
|
| Upload button / FAB | ✗ | |
|
|
| Account / Host / Admin | ✗ | |
|
|
| Authentication | ✗ | No JWT, no PIN |
|
|
| Service Worker (offline cache) | Future | Could be added later for PWA behavior |
|
|
|
|
---
|
|
|
|
## Backend Export Job Flow
|
|
|
|
The `html` job variant is repurposed. The old minijinja template rendering path is removed
|
|
and replaced entirely by the steps below.
|
|
|
|
```
|
|
1. Query all posts, media, reactions, and comments for the event from the DB
|
|
2. Copy pre-built viewer assets:
|
|
backend/static/export-viewer/ → tmp/{job_id}/
|
|
3. Generate data.json:
|
|
- Build the JSON structure from queried data
|
|
- Write to tmp/{job_id}/data.json
|
|
4. Process and copy media:
|
|
For each media file:
|
|
a. Copy original; if image >5 MB, also produce compressed _full variant
|
|
b. Generate _thumb (resize to 400 px wide via image library)
|
|
c. For video, extract JPEG frame for _thumb
|
|
d. Write to tmp/{job_id}/media/
|
|
5. Create ZIP:
|
|
zip -r event_name_viewer.zip tmp/{job_id}/
|
|
6. Store ZIP path, mark job as complete
|
|
7. Clean up tmp/{job_id}/
|
|
```
|
|
|
|
The backend needs an image processing dependency (e.g. `image` crate in Rust) for thumbnail
|
|
generation and compression. Video frame extraction requires `ffmpeg` available in the
|
|
deployment environment (already used for video handling if applicable, otherwise add to
|
|
docker-compose).
|
|
|
|
---
|
|
|
|
## Monorepo Structure After Implementation
|
|
|
|
```
|
|
EventSnap/
|
|
├── backend/
|
|
│ ├── static/
|
|
│ │ └── export-viewer/ ← pre-built viewer output (committed)
|
|
│ │ ├── index.html
|
|
│ │ └── _app/...
|
|
│ └── src/
|
|
│ └── handlers/
|
|
│ └── export.rs ← export job assembles ZIP
|
|
├── frontend/
|
|
│ ├── export-viewer/ ← new mini SvelteKit project
|
|
│ │ ├── package.json
|
|
│ │ ├── svelte.config.js ← adapter-static, output → backend/static/export-viewer
|
|
│ │ ├── tailwind.config.js ← extends ../tailwind.config.js
|
|
│ │ └── src/
|
|
│ │ └── routes/
|
|
│ │ ├── +layout.ts ← prerender=true, ssr=false
|
|
│ │ └── +page.svelte ← list/grid feed, lightbox, search
|
|
│ └── src/ ← existing main app (unchanged)
|
|
└── docs/
|
|
├── CONCEPT_MOBILE_UI.md
|
|
└── CONCEPT_HTML_VIEWER.md
|
|
```
|
|
|
|
---
|
|
|
|
## Open Questions for Implementation
|
|
|
|
1. **Image processing library**: The `image` crate handles JPEG resize/compress; is it
|
|
already a backend dependency, or does it need to be added?
|
|
2. **Video thumbnail extraction**: Is `ffmpeg` available in the Docker environment?
|
|
If not, a fallback (no video thumb, use a placeholder) is needed.
|
|
3. **Viewer rebuild workflow**: Add a `make build-viewer` or `pnpm --filter export-viewer build`
|
|
step to the developer workflow docs and CI so the committed output stays in sync.
|
|
4. **ZIP file naming**: `{event_slug}_viewer_{date}.zip` or a fixed name?
|