Replaces single-MP4 + range-request streaming with HLS multi-bitrate
segments to fix video stutter through the Newt tunnel. Range-request
bursts were the root cause; HLS chunks are small and tunnel-friendly,
plus the player adapts bitrate to bandwidth.
Backend
- New BullMQ `hls-transcode` queue (in-process worker, concurrency 1)
- FFmpeg single-pass transcode → 360p/720p/1080p variants with aligned
keyframes; output at /media/local/hls/{id}/master.m3u8
- New /api/{videos|public}/{id}/hls/* routes serving signed manifests
and segments (URLs emitted as /media/* so nginx rewrites to media-api)
- Prisma: HlsStatus enum + 6 fields on Video + index, migration
- Upload + yt-dlp fetch paths enqueue transcode jobs
- ENABLE_HLS_TRANSCODE flag (default off; gates enqueue only)
- Backfill script: `npm run backfill:hls`
- media-api bumped to 4 CPU / 2G for FFmpeg headroom
Frontend
- New useHls hook: lazy-imports hls.js (kept out of main bundle),
native HLS on Safari/iOS, gives up after 2 NETWORK_ERRORs so MP4
fallback engages cleanly
- VideoPlayer, VideoViewerModal, ShortsPage, ProductDetailPage now
prefer HLS when ready; MP4 fallback is automatic
- ShortsPage prefetches next-3 master manifests via <link rel="prefetch">
- PublicVideoCard hover preview stays MP4 (avoids hls.js init latency)
Bunker Admin
Shift.ticketedEventId (nullable FK to TicketedEvent) persists the
link between a staffing shift and its parent event. The shift list
response now includes the linked event (id, slug, title, date) so
the admin UI can show context without a second request.
The Create/Edit Shift drawer gains a conditional Event picker that
only appears when kind is EVENT_STAFFING. Picking an event pre-fills
the form's date, startTime, endTime, and location (venue name +
address) — and seeds the title with "Staff: {event.title}" only if
the title field is still empty, so hand-typed overrides aren't
stomped. Switching kind away from EVENT_STAFFING clears the link.
The shifts table's Kind tag wraps in a tooltip showing the linked
event title when one is present, so organizers can see at a glance
which staffing shifts belong to which event without opening the
drawer.
Bunker Admin
Foundation schema for the FAFC-style volunteer dashboard. Adds:
- ActionCampaign / ActionStep / ActionStepCompletion (stacked-action
mini-campaigns where steps reference existing entities like videos,
petitions, ticketed events; completion is detected at query time
against the per-user model for each step kind)
- Document model for downloadable resources (PDFs etc.) — Photo's
EXIF/sharp pipeline can't host non-image files
- Shift.kind discriminator (ShiftKind enum) so training shifts can
surface separately on the dashboard
- TicketedEvent.featured for the "Take Action" CTA tile
Bunker Admin
Addresses 11 original findings (1 critical, 3 high, 4 medium, 3 low)
plus 4 additional findings from security review:
- Mask secrets in PUT /settings response (was leaking decrypted keys)
- Add paymentCheckoutRateLimit (10/hr/IP) to all 5 checkout endpoints
- Implement durable audit logging to payment_audit_log table
- Pin Stripe API version to 2026-01-28.clover (SDK v20.3.1)
- Add charge.dispute.created/closed webhook handlers with DISPUTED status
- Restore tickets on dispute won, handle charge_refunded closure
- Guard against sentinel passthrough corrupting stored Stripe keys
- Wrap refund DB updates in try/catch with webhook reconciliation fallback
- Add $transaction for product maxPurchases race condition
- Remove dead Payment model lookup from handleChargeRefunded
- Cap donation amount at $100k in both schemas
- Add requirePaymentsEnabled middleware on all checkout routes
- Remove Stripe internal IDs from CSV exports
- Add Cache-Control: no-store on admin settings responses
Bunker Admin
Deferred findings from the March 27 security audit, plus a bug fix:
MongoDB keyfile (bug fix):
- Generate replica.key on first boot via entrypoint script
- Fixes crash from --auth + --keyFile without an existing keyfile
- Applied to docker-compose.yml, docker-compose.prod.yml, CCP template
I7 — Ticket overselling prevention (reservation pattern):
- Add reservedCount field to TicketTier schema
- Atomically increment reservedCount inside transaction on checkout
- Release reservation on checkout.session.completed (webhook)
- Release reservation on checkout.session.expired (webhook)
- Include reservedCount in availability calculations
I17 — Move refresh token to httpOnly cookie:
- Server sets httpOnly SameSite=Strict cookie on login/register/refresh
- Cookie scoped to /api/auth path, secure in production
- Refresh/logout endpoints read from cookie (with body fallback for compat)
- Frontend no longer stores refreshToken in localStorage
- Auth store simplified: removed refreshToken from state + persistence
- API interceptor uses withCredentials:true for automatic cookie sending
- Updated media-api, media-public-api, QuickJoinPage, volunteer-invite
- Renamed getTokens → getAccessToken across all media components
- Install cookie-parser middleware
L2 — FeatureGate loading state:
- Show Skeleton instead of children while settings are loading
- Prevents briefly exposing disabled feature pages
Bunker Admin