feat: v0.17.0 — ISS-008 toasts for errors/events + browser notifications
CI / build + unit + e2e (pull_request) Failing after 6m59s
CI / build + unit + e2e (pull_request) Failing after 6m59s
This commit is contained in:
@@ -1,5 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## v0.17.0 — 2026-09-19 14:49
|
||||
- **Bottom-right toasts for action errors (ISS-008).** Error messages like "insufficient funds: a ATR 72-600 costs 200000, you have 62721" now appear as toasts in the bottom-right corner — new `lib/useToasts.ts` hook (`useToasts() → {toasts, push(kind, msg), dismiss(id)}`, auto-dismiss after ~5 s, queue capped at 4 dropping the oldest; pure core `pushToast` unit-tested) and new `components/Toasts.tsx` `<ToastStack>` (fixed container `.toasts`, items `[data-testid="toast"]` with class `toast toast-error|toast-info` + a × dismiss button). Wired into BOTH error paths: `RightPane.runAction`'s catch now calls the shell's `onError` in addition to keeping the inline `[data-testid="right-error"]` pane error, and `LeftPane.applySimControls`'s catch (previously console.error-only) reports "Failed to update time controls (…)" through the same callback. The shell (`app/page.tsx`) owns the hook and renders the stack; CSS in `app/globals.css`
|
||||
- **Event messages via snapshot diffing.** No server event feed exists, so new pure `lib/deriveEvents.ts` — `diffSnapshots(prev: StateSnapshot | null, next) → {kind:'info', message}[]` — derives human-readable events from the fields already in `/api/state`: plane landing (`flying→available`, "ATR 72-600 landed at Frankfurt"), package delivery (`loaded→delivered`, "Package delivered: +€X" with the reward), expiry (`open|loaded→expired`, "Package expired: A → B"), new packages (id only in next, "New package available: A → B") and planes bought/sold (plane id set changed, "Bought/Sold {model}"). The shell keeps a `prevRef` and diffs on every snapshot change, pushing each event as an info toast; the first snapshot after load emits nothing (no baseline — no page-load spam)
|
||||
- **Browser notifications when the window is not focused.** Each derived event also fires `new Notification('Flight Ops', {body})` when `document.hidden && 'Notification' in window && Notification.permission === 'granted'`. The left-pane Time section gains an explicit opt-in button (`[data-testid="enable-notifications"]`) calling `Notification.requestPermission()`; its label reflects the result ("Enable notifications" → "Notifications enabled"/"Notifications blocked"), and it renders disabled with a tooltip title when the API is absent. SSR-safe: permission state starts null and is read in an effect after hydration
|
||||
- No server or OpenAPI changes — diffing uses only existing snapshot fields (verified: planes by id/status/locationName, packages by id/status/reward/origin+dest names)
|
||||
- Tests: unit — new `lib/deriveEvents.test.ts` suite pins every event format (landing, delivery with reward, expiry from open and loaded, new-package appearance, bought/sold, identical snapshots → [], first snapshot → [], non-events like takeoff/load ignored); `lib/useToasts.test.ts` covers the queue cap. e2e — new `e2e/toasts.spec.ts`: deterministic insufficient-funds path (select the most expensive model in `[data-testid="market-buy-model"]`, click Buy, expect a visible error toast matching /insufficient funds/ AND the inline `right-error` still showing; skips only if cash already covers the plane on a long-lived local instance) and the notifications opt-in (unsupported → disabled + title; otherwise fresh context shows "Enable notifications" and after clicking the label reflects the result — soft, since headless CI may deny permission)
|
||||
- Verified: `tsc --noEmit` clean; unit suite green locally; e2e not run locally by design — CI verifies against an isolated stack
|
||||
|
||||
## v0.16.1 — 2026-09-19 14:00
|
||||
- **Smooth client-side sim clock with per-snapshot re-sync (ISS-007).** The left-pane "Time" display used to jump once per 1 s poll because it rendered the snapshot's `clockMs` verbatim. It now ticks smoothly on the client between polls: new hook `lib/useSimClock.ts` keeps an anchor `{clockMs, at, timeScale, paused}` and re-derives the display clock every ~100 ms as `anchor.clockMs + (now − at) × timeScale` — exactly the server's own tick model (`tickOnce`: real-time × timeScale), so client extrapolation mirrors authoritative game time. Every new snapshot re-anchors to server truth, bounding any drift by network latency only; while paused the display is frozen at the anchor value (advance 0). The pure core `advanceSimClock(anchor, nowMs)` is exported and unit-tested in node
|
||||
- **LeftPane consumes it for the `sim-clock` paragraph ONLY** (`components/LeftPane.tsx`): fed with EFFECTIVE controls `{timeScale: pendingScale ?? snapshot scale, paused}` so a slider drag or pause click takes effect on click/drag without waiting for a confirming snapshot; the `(paused)` suffix keeps coming from the effective paused flag. Right-pane deadline/penalty previews are untouched — they stay computed from the snapshot's `clockMs` (deterministic against server truth)
|
||||
|
||||
Reference in New Issue
Block a user