v0.4.2
Patch release: un-bricks plain-http self-hosts.
Fixed
- Plain-http self-hosts were stuck on "Loading…". v0.4.1 added
upgrade-insecure-requests+ HSTS to the production CSP unconditionally. Browsers honour the upgrade directive even onhttp://origins, so a LAN install athttp://192.168.x.x:3000had every/_next/staticchunk rewritten tohttps://and failing withERR_SSL_PROTOCOL_ERROR— no JS ran, so the page never hydrated past the SSR "Loading…" fallback. Both directives are now opt-in viaSTAYBATTLE_HTTPS=true(a build-time flag / Docker build arg) for TLS-terminated deployments. Header logic moved tosrc/lib/security-headers.tswith vitest coverage. - Plain-http self-hosts answered "Sign in first" to every action. The voter cookie was
secure: NODE_ENV === "production"; browsers drop Secure cookies on any non-localhost http origin, so athttp://192.168.x.x:3000sign-in stuck client-side but the server never saw a session — "Start the battle", voting, everything refused.Secureis now gated on the sameSTAYBATTLE_HTTPS=trueflag (read at run time; the Dockerfile carries it into the runtime stage). Cookie attributes moved to a testedvoterCookieOptions()helper. - Public demo showed "checking…" on every listing forever.
scripts/admin/reset-demo.mjswiped the wholesettingstable, which dropped theavailability_reset_v1flag thatdb.tschecks at boot — so the next container restart re-ran the one-shot availability wipe and nulled every status the reset had just baked. The reset now re-arms the flag inside its own transaction. Integration test runs the real script against a temp DB. - Demo mode no longer scrapes Airbnb.
addListingwas not demo-gated, so any visitor could make the public demo fetchairbnb.com(README had claimed the scrape was skipped).addListingandrefreshAvailabilitynow refuse up front inSTAYBATTLE_DEMO_MODEwith a message saying why. - Re-check buttons surface refusals. Both
AvailabilityPaneland theBattleHeader"recheck all dates" button ignored the action result, so a refused recheck (demo mode, missing trip dates) either showed "checking 0 of N…" until a timeout or silently did nothing. Both now show the error inline. - "New code" / "Kick" silently did nothing on refusal.
InviteCodePanelignored the action result, so an expired session or a non-organizer cookie made the confirm dialog close with no visible effect. Refusals now render inline. - Demo modal header was a black smear in light mode. Its
bg-gradient-to-br … via-zinc-950gradient wasn't covered by the light-theme surface remap (which can't reach gradient stops). Tagged with the existing.sb-deep-surfacehelper so it swaps like the other dark gradient surfaces.
Added
- Stale battles archive themselves. A battle whose check-out day ended more than 7 days ago is moved to the trophy case on the next page load, exactly as the organizer's "close battle" does (listings, pins, participants, and trip dates cleared). Before, a trip that ended months ago blocked the whole one-battle-per-server instance until its organizer signed in with their PIN. Battles with no dates never go stale.
isStale()insrc/lib/battle.ts(unit-tested) + a sharedcloseCurrentBattle()inbattle-server.tsused by both paths (DB-level test).
Changed
- Demo discloses what is simulated. Sign-in screen gets a "Public demo" hint (anonymous visitors never saw the demo modal), the demo modal explains that badges / votes / comments are seed data, and the footer disclaimer has a demo-mode variant instead of claiming badges were "scraped from Airbnb's public page".
vitest.config.tsaliases theserver-onlymarker package so server-action modules can be imported in tests.