A faster, safer Whal — performance sweep, private damage photos, and nav polish
The morning's multi-tenant release was just the warm-up. This afternoon a long backlog of performance, security, and polish work landed in one push — the kind of changes you'll feel without being able to point at a single new button.
Dashboards that feel instant
The admin dashboard has been the slowest page in the app for months. That's over. Three things happened at once:
- Shared connection pool. Every dashboard card used to open its own Neon connection; now they share a warm WebSocket pool with a bounded LRU so the third and fourth cards inherit the handshake the first one paid for.
- One query instead of six. The KPI cards that used to each run their own aggregate now read from a single collapsed scan. Pallets, cartons, clients, LAL, occupancy, today's inbound, today's outbound, pending dispatches — one trip to the database.
- Tiered caches. Tenant resolution, warehouse config, and the "which slots exist" lookup are cached with per-key TTLs so a hot dashboard reload doesn't hammer the same rows.
Vercel is also now pinned to syd1 (Sydney), so every page load avoids a cross-ocean round trip. Speed Insights is wired in — you can watch the numbers yourself on the Vercel dashboard.
Damage photos are now private
Before today, photos attached to inbound damage reports and stock-adjustment evidence lived on a public CDN with a guess-resistant URL. That's still leaky — the URL only has to escape once (a log entry, a screenshot, a forwarded email) to become permanently accessible. Driver signatures, shipping documents, customer addresses, bonded-goods labels: all potentially exposed with no revocation path.
New uploads to damage and adjustment folders now land on private storage and are served only through the tenant-scoped proxy routes the admin UI already used. Anyone trying the underlying URL directly gets nothing. Legacy photos already in the system continue to render through the same proxy so nothing breaks.
Product photos stay public on purpose — those are catalogue assets that render on public product pages.
Stricter uploads
- Magic-byte sniffing. The upload endpoint no longer trusts the browser's claim about what type a file is. It reads the first bytes and rejects anything whose content doesn't match its declared MIME, stopping an SVG-disguised-as-PNG from slipping through.
- Per-user upload budget. 30 uploads per minute per user. Well above any realistic receipt-photo burst, well below a runaway script or stolen session.
- No direct deletes. The old delete endpoint accepted any blob URL from any session. It's been removed. Photo cleanup now goes through the server action that owns the row (receipt, adjustment, product) and verifies tenant ownership before touching storage.
- Allow-listed folders. Uploads can only target four known folders. No more path-traversal-style shenanigans.
Navigation that knows where you are
The top bar has been relabelled so every page title matches the sidebar item you clicked to get there — no more "Inbound (FSM)" mystery or blank titles on deep pages. The mobile bottom nav now includes Transfers, Batches, and Alerts under "More" so you're not forced to open the hamburger for a two-tap jump.
Alerts' empty state stopped saying "The warehouse is quiet. ☕" — cute, but a warehouse with zero alerts is probably a warehouse with a broken alert pipeline; the new copy just reads "No alerts match this filter."
Fewer moving parts on the admin surface
- Setup wizard retired. Every capability the wizard exposed is reachable from the regular settings pages, and the wizard's mid-flow state could desync from reality. Gone.
- Stocktakes removed. The feature was half-built and nobody used it. Cycle counts via adjustments cover the same ground.
- Transfer fees are billable. Movements between warehouses can now carry a per-transfer fee that flows through the normal charge/invoice pipeline.
Outbound for Standard warehouses
The outbound orders table has been unified so Standard (non-bonded) warehouses get the same order lifecycle the bonded flow has always had. Excise rates are snapshotted onto the order at allocation time — so a rate change between allocation and dispatch can't quietly shift what the customer pays.
Related vocabulary sweep: labels and empty states that used to say "bonded" in places where the feature works identically for Standard warehouses now use neutral terms (movements, dispatches, stock) so Standard tenants don't wonder whether the feature applies to them. Internal schema names are unchanged; this is a UI-only pass.
What this means for you
Nothing to do. Existing sessions keep working. If you've been dreading the dashboard load time, give it another try — it should feel different. If you work with damage photos, the URL structure has changed (they're served from /api/photos/... under your session) but the UI is identical.