# 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?
