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:
313
docs/FEATURES.md
Normal file
313
docs/FEATURES.md
Normal file
@@ -0,0 +1,313 @@
|
||||
# EventSnap — Feature Set & Capability Matrix
|
||||
|
||||
This document is the authoritative, code-cross-checked summary of what EventSnap can do today
|
||||
and what is planned. For the design rationale of each area see [PROJECT.md](../PROJECT.md);
|
||||
for journeys / step-by-step flows see [USER_JOURNEYS.md](USER_JOURNEYS.md).
|
||||
|
||||
Status legend: **✓ shipped** · **◐ partial** · **◯ planned** · **✗ out of scope**
|
||||
|
||||
---
|
||||
|
||||
## 1. Capability matrix by role
|
||||
|
||||
| Capability | Guest | Host | Admin | Notes |
|
||||
|---------------------------------------------------------|:-----:|:-----:|:-----:|-----------------------------------------------------------------------|
|
||||
| **Onboarding & sessions** | | | | |
|
||||
| Join via shared event link / QR code | ✓ | ✓ | ✓ | Name-only registration; server issues JWT + 4-digit PIN |
|
||||
| First-visit guided tour (4 steps) | ✓ | ✓ | ✓ | Dismissed once, flag in `localStorage` |
|
||||
| Persistent 30-day session | ✓ | ✓ | ✓ | JWT in `localStorage`; refreshed on activity |
|
||||
| Sign in on another device using name + PIN | ✓ | ✓ | ✓ | 3 wrong PINs → 15-min lockout |
|
||||
| "Ich habe bereits einen Account" link on the join page | ✓ | ✓ | ✓ | Small inline link → `/recover` (name + PIN) |
|
||||
| View / copy own PIN any time ("My Account") | ✓ | ✓ | ✓ | Read from `localStorage`; never sent back from the server |
|
||||
| Log out / "Leave event" | ✓ | ✓ | ✓ | Confirmation bottom-sheet; invalidates the session row |
|
||||
| Rename own display name | ◯ | ◯ | ◯ | Not yet wired; PIN-protected change |
|
||||
| Pick **data mode** (Saver / Original) in My Account | ✓ | ✓ | ✓ | Saver = compressed (default). Original = full files + data-usage warning. Applies to feed and diashow. Per-device, in `localStorage` |
|
||||
| Read the **Datenschutzhinweis** (privacy note) | ✓ | ✓ | ✓ | Free text set by Admin during setup; rendered preformatted in My Account; first-visit guide briefly points to it |
|
||||
| Admin password login (separate route) | | | ✓ | 1-day token; lives in `sessionStorage` |
|
||||
| Reset another user's PIN (one-time display modal) | | ✓* | ✓ | Host: guests only. Admin: hosts + guests. New PIN shown once to the requester; user signs in with it; PIN is stored on their device on next login. \* Host cannot reset another Host's PIN |
|
||||
| | | | | |
|
||||
| **Posting** | | | | |
|
||||
| Pick photos/videos from device library (multi-select) | ✓ | ✓ | ✓ | Bottom-sheet source picker |
|
||||
| In-app camera capture (`getUserMedia`) | ✓ | ✓ | ✓ | Front/back toggle, photo, `MediaRecorder` video |
|
||||
| Caption + `#hashtag` extraction | ✓ | ✓ | ✓ | Optional; hashtags parsed server-side |
|
||||
| Edit own caption / hashtags after upload | ✓ | ✓ | ✓ | `PATCH /api/v1/upload/{id}` |
|
||||
| Delete own upload | ✓ | ✓ | ✓ | Long-press on the card (or the kebab menu on desktop) → **Löschen** in the context sheet. Comment-style trash icon also available on each post elsewhere as it's added. |
|
||||
| Delete own comment | ✓ | ✓ | ✓ | Trash icon in lightbox |
|
||||
| Background upload queue (survives reload) | ✓ | ✓ | ✓ | IndexedDB-persisted, sequential, retry |
|
||||
| Rate-limit auto-resume banner | ✓ | ✓ | ✓ | Countdown above bottom nav; resumes when window opens |
|
||||
| Chunked / resumable upload for > 100 MB | ◯ | ◯ | ◯ | Planned (v1.x) |
|
||||
| | | | | |
|
||||
| **Feed & social** | | | | |
|
||||
| Chronological list feed (full-width cards) | ✓ | ✓ | ✓ | Default view, infinite scroll |
|
||||
| 3-column grid feed with toggle | ✓ | ✓ | ✓ | Video play badges, duration |
|
||||
| Search & autocomplete (uploader + hashtag) | ✓ | ✓ | ✓ | Grid view; derived in-memory, no extra API calls |
|
||||
| Active filter chips (OR within type, AND across types) | ✓ | ✓ | ✓ | Multiple hashtags = OR; uploader + hashtag = AND |
|
||||
| Fullscreen lightbox with swipe | ✓ | ✓ | ✓ | Swipe navigates the filtered set |
|
||||
| Like / unlike any post | ✓ | ✓ | ✓ | Single toggle; SSE `like-update` |
|
||||
| Read comments on any post | ✓ | ✓ | ✓ | |
|
||||
| Add a comment | ✓ | ✓ | ✓ | Hashtags in comments also parsed |
|
||||
| Real-time feed via SSE | ✓ | ✓ | ✓ | `new-upload`, `new-comment`, `like-update`, `upload-processed`, `pin-reset`, `event-updated`, etc. |
|
||||
| Pause SSE when app is backgrounded | ✓ | ✓ | ✓ | Page Visibility API; reconnect on foreground |
|
||||
| Delta-fetch (`/feed/delta?since=`) on reconnect | ✓ | ✓ | ✓ | Runs on every visibility-restore; merges new + deleted uploads |
|
||||
| Individual file download button per post | ✓ | ✓ | ✓ | "Original anzeigen" in the post context sheet — streams via `/api/v1/upload/{id}/original` |
|
||||
| | | | | |
|
||||
| **Live diashow** (see [CONCEPT_DIASHOW.md](CONCEPT_DIASHOW.md)) | | | | |
|
||||
| Start fullscreen auto-advancing slideshow | ✓ | ✓ | ✓ | Two queues: live (SSE) drains first, shuffle as fallback. Crossfade + Ken Burns transitions; pluggable. Respects data mode. |
|
||||
| | | | | |
|
||||
| **Moderation (Host)** | | | | |
|
||||
| List all event users | | ✓ | ✓ | Includes upload count, total bytes |
|
||||
| Ban / unban a user | | ✓ | ✓ | Modal asks: hide their existing uploads, or keep visible? |
|
||||
| Delete any upload | | ✓ | ✓ | |
|
||||
| Delete any comment | | ✓ | ✓ | |
|
||||
| Promote guest to Host | | ✓ | ✓ | |
|
||||
| Demote Host to guest | | ✓ | ✓ | Hosts may demote other Hosts. Cannot demote self. Admins cannot be demoted by hosts. |
|
||||
| Reset a guest's PIN (Host) / any non-admin PIN (Admin) | | ✓ | ✓ | New PIN shown once in modal; Host shows/shares it with the guest |
|
||||
| Lock new uploads ("Event schließen") | | ✓ | ✓ | Likes + comments + browsing remain open |
|
||||
| Unlock new uploads | | ✓ | ✓ | |
|
||||
| Release gallery → trigger export generation | | ✓ | ✓ | Enqueues both ZIP and HTML-viewer jobs |
|
||||
| | | | | |
|
||||
| **Instance configuration (Admin)** | | | | |
|
||||
| Live disk-usage / user / upload / banned stats | | | ✓ | Stats tab; queries `sysinfo` |
|
||||
| Edit per-file limits (image MB / video MB) | | | ✓ | Config tab; hot-reloadable from DB |
|
||||
| Edit per-endpoint rate limits | | | ✓ | Upload/hour, feed/min, export/day |
|
||||
| Toggle **all** rate limits on/off | | | ✓ | Master switch — when off, every limiter passes through |
|
||||
| Toggle **individual** rate limits on/off | | | ✓ | Per-endpoint switch (upload / feed / export / join) |
|
||||
| Toggle quota enforcement on/off (master + per-area) | | | ✓ | Master switch + per-area (storage / upload count). When off, nothing is enforced |
|
||||
| Edit quota tolerance | | | ✓ | Live `(free_disk × tolerance) / active_uploaders` formula enforced on upload |
|
||||
| Edit estimated guest count | | | ✓ | |
|
||||
| Edit compression-worker concurrency | | | ✓ | |
|
||||
| Edit **Datenschutzhinweis** (privacy note, free text) | | | ✓ | Plain text, whitespace + newlines preserved, no HTML. SSE `event-updated` broadcasts edits live. |
|
||||
| Inspect export job list & progress | | | ✓ | |
|
||||
| Low-disk alert (< 10 GB free) | | | ◯ | Planned |
|
||||
| Event banner / cover image | | | ◯ | DB column exists, no UI |
|
||||
| | | | | |
|
||||
| **Quota visibility (Guest-facing)** | | | | |
|
||||
| Show current per-user quota estimate | ✓ | ✓ | ✓ | "Du hast X MB von Y MB genutzt." in My Account and on the upload screen. Computed from the live formula. Hidden when quota enforcement is toggled off |
|
||||
| | | | | |
|
||||
| **Export** | | | | |
|
||||
| Wait at locked export page until released | ✓ | ✓ | ✓ | Friendly "not yet available" copy |
|
||||
| Download `Gallery.zip` (full-quality originals) | ✓ | ✓ | ✓ | Streamed via `async-zip`; `Photos/` + `Videos/` folders |
|
||||
| Download `Memories.zip` (offline HTML viewer) | ✓ | ✓ | ✓ | Self-contained SvelteKit-static app + `data.json` + `media/` |
|
||||
| HTML-export in-app guide modal before download | ✓ | ✓ | ✓ | Explains: unzip first, open `index.html` |
|
||||
| Per-IP export download rate limit (3 / day) | ✓ | ✓ | ✓ | |
|
||||
| | | | | |
|
||||
| **Banned guest** (subset) | | | | |
|
||||
| Cannot upload, like, or comment | ✗ | | | Returns HTTP 403 |
|
||||
| Can browse the feed | ✓ | | | |
|
||||
| Can still download the export once released | ✓ | | | Spec design choice |
|
||||
|
||||
---
|
||||
|
||||
## 2. Feature areas in detail
|
||||
|
||||
### 2.0 Touch-first interactions (mobile) vs. buttons (desktop)
|
||||
|
||||
EventSnap is mobile-first. Where it makes the UI cleaner, primary actions are reached via
|
||||
**gestures** on touch devices, with conventional **buttons** mirrored on tablet/desktop:
|
||||
|
||||
- **Long-press on a post** → context bottom sheet ("Löschen", "Original anzeigen", report,
|
||||
share). On desktop the same actions are a kebab/⋯ menu in the card's corner.
|
||||
- **Long-press on a comment** → context sheet with "Löschen" (own comments only) and
|
||||
"Kopieren".
|
||||
- **Swipe left/right in the lightbox** → navigate the filtered set.
|
||||
- **Swipe down on a bottom sheet** → dismiss.
|
||||
- **Pull-to-refresh on the feed** → force a delta-fetch even when SSE is up.
|
||||
- **Double-tap on a post** → like (Instagram-style), with a heart-burst animation. Tap the
|
||||
heart icon as the explicit alternative.
|
||||
|
||||
Design rule: **gestures should always have a discoverable button equivalent** somewhere on
|
||||
the page, so the app stays usable on a stylus, mouse, or for users who don't know the
|
||||
gesture vocabulary. Take inspiration from Instagram, WhatsApp, and Telegram for the
|
||||
"feels right" baseline — long-press for context, swipe to dismiss, double-tap to react.
|
||||
|
||||
### 2.1 Authentication and identity
|
||||
|
||||
EventSnap's identity model is "**a name + a 4-digit PIN, scoped to one event**". There is no
|
||||
email, no password, no account portal.
|
||||
|
||||
- **Joining.** On the join page the user types a display name. The server creates a `user`
|
||||
row, generates a 4-digit PIN, stores `bcrypt(pin)`, signs a 30-day JWT, and returns the
|
||||
PIN in clear text **once** in the response. The client persists the JWT and the PIN to
|
||||
`localStorage`.
|
||||
- **PIN visibility.** The PIN is shown to the user *prominently* once at registration, and
|
||||
remains visible in the My-Account page (read directly from `localStorage` — never sent
|
||||
back from the server).
|
||||
- **Returning on the same device.** A valid JWT in `localStorage` → straight to the feed.
|
||||
- **Returning on a new device.** Type the name on the join page → server detects the
|
||||
existing user → user is prompted for their PIN. `bcrypt.verify` → new JWT, fresh device
|
||||
is now bound to the same account.
|
||||
- **Lockout.** 3 wrong PIN attempts → 15-minute lockout per user (`pin_locked_until` column,
|
||||
migration 006).
|
||||
- **Name collisions.** Names are unique per event (case-insensitive, migration 007). If
|
||||
someone tries to join with a name already taken, the join page automatically presents the
|
||||
PIN-recovery form for that account ("Already taken — sign in instead, or pick another
|
||||
name"). The join page also surfaces an explicit **"Ich habe bereits einen Account"**
|
||||
link routing to `/recover` for users who already know they want to sign in.
|
||||
- **PIN reset by Host / Admin.** Planned. If a guest loses their PIN and `localStorage` is
|
||||
gone everywhere, a Host (for guests) or Admin (for hosts and guests) can hit a
|
||||
**PIN zurücksetzen** action in the user list. A fresh PIN is generated server-side, its
|
||||
bcrypt stored, and the plaintext is shown **once** in a modal to the requesting
|
||||
operator. The operator shows / sends the new PIN to the user, who then signs in via
|
||||
`/recover` — the PIN is persisted to `localStorage` on that device on a successful
|
||||
recovery, exactly like a brand-new join. Host cannot reset another Host's PIN; only
|
||||
Admins can.
|
||||
- **Roles.** `guest` (default), `host`, `admin`. The Admin role is seeded from the
|
||||
`ADMIN_PASSWORD_HASH` env var; admins log in at `/admin/login` with a password (separate
|
||||
JWT, 1-day expiry, in `sessionStorage`). Hosts are guests promoted by an admin. **Hosts
|
||||
may also demote other Hosts to guests** (planned) — but never themselves, to avoid
|
||||
locking the event out of moderation. Admins can demote anyone except admins.
|
||||
|
||||
### 2.2 Posting pipeline
|
||||
|
||||
The upload pipeline is built for flaky mobile networks:
|
||||
|
||||
1. **Source picker** (bottom sheet from the FAB): camera or gallery.
|
||||
2. **Preview screen** — staged files appear as thumbnails; user can remove individuals, add
|
||||
a caption (with `#hashtags`), and tap quick-tag chips derived from the caption.
|
||||
3. **Submit** — the client immediately returns to the feed (optimistic UX). Files enter an
|
||||
IndexedDB-persisted queue.
|
||||
4. **Queue worker** — runs sequentially (one upload at a time), per-file progress via XHR.
|
||||
Survives reloads and app backgrounding. A red badge on the FAB indicates active uploads.
|
||||
5. **Server-side processing** — multipart received → MIME-sniffed via `infer` → size
|
||||
validated → original stored → compression worker (bounded by a `tokio::sync::Semaphore`)
|
||||
resizes to an 800-px preview (images via the `image` crate + `oxipng` for PNG) or
|
||||
extracts a frame at the 1-second mark (videos via `ffmpeg`). Status is tracked in the new
|
||||
`compression_status` column (migration 008).
|
||||
6. **Real-time fan-out** — `new-upload` SSE first (no preview yet), then `upload-processed`
|
||||
when the preview/thumbnail is ready, so clients can swap a placeholder for the real image
|
||||
without re-fetching the feed.
|
||||
7. **Rate-limit-aware client** — when the server returns HTTP 429 with `Retry-After`, the
|
||||
queue parks remaining items and shows an inline countdown banner; uploads resume
|
||||
automatically.
|
||||
|
||||
### 2.3 Feed
|
||||
|
||||
- **Two layouts** — chronological list (default) and 3-column grid. Toggle in the header.
|
||||
- **List view** has no search; it's the consumption-focused mode (like an Instagram feed).
|
||||
- **Grid view** has the search bar — autocomplete suggestions are computed in-memory from
|
||||
the loaded uploads, so typing never hits the server.
|
||||
- **Filter chips** — multiple hashtags combine with OR; multiple uploaders combine with OR;
|
||||
hashtag + uploader combine with AND. Matches the redesign concept exactly.
|
||||
- **Lightbox** — fullscreen view, swipe navigates the *filtered* set, with embedded
|
||||
like/comment UI.
|
||||
- **Real-time** — SSE delivers `new-upload`, `upload-processed`, `like-update`,
|
||||
`new-comment`, `upload-deleted`, `event-closed`/`event-opened`, `export-progress`,
|
||||
`export-available`. Client pauses SSE on `visibilitychange: hidden` and reopens on visible.
|
||||
|
||||
### 2.4 Host / Admin tooling
|
||||
|
||||
- **Host dashboard** — three collapsible sections: Stats, Event-Einstellungen,
|
||||
Nutzerverwaltung. Ban modal asks explicitly whether to hide the user's existing uploads
|
||||
from the public feed. Promote/demote, lock/unlock, release-gallery are one-tap.
|
||||
- **Admin dashboard** — same dashboard plus three more inner tabs (Stats, Config, Export,
|
||||
Nutzer). Config form covers per-file limits, rate limits, quota tolerance, estimated
|
||||
guest count, and compression concurrency — all stored in the `config` table and read on
|
||||
each request, so changes take effect without a restart. Disk widget pulls from the
|
||||
`sysinfo` crate live.
|
||||
|
||||
### 2.5 Data mode (planned)
|
||||
|
||||
Each device picks a **data mode** in My Account; the setting lives in `localStorage` so a
|
||||
guest can be on Saver on their phone and Original on their laptop.
|
||||
|
||||
| Mode | Default? | Feed loads... | Lightbox / diashow loads... | Warning shown? |
|
||||
|------------|:--------:|-----------------------------|------------------------------|:-------------:|
|
||||
| Datensparer (Saver) | ✓ | preview (compressed) | preview | no |
|
||||
| Original | | original | original | yes — "kann mobile Datennutzung erhöhen" once on enable |
|
||||
|
||||
Applies uniformly to the live app's feed/lightbox **and** the diashow. The viewer (offline
|
||||
HTML export) is unaffected — it's already a snapshot of pre-bundled media variants.
|
||||
|
||||
### 2.6 Rate limits and quotas — toggleable (planned)
|
||||
|
||||
The Admin Config tab gains explicit on/off toggles in addition to the numeric inputs:
|
||||
|
||||
- **Master switch — all rate limits.** When off, every limiter middleware short-circuits to
|
||||
pass-through. Useful for testing or trusted internal events.
|
||||
- **Per-endpoint switches.** Upload / feed / export / join each have their own toggle. The
|
||||
numeric input becomes informational while the toggle is off.
|
||||
- **Master switch — quotas.** When off, no quota check ever runs.
|
||||
- **Per-area quota switch.** Storage-bytes quota and upload-count quota can be disabled
|
||||
independently.
|
||||
|
||||
When a feature is toggled off, the relevant UI in the guest-facing app should adapt: e.g.
|
||||
the "Du hast X von Y MB genutzt" widget hides itself when storage quota is disabled. The
|
||||
quota estimate is computed from the same formula the server uses
|
||||
(`(free_disk × tolerance) / max(active_uploaders, 1)`) — surfaced in My Account *and* on
|
||||
the upload preview screen so guests know before they pick files.
|
||||
|
||||
### 2.7 Privacy note (Datenschutzhinweis, planned)
|
||||
|
||||
Admin sets a free-text **Datenschutzhinweis** during instance setup (Admin Dashboard →
|
||||
Config). It's stored as a single config key (plain text, whitespace and newlines
|
||||
preserved, no HTML). Guests see it in their **My Account** page, rendered inside a
|
||||
preformatted block — no parsing, no markdown, just exactly what the admin typed. The
|
||||
first-visit onboarding guide gains a one-line nudge: *"Datenschutzhinweis findest du in
|
||||
deinem Account."*
|
||||
|
||||
Rationale: many real events (in Germany especially) need a per-event privacy statement
|
||||
without the operator wanting to ship a separate static page or rebuild the app.
|
||||
|
||||
### 2.8 Export
|
||||
|
||||
Two artifacts, both generated on demand after the host taps "Release gallery":
|
||||
|
||||
- **Gallery.zip** — full-quality originals only, structured into `Photos/` and `Videos/`,
|
||||
filenames `{date}_{time}_{username}_{id}.{ext}`, streamed via `async-zip` with no full
|
||||
archive in memory.
|
||||
- **Memories.zip** — the offline HTML viewer. Pre-built SvelteKit-static app from
|
||||
[frontend/export-viewer/](../frontend/export-viewer/), bundled with a generated
|
||||
`data.json` snapshot and a `media/` folder of thumbnails + full-size variants. Open
|
||||
`index.html` in any browser — no server required, no internet required. List/grid views,
|
||||
lightbox, hashtag chips, like counts, comments — all visually matched to the live app.
|
||||
|
||||
The export page shows live progress (SSE) while jobs run, then becomes a download button
|
||||
when complete.
|
||||
|
||||
---
|
||||
|
||||
### 2.9 Maintainability and extensibility
|
||||
|
||||
EventSnap is small enough to be a single-developer project; it should stay easy to extend.
|
||||
A few principles to keep adding features cheap:
|
||||
|
||||
- **Diashow transitions are drop-in components.** Each animation implements a small
|
||||
interface and lives under `frontend/src/lib/diashow/transitions/`. Adding a transition is
|
||||
one file + one entry in a registry.
|
||||
- **Feature toggles live in the `config` table.** Today's rate-limit and quota switches
|
||||
follow the same pattern any new opt-in feature would use — no redeploy to flip
|
||||
behaviour.
|
||||
- **One Svelte store per cross-cutting concern.** Auth, upload queue, SSE, data mode,
|
||||
diashow state — composable rather than copy-pasted into each route.
|
||||
- **Migrations are append-only.** Never edit a shipped migration; always add a new pair.
|
||||
- **Background jobs share one pipeline.** Export and compression already publish progress
|
||||
via the `export_job` row + SSE; future long-running work (analytics, archival) should
|
||||
plug into the same shape.
|
||||
|
||||
See [IDEAS.md](IDEAS.md) for a longer riff on these patterns.
|
||||
|
||||
## 3. Out of scope (intentionally not built)
|
||||
|
||||
These are explicit non-goals from [PROJECT.md §4](../PROJECT.md):
|
||||
|
||||
- Native iOS / Android apps
|
||||
- Multiple simultaneous events (multi-tenancy)
|
||||
- Email-based auth / password reset
|
||||
- Push notifications
|
||||
- User-to-user direct messaging
|
||||
- Payment / monetisation
|
||||
- CI/CD pipeline
|
||||
- "Save to camera roll" automation on iOS/Android — guests download the ZIP and use their
|
||||
platform file manager
|
||||
|
||||
---
|
||||
|
||||
## 4. See also
|
||||
|
||||
- [USER_JOURNEYS.md](USER_JOURNEYS.md) — step-by-step flows for every supported scenario.
|
||||
- [CONCEPT_MOBILE_UI.md](CONCEPT_MOBILE_UI.md) — design reference for the mobile layout.
|
||||
- [CONCEPT_HTML_VIEWER.md](CONCEPT_HTML_VIEWER.md) — export-viewer design.
|
||||
- [CONCEPT_DIASHOW.md](CONCEPT_DIASHOW.md) — planned diashow design.
|
||||
- [IDEAS.md](IDEAS.md) — speculative extensions (global diashow, reactions, multi-tenancy, ...).
|
||||
- [PROJECT.md](../PROJECT.md) — full architectural blueprint and rationale.
|
||||
- [TEST_GUIDE.md](../TEST_GUIDE.md) — manual smoke-test script for the main flows.
|
||||
Reference in New Issue
Block a user