Skip to content

Darling custom views/notebooks (#1563) - #1576

Merged
erikdarlingdata merged 6 commits into
devfrom
feature/1563-custom-views
Jul 18, 2026
Merged

erikdarlingdata merged 6 commits into
devfrom
feature/1563-custom-views

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

Custom views / notebooks for the Darling web dashboard (#1562): compose your own dashboards from the read catalog — pick a read, choose a visualization, set its parameters, and save a named, shareable, versioned view that every seat can open and export. No query engine, no new SQL: a view is stored JSON ({panels:[<renderPanel descriptor>]}) driven through the SAME renderPanel seam the built-in pages already use.

Built by two builders on one branch.

Backend (store + grant + loopback-gated CRUD + catalog)

  • Store — migration V31: config.custom_views (name-unique, definition jsonb, an in-place version column for optimistic concurrency, UTC timestamps, updated_by). A NARROW GRANT INSERT, UPDATE, DELETE ON config.custom_views to the least-privilege VIEWER role (mirroring the mcp role's exactly-two-table precedent — deliberately NOT an admin pool on the network-reachable web host, and the table holds no secrets so it opens no config pivot); tools/provision-roles.sql carries the grant for BYO stores (re-run after V31).
  • CustomViewStore built from the same VIEWER-role pool as DarlingAnalysisService: public-const SQL, bound $N params, definition bound as jsonb, optimistic-concurrency update (WHERE id=$ AND version=$expected, a 0-rowcount disambiguated into NotFound vs stale-Conflict), duplicate-name (23505) -> Conflict.
  • API: GET /api/views (bare-array summaries, no definition), GET /api/views/{id} (full, definition embedded as JSON), POST (201 + Location), PUT (version required), DELETE (204); plus GET /api/catalog (read names taken from the endpoint dispatch table so they can't drift, each enriched with hand-authored param metadata naming the ACTUAL wire query keys + type/required/default) and GET /api/session ({can_edit}). Error bodies are always {"error": ...}, matching the read surface.
  • ValidateDefinition is the authority on every write: each panel's read must be on the dispatch allowlist, its viz in {table,line,stat,bandlist}, span 1 or 2, raw path-mode rejected, 128 KB / 48-panel caps — a malformed dashboard can never be stored.
  • Editing is LOOPBACK-ONLY (Erik-ratified): POST/PUT/DELETE 403 off-loopback, using the EXACT loopback determination the network auth gate uses (so the two can't drift); reads / render / export stay open to every seat. The network-reachable viewer role's writes never go on the wire; a one-line lever can open LAN editing later.

Frontend (renderer + composer + export/import)

  • Routes in app.js: #/views (list), #/view/{id} (renderer), #/view/{id}/edit + #/view/new (composer), with an editor-route poll-clobber guard so the 60s background refresh never rebuilds an in-progress edit (the sidebar still refreshes). New sidebar Views section + dynamic #view-list; "New view" shown only when can_edit.
  • Renderer (js/pages/views.js): tiles a view's stored panels into a responsive .panel-grid through the UNMODIFIED renderPanel (per-panel isolation is free — each panel error-strips its own fetch/throw), pre-checking every panel's read against the cached catalog and viz against the registry so a stale/removed read renders a clean "unknown read" strip instead of an opaque 404.
  • Composer (js/editor.js + pure js/derive.js): a read picker grouped by catalog category; typed parameter inputs from the catalog (int/double -> number, bool -> checkbox, text -> input, server -> a fleet dropdown; a required parameter blocks preview + save until filled); a viz picker; width (span 1|2); title; and a per-viz field sub-editor SEEDED by client-side derivation from a live sample fetch (infers the row set, columns/series/stats, and a format per field from the key name + sample value, reusing the util.js formatter registry) then hand-tunable — all behind a LIVE debounced preview through the real renderPanel. Panels reorder with up/down buttons (a11y, not drag).
  • Export is a runtime Blob download of the view JSON; Import pastes it back (client-validated against the catalog, then re-validated server-side as the authority). Both gate on can_edit, so a network seat sees no New / Edit / Delete / Save / Import affordance — only render + export.
  • Security: all user text (view names, descriptions, parameter values, titles) reaches the DOM through el()/textContent (never innerHTML); a series color is constrained to an <input type=color> (#rrggbb) or the chart palette — never free text — so it can't reach the chart's style-attribute sink.
  • js/views-api.js (CRUD/catalog/session clients on apiGet, catalog+session cached, plus a pure client-side validateDefinition mirroring the server's); util.js gains apiSend (a POST/PUT/DELETE helper sharing apiGet's exact response classifier); css/editor.css + a .panel-grid in app.css. Fully air-gapped (no build step, no off-origin references — the CI self-containment scan auto-covers the new files).

Tests / gates

  • Backend: migration-shape + live (config schema), pure ValidateDefinition (unknown read/viz rejected), catalog Keys == dispatch Keys parity pin, CRUD-contract + version-conflict, loopback decision table, gated live own-scoped round-trip (create -> get -> list -> update -> stale-409 -> dup-409 -> delete-404) + grant proof.
  • Frontend: DarlingWebAssetsTests pins the new entry files; the self-containment scan auto-covers them; the pure derive/validate logic verified out-of-band.
  • node --check on every touched/new JS file; Darling service builds clean in Release; full Darling.Tests green (0 failed / 2442 passed / 140 gated-live skipped).

Closes #1563

🤖 Generated with Claude Code

erikdarlingdata and others added 5 commits July 18, 2026 18:34
…k-gated CRUD + catalog (#1563)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, export/import (#1563)

Builds the #1563 frontend on Builder 1's loopback-gated CRUD + catalog backend:

- Routes in app.js (#/views, #/view/{id}, #/view/{id}/edit, #/view/new) with an
  editor-route poll-clobber guard so the 60s refresh never discards an in-progress
  edit; a sidebar Views section + dynamic #view-list, "New view" only when can_edit.
- Renderer (js/pages/views.js): tiles a view's stored panels into a responsive
  .panel-grid through the UNMODIFIED renderPanel, pre-checking each panel's read
  against the cached catalog and viz against the registry (clean strip, not a 404).
- Composer (js/editor.js + pure js/derive.js): read picker grouped by category,
  typed param inputs from the catalog (server -> fleet dropdown; required blocks
  save/preview), viz + span + title, a vizcfg sub-editor SEEDED by client-side
  derivation from a live sample, a live debounced preview through renderPanel, and
  up/down panel reorder.
- Export = runtime Blob download; Import = paste -> client-validate vs catalog -> POST.
- can_edit gates every edit affordance (New/Edit/Delete/Save/Import); render + export
  stay open to all. XSS-safe (el()/textContent); series color constrained to the
  palette or <input type=color> (#rrggbb), never free text.
- js/views-api.js (CRUD/catalog/session clients + cache + pure validateDefinition);
  util.js gains apiSend (shared classifier with apiGet); css/editor.css + .panel-grid.
- DarlingWebAssetsTests pins the new entry files; self-containment scan auto-covers them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…validate series color server-side (#1563)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ss panels (#1563)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@erikdarlingdata
erikdarlingdata force-pushed the feature/1563-custom-views branch from 73a2e6a to b5ccc5d Compare July 18, 2026 22:35
…-only

Networked operation is the normal mode for the web dashboard, so custom-view
editing is available to any authenticated seat (network: past the token->cookie
+ CIDR gate; loopback: on the host) — not restricted to loopback. Removes the
IsLoopbackRemote 403 gate on POST/PUT/DELETE; /api/session can_edit = true for
any request that cleared the host auth gate (false only on a failed session
probe -> UI shows a reload notice). The narrow single-table viewer grant is
unchanged, and CSRF/rebind stays covered by the always-on Host-header allowlist,
the SameSite=Strict cookie, and the application/json requirement — NOT by a
loopback restriction. IsLoopbackRemote stays (the host's tokenless-loopback
auth arm still uses it). Frontend copy + the read-only notice reworded off the
loopback framing; CHANGELOG entry corrected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant