Adversarial re-review of the persona-audit + audit-followup rounds (6411747..)
found one HIGH and one MED regression plus LOW gaps. All fixed with coverage.
HIGH — export stale-keepsake resurrected by an open_event race
A reopen landing in the window between a *current* export worker's finalize_job
and its ready-flag flip cleared export_released_at + the ready flags but left the
export_job row `done` at the same release_seq. The seq-guarded flip then still
matched and re-set export_{zip,html}_ready=TRUE on a pre-reopen snapshot; the next
re-release read that stale TRUE and skipped regeneration (`if ready { continue }`),
serving a keepsake missing every upload from the reopen window — the exact data
loss migration 012 exists to prevent. Both ready-flip UPDATEs are now additionally
anchored on `export_released_at IS NOT NULL`, so a landed reopen makes the flip a
no-op and the re-release regenerates cleanly.
MED — queue dedup broke for reloaded items
loadQueue rebuilt QueueItems from IndexedDB without copying lastModified, which the
new addToQueue dedup keys on. A file re-selected after a page reload / PWA relaunch
missed the duplicate check and uploaded twice. Rehydration now carries lastModified
(extracted to a pure, tested entryToQueueItem helper).
LOW
- diashow: clear the upload-processed debounce timer in onDestroy (no stray
post-unmount /feed fetch).
- USER_JOURNEYS §9.5: document the reconnect-delta ban replay (hidden_user_ids /
uploads_hidden_at, migration 013), not just the live user-hidden SSE.
- e2e api-client: drop the misleading hide_uploads param from banUser — the backend
takes no body and always hides; strip the dead boolean at all call sites.
Tests
- Extract isReversibleLock (the terminal-403 KEEP-vs-PURGE-blob discriminator) into a
pure exported helper + unit tests, so the data-loss-critical branch is covered
without an XHR harness.
- entryToQueueItem unit tests lock the lastModified-carry regression.
- Document the export flip-race guard in the reopen/re-release spec (the sub-ms
finalize↔flip interleave isn't deterministically forceable with fast fixtures;
covered by the SQL guard + the end-to-end completeness test).
Verified: backend 40 tests, frontend 44 unit tests, svelte-check 0 errors,
e2e 156 passed / 1 skipped on chromium-desktop.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follows the perf + security + user-flow work with a role/persona audit (guest, host,
admin, projector) and fixes across three review rounds. Highlights:
HIGH
- Ban now replays on reconnect. A ban isn't a soft-delete, and the `user-hidden` SSE has
no replay, so a client that missed it (esp. the unattended diashow) kept cycling a
banned user's slides. New `uploads_hidden_at` (migration 013) + `hidden_user_ids` in
/feed/delta; feed + diashow evict those users. Applied even on a truncated delta.
MEDIUM
- Locked-upload data loss: a photo staged offline during a lock/release was purged as a
terminal 4xx and lost when the host reopened. New reversible `uploads_locked` error code;
the queue keeps the blob and auto-resumes on the `event-opened` SSE.
- Reopen after release now warns (ConfirmSheet) that it revokes the published keepsake.
- Host "forgotten-PIN" badge updates live (`pin-reset-requested` was broadcast but never
in KNOWN_EVENTS / subscribed); host page also refetches on `pin-reset` so a two-host
race can't hand out a conflicting PIN.
- Ban modal copy fixed (read-only ban, not "session ended"); Degradieren/Sperren/Entsperren
hidden on peer-host rows for non-admins (they always 403'd).
- Host dashboard shows live keepsake generation progress / ready state + link to /export.
- Admin JWT moved to sessionStorage (§11.1) to bound exposure on shared devices.
Export generation guard (H1 from the prior round, hardened): per-(event,type) `release_seq`
(migration 012) with seq-guarded claim/finalize/mark_failed/update_progress, per-generation
temp/final paths, download follows `file_path`, prune only strictly-older generations.
LOW: diashow coalesces upload-processed (avoids self-rate-limit); event-closed reconciles
galleryReleased; /recover gains a forgot-PIN request + drops a stale cached PIN on 401;
delta `>=` tie-break + 429 retry; misc copy/labels.
Adds e2e: ban-replay, upload-lock-code, and rewrites export-reopen-rerelease with a
data-completeness test. Reconciles USER_JOURNEYS §9/§11.
Verified: cargo build clean, 40 unit tests, svelte-check 0 errors, 33 frontend unit
tests, 155 e2e passing on chromium-desktop.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- compression_concurrency wired to boot config (state.rs) and removed as a
dead admin control.
- patch_config gains per-key integer/range validation so numerically-valid-but-
nonsensical values (-1/NaN/3.5) are rejected instead of silently reverting to
default at read time.
- clearAuth now also clears the display name (shared-device residual).
- e2e/Caddyfile.test mirrors prod security headers + the SSE encode-exclusion so
the test stack is representative and can catch header regressions.
- .gitignore: ignore root-level Playwright artifacts (test-results/, report).
- docs: USER_JOURNEYS §10 and SECURITY-BACKLOG updated to match the implemented
read-only-ban behavior and the export-path fix.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Re-review follow-ups (non-blocking quality items):
Tests:
- moderation H1 specs now assert exact `→ 403` instead of bare .rejects.toThrow(),
so a spurious 500 can no longer masquerade as "revocation worked".
- config: added case-insensitivity (upper/mixed-case placeholder) and the
len==32/31 boundary cases for validate_secrets.
- rate_limiter: added the trailing-comma empty-entry case for client_ip (must
fall back, not return "").
(Backend unit tests: 35 pass.)
Docs:
- README: note that with APP_ENV=production a placeholder .env makes the app
refuse to boot and Caddy wait unhealthy — reason is in `docker compose logs app`.
- SECURITY-BACKLOG + sse.rs comment: document that a mid-session ban does not
tear down an already-open SSE stream (new tickets are blocked; low blast radius).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
C1: reset_user_pin wrote to a non-existent column (pin_failed_attempts);
the real column is failed_pin_attempts, so every PIN reset 500'd. Fixed
the column name; new e2e (pin-reset.spec.ts) proves a reset returns a
usable PIN and the target can recover with it.
C2: config.rs::validate_secrets now rejects placeholder-ish secrets
(change_me/dev_secret/placeholder), enforces len>=32 in prod, and
requires a real ADMIN_PASSWORD_HASH. docker-compose.yml sets
APP_ENV=production so the guard actually runs. Corrected the false
"fixed" claim in SECURITY-BACKLOG.md. .env.example documents the rule.
Riders in these files (documented here since git can't split hunks):
- host.rs also carries the event-scoped ban_user fix and the
close_event/open_event no-op broadcast guard (medium).
- docker-compose.yml also adds ORIGIN (H6), app/frontend healthchecks with
Caddy waiting on health, and per-service memory limits (medium).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
From the 2026-06-27 audit branch (fix/audit-2026-06-27-critical-medium), whose
work was ~80% absorbed into main via the batch branches. This ports the pieces
that were NOT in main:
- compression.rs: cap image decode with image::Limits (12000x12000, 256 MiB
max_alloc) via ImageReader instead of image::open(). The upload body-size cap
bounds the file on disk but not the *decoded* dimensions, so a small
decompression-bomb image could OOM the box during decode/resize. Surgical port
of the audit's decode guard only (not its image/video semaphore split).
- docs/SECURITY-BACKLOG.md: the audit's triaged backlog, reconciled against main
(each item tagged done / open / contingent-on-signed-media). Records the still-
open items: host moderation UI gap, owner-delete SSE broadcast, quota
mount-detection (starts_with) + low-disk guard.
- docs/DECISION-media-auth.md: writes up the one real architectural divergence —
main serves media unauthenticated (ServeDir + UUID-capability) while the audit
built a signed/TTL'd gateway. Lays out the tradeoff (leaked URL = permanent vs
~24h access; banned-user access) and a recommendation, for a human decision.
Verified: cargo build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A bundle of v0.16 platform work landing on top of the SvelteKit viewer.
Brings in 7 commits across docs, backend infra + features, frontend
plumbing, the live diashow, all UI surfaces (with dark mode), and a
viewer rebuild.
Headline features:
- Live diashow with two-queue policy (live drains first, shuffle as
fallback) and pluggable transitions. Design: docs/CONCEPT_DIASHOW.md.
- Dark mode: 'system' / 'light' / 'dark' preference, picked in the
onboarding step + the Account page, applied via Tailwind v4
class-based dark variant with FOUC guard.
- Host/Admin PIN reset with one-time PIN modal; admin may also reset
host PINs. Hosts may demote other hosts.
- Per-user dynamic storage quota enforced on upload + live widget in
My Account and on the upload screen. Toggleable per-area.
- All rate-limits + quotas individually toggleable from the admin
config UI (rendered as switches + a privacy_note textarea).
- Mobile-first gestures: long-press → context sheet, double-tap to
like with heart-burst. Buttons stay as desktop equivalents.
- Data mode (Saver vs Original) per device, applied across feed,
lightbox, and diashow.
- Per-event Datenschutzhinweis admin-editable, live-refreshed on all
clients via SSE event-updated.
- /api/v1/upload/{id}/original endpoint, /me/context + /me/quota.
Hardening (latent issues from the long-term review):
- Startup recovery for stuck compression / export jobs after a crash.
- Hourly cleanup of expired sessions + cold rate-limiter HashMap keys.
- ffmpeg 120s timeout with kill_on_drop (no more permit leaks).
- Per-user IndexedDB upload queue (no more cross-user leak on shared
devices); IDB schema bumped to v2.
- SSE reconnect uses exponential backoff (no more retry storm).
- PIN lockout no longer escalates — attempts reset when the cooldown
expires.
- soft_delete is now transactional and decrements total_upload_bytes
so quotas don't drift.
- pin-reset SSE handler filters by user_id so a host resetting Anna's
PIN doesn't clear Bob's cached PIN.
- Privacy note shown preformatted; admin-editable, ≤16 KiB cap.
Docs:
- New: FEATURES.md (role matrix), USER_JOURNEYS.md, IDEAS.md,
CONCEPT_DIASHOW.md, backend/migrations/README.md,
frontend/src/lib/README.md.
- Refresh: PROJECT.md, README.md, TEST_GUIDE.md, the two existing
CONCEPT_*.md banners.
- 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>
- Persistent bottom tab bar (Feed · FAB · Account) on all authenticated pages
- Upload FAB triggers bottom sheet (Galerie / Kamera) → navigates to composer
- Upload page redesigned as full-screen composer with thumbnail strip, textarea,
quick-tag chips, sticky submit button; bottom nav suppressed while composing
- Slim upload progress bar above bottom nav driven by queue state
- Feed: list/grid view toggle; list = chronological full-width FeedListCard;
grid = 3-col with search bar, autocomplete from loaded posts, filter chips
- Account page: role-gated dashboard links (Host / Admin); Konto section with
leave-confirm bottom sheet; no more per-page header nav icons
- Host dashboard: back arrow, collapsible sections, 2-col stats, user search
- Admin dashboard: back arrow, inner tab bar (Stats/Config/Export/Nutzer),
stacked config inputs with sticky save, new Nutzer tab
- BottomNav hidden on unauthenticated pages via isAuthenticated store
- FeedGrid: threeCol prop; OnboardingGuide upload step updated for FAB
- Concept docs added to docs/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>