Per t-projax-6-sliceB-readpath. mBrian migration (m/mBrian#73) is live
on msupabase with 65 nodes + 78 child_of + 81 projax-* edges. This
commit makes the projax read path source from there behind an env
switch.
CLIENT ARCH: direct pgxpool against mbrian.* schema (same
SUPABASE_DATABASE_URL the projax binary already uses for projax.*) —
matches flexsiebels/head's cross-coupling pattern. No MCP token
plumbing.
CONTRACT (all three honoured)
- External links are SELF-EDGES (source=target=item, rel='projax-*',
payload in edges.metadata). linkFromEdge reads the node's outbound
projax-* edges; ref_id derived per ref_type from metadata (caldav
url, gitea owner/repo, mai-project mai_project_id).
- Slugs finalised: 'work'/'dania' resolve to mBrian's canonical nodes;
projax-side squatters (renamed-aside, not deleted) are documented in
the parity test as legacy-only and skipped from field comparison.
- created_at/updated_at NOT preserved — ItemsCreatedInRange orders off
metadata.projax.start_time when present, fall back to mBrian
created_at. Aggregator surfaces (timeline / dashboard) read off
caldav DTSTART + gitea updated_at, so they're unaffected.
NEW FILES
- store/mbrian.go: MBrianReader concrete impl. Bulk-loads projax-
managed nodes + child_of edges in one pair of queries per call,
builds a graphContext in memory, derives Paths via ancestor walk
(depth-capped at 64 like projax's trigger). Implements every
ItemReader method.
- store/mbrian_parity_test.go: 5 parity tests against the live db —
ListAll field equality (skipping the renamed squatter slugs),
spot-check resolves, caldav-list link round-trip, gitea-repo link
round-trip, AllTags union, NotFound consistency. All 5 GREEN.
- cmd/projax-remap-views/main.go: one-shot tool to rewrite
projax.views.filter_json.project_id from old projax uuids to new
mBrian uuids using the audit map mBrian dropped (head will relay
the path). Dry-run default; --apply commits. Idempotent.
- docs/plans/slice-b-views-projectid-gap.md: surfaces the gap + the
remediation path. Must run remap BEFORE slice E drops projax.items.
CHANGES
- store/adapter.go: kept the ItemReader interface + *Store assertion;
removed the prep stub (replaced by mbrian.go).
- web/server.go: Server.Items store.ItemReader field. web.New defaults
Items to the concrete *Store (legacy path). main.go overrides to
MBrianReader when PROJAX_BACKEND=mbrian.
- All read-path call sites in web/ swapped from s.Store.<readMethod>(
to s.Items.<readMethod>( for the 15 ItemReader methods. MCP tools
unchanged (separate scope; can pivot in a follow-up). Writes still
flow through s.Store.
- cmd/projax/main.go: PROJAX_BACKEND env switch with "store" (default)
and "mbrian" values. Logs the choice at startup. Unknown value
refuses to start.
SMOKE
- go build ./... green; go vet green.
- go test ./store/ -count=1 — all parity tests pass against live data.
- Local server boot with PROJAX_BACKEND=mbrian — backs binding logs
"backend=mbrian (read path via store.MBrianReader)" and serves
/views/tree (auth wall protects deeper smoke; parity tests cover
that surface).
PRE-EXISTING failure NOT addressed in this commit: 3 timeline_filter
tests in web/ already failed on main (legacy /timeline URL hits the
Phase 5j 301 redirect to /views/timeline). No diff vs main in those
test files; out of scope for slice B.
OUT OF SCOPE FOR SLICE B (deferred):
- MCP read tools migration to ItemReader (separate diff, low risk).
- Aggregator's LinkLister wired to ItemReader (currently consumes
*Store directly through Server.Aggregator()).
- views.filter_json.project_id remap RUN — tool ships here, run waits
on the head's relay of the audit-map path.
- Slice C write-path. Slice D mai-bridge worker. Slice E drop.
## Slice A — explicit dark/light toggle
projax now ships with two palettes and a 1y cookie to remember the choice.
Dark is the new default; ☀ button in the header nav flips to light and
writes projax_theme=light. Server reads the cookie via themeFromRequest(r)
and injects Theme + ThemeColor into every template via the centralised
render(w, r, …) path, so first paint never flashes the wrong theme. Inline
JS in layout.tmpl handles the toggle without a server roundtrip.
Every panel colour now lives in a CSS variable under
:root[data-theme=dark|light]; the only hardcoded hex values left are
inside those two :root blocks. A future palette tweak is one edit, not
30 selectors. Graph node colours, kind-badges, highlights and warn/ok/bad
all have parallel dark/light values picked for contrast.
Standalone SVG download bakes the light palette inline because the
downloaded asset has no parent :root providing vars — m's existing
snapshots stay print-friendly regardless of his current cookie.
Login page keeps its embedded dark CSS — it's the gateway, intentionally
always dark.
Tests: TestThemeDefaultIsDark, TestThemeCookieRoundTrips,
TestThemeCookieUnknownFallsBackToDark, TestThemeTogglePagesShareSameTheme,
TestThemeToggleScriptPresent, TestThemeColorMetaHelper. Full suite green.
## Slice B — file-upload permanently out of scope (m, 2026-05-17)
docs/design.md moves "File uploads / in-projax storage" from the §3c
parked list to a permanent "Out of scope (decided 2026-05-17)" clause
with the rationale: PER is the cross-reference index, not the file
system. docs/standards/per.md gains the same explicit clause so future
shifts working from the PER standard see the constraint where they
look. Memory note filed so future workers don't re-propose multipart
uploads, attachments tables, or documents buckets.
## docs/design.md §13 Theming
Documents the toggle approach, cookie semantics, palette table, the
standalone-SVG carve-out, the login-page exception, and the 4b
out-of-scope (prefers-color-scheme detection, per-page overrides,
transitions on swap).