Skip to content

Commit 9d768bf

Browse files
beetlebugorgclaude
andcommitted
docs(specs): map-first chrome note + profiles/server-state proposal
- web-architecture: update the shell chrome description (corner buttons, bottom data card) to match the map-first UI. - profiles-and-sync (new): draft design for server-side shared state and profiles, grounded in the local-first single-device deployment model (Pi/laptop/iPad; Go server is the hub, client screens sync). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 9fb13d9 commit 9d768bf

2 files changed

Lines changed: 125 additions & 2 deletions

File tree

specs/profiles-and-sync.md

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# Profiles & server-side shared state (DRAFT proposal)
2+
3+
Status: **proposal for review** — not yet implemented. Captures the direction
4+
from the "profiles + everything server-side + two clients in sync" discussion.
5+
6+
## Vision
7+
8+
**Deployment model:** local-first, single-device. The native Go server runs on
9+
the boat's Raspberry Pi (or a laptop), bakes + serves tiles, and holds all app
10+
state. Client screens (phones/tablets at the helm + nav station) connect over the
11+
boat's LAN. A future iPad app is the same thing packaged self-contained. There is
12+
always a local server — this is not a cloud/CDN service.
13+
14+
Treat the app as a **single shared instrument**: all meaningful state lives on
15+
the server, and any number of clients render from it and stay in sync. The helm
16+
and nav-station screens (or two people aboard) see the same profiles, the same
17+
active selection, and each other's changes live.
18+
19+
Built **single-tenant** now (one boat = one shared state), but every API is
20+
shaped so a later `userId`/`boatId` layer can scope it for multi-user without
21+
reshaping the endpoints.
22+
23+
**Rendering:** one client rendering model (per-band overzooming sources), fed by
24+
either the local server (`/tiles/<set-band>`) or bundled PMTiles (`pmtiles://`,
25+
for the offline/iPad case). The PMTiles path is a tile *source*, not a second
26+
rendering path — see specs note + the coverage-hole fix below.
27+
28+
## Profile
29+
30+
A named bundle that captures "how I sail here":
31+
32+
- **charts** — which packs/sets belong to the profile, and enabled/disabled
33+
within it (e.g. *Chesapeake* = NOAA D5 + a couple of user imports).
34+
- **config** — the S-52 mariner/display settings (depths, units, detail level,
35+
the per-feature toggles), basemap, and default colour scheme.
36+
- **view** — home centre/zoom to jump to when the profile is activated.
37+
- **meta** — name, kind ("coastal cruising", "offshore racing", …), timestamps.
38+
39+
Examples: *Chesapeake cruising* (harbor detail, shallow contour 2 m, OSM
40+
basemap) vs *Offshore delivery* (coarser bands, deep contours, night scheme).
41+
42+
## Server state model (extends `prefs.go`)
43+
44+
Today `prefs.json` holds only `{disabled: {set:bool}}`. Generalise to a single
45+
persisted store + an in-memory broadcast hub:
46+
47+
```
48+
state.json {
49+
activeProfile: "<id>",
50+
profiles: {
51+
"<id>": { name, kind, config{…s52…}, view{center,zoom}, basemap, scheme,
52+
sets: { "<set>": enabled } }
53+
}
54+
}
55+
```
56+
57+
The **download queue** is server-side runtime state (persisted enough to resume):
58+
`queue: [{key, profile, kind, …}]` + the one active job (the existing
59+
`importJobs`).
60+
61+
## API (REST for mutations, SSE for sync)
62+
63+
- `GET /api/state` → full snapshot `{activeProfile, profiles[], queue[], packs[]}`.
64+
- `GET /api/events` (SSE) → server pushes on **any** state change (profile
65+
switched, config edited, queue/job progress, pack installed/removed). This is
66+
the sync backbone — every client subscribes; one client's change reaches the
67+
rest. (We already use SSE for bake job progress — same machinery.)
68+
- Profiles: `POST /api/profiles`, `PATCH /api/profiles/{id}`,
69+
`DELETE /api/profiles/{id}`, `POST /api/profiles/{id}/activate`.
70+
- Config: `PATCH /api/profiles/{id}/config` (a mariner/display/basemap/scheme
71+
patch) — broadcast so other clients restyle live.
72+
- Queue: `POST /api/queue` (enqueue a pack for the active profile),
73+
`DELETE /api/queue/{key}` (cancel/dequeue). Queue + progress arrive via
74+
`/api/state` + `/api/events`.
75+
76+
## Server-side download queue
77+
78+
Move `_dlQueue`/`_activeDownloadKey` out of the browser into the server:
79+
80+
- Client `POST /api/queue` with a pack ref. Server appends, runs one at a time
81+
(reusing `importJobs`), and broadcasts queue + progress on `/api/events`.
82+
- Survives client reload; both clients see the same Download / Downloading… /
83+
Queued states. The notification pill + per-pack buttons render from the
84+
streamed queue state instead of local fields.
85+
- A finished download adds the pack to the **active profile's** `sets`.
86+
87+
## Client
88+
89+
- On boot: `GET /api/state`, then subscribe `/api/events`. Render everything —
90+
active profile, its charts, its config, queue/button states — from server
91+
state. Drop `localStorage` for anything shared (keep only truly client-local
92+
view bits like "which dev panel is open").
93+
- Switch profile → `POST …/activate`; the map swaps to that profile's sets,
94+
applies its config + view. Every client follows.
95+
- Edit a setting → `PATCH …/config`; broadcast; other clients restyle live.
96+
97+
## Multi-user (future, not now)
98+
99+
Add a tenant key (`userId`/`boatId` from auth) over `profiles` + `activeProfile`
100+
and scope `/api/events` per tenant. The single-tenant store becomes a map keyed
101+
by tenant; the REST/SSE shapes are unchanged.
102+
103+
## Phasing
104+
105+
1. **State + SSE backbone** — generalise `prefs` into the state store; add
106+
`GET /api/state` + `GET /api/events` broadcast; migrate pack enable/disable
107+
and the currently-`localStorage` config (mariner/basemap/scheme/view) into
108+
it; client reads/writes via API and subscribes. *Foundational.*
109+
2. **Server-side download queue** — move the queue server-side; client renders
110+
queue/button state from `/api/state` + `/api/events`. *(The feature asked for.)*
111+
3. **Profiles** — CRUD + activate; each profile owns sets + config + view; UI to
112+
create/switch profiles.
113+
4. **Multi-user** — auth + per-tenant scoping. *(Future.)*
114+
115+
## Open / parked (track separately from this work)
116+
117+
- **Missing-holes rendering regression** — could not reproduce in the Annapolis
118+
harbor view (z15.5 renders clean); need a region/zoom where it shows.
119+
- **Picker circle not showing**`_pickReportAt` uses `queryRenderedFeatures`,
120+
so it depends on chart features actually rendering; likely the same root cause
121+
as the holes regression.
122+
- **Zoom remap** ("z3.5 → z0, stretch to z18") — needs a decision on mechanism
123+
(clamp min-zoom vs client zoom-offset vs re-bake the tile pyramid).

specs/web-architecture.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ added without touching the renderer.
77
```
88
┌──────────────────────────────────────────────────────────────┐
99
│ <chart-plotter-app> the shell — composes everything │
10-
│ • chrome: rail, drawer, status bar, scheme toggle
10+
│ • chrome: corner buttons, drawer, bottom data card, scheme │
1111
│ • wires events between the base widget and the components │
1212
│ │
1313
│ ┌───────────────────┐ ┌───────────────┐ ┌──────────────┐ │
@@ -86,7 +86,7 @@ A shared `MapOverlay` base class (`map-overlay.mjs`, planned) will standardize
8686

8787
## Layer 3 — `<chart-plotter-app>` (shell)
8888

89-
Owns the chrome (rail, drawer, status bar, search, modals) and composes the
89+
Owns the chrome (corner round buttons, drawer, bottom data card, search, modals) and composes the
9090
base + components, forwarding events between them. Target: shrink it to
9191
orchestration once the downloader and plugins are extracted.
9292

0 commit comments

Comments
 (0)