Cozier dev tool: hero panels, a real data browser, and profiling - #252
Merged
Conversation
Condense the 20-tab bottom strip into 4 hero panels (Data, Changes, Logs, Performance) with the rest grouped in a 'More' menu + a ⌘⇧P command palette. - Typed panel registry (tier/group/icon/keywords) replaces the flat array; persisted-panel validation now derives from the registry (no stale allowlist) and migrates the old 'nodes' id -> 'data'. Panel height now persists too. - Data panel: rebuilt on the real database grid (GridSurface, read-only) + store.query() with a query-plan inspector, schema-aware columns, live store.subscribe updates (no more 2s poll), and a row detail pane. - Logs panel (new): toggle the five debug channels + a searchable console capture ring buffer. - Performance panel (new): boot-timeline waterfall (from perf marks), live FPS/heap, storage stats, active queries, and recent traces. - PanelErrorBoundary guards the data grid against malformed node data. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The read-only data grid crashed ("str.replace is not a function") when a
node property whose value wasn't a string landed in a text-family cell —
text/select/date/etc. renderers call string methods. Coerce each cell to its
column's FieldType: numbers->number, checkbox->boolean, multiSelect->string[],
everything else stringified. Caught live by the PanelErrorBoundary fallback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t, log tags)
From a 10-agent adversarial review of the diff:
- Data grid: coerce cells to each renderer's native shape — date/created/updated
keep an epoch number (was "Invalid Date"), dateRange a {start,end} object,
relation/person/multiSelect an array, file a FileRef; only text-family types
are stringified. Fixes broken rendering of common schema column types.
- Data panel: land on a terminal empty state when the store is null instead of a
perpetual "Loading nodes…" spinner.
- Data query: request count:'exact' so the "X / Y rows" total is populated under
storage pushdown ('estimate' left it undefined).
- Logs: classifyChannel now buckets the real emitters correctly (the SQLite
adapter's 'query plan' line -> Query; '[WSSyncProvider]' -> Sync).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The schema registry exposes each schema under both a versioned IRI (.../Task@1.0.0) and a bare alias (.../Task); schemaLabel strips the version, so every schema showed up twice in the picker (154 entries). Group options by base IRI and emit one per schema — preferring the versioned IRI to query with, and only splitting out genuinely-distinct versions (with a version suffix). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
|
Preview removed for PR #252. |
| } from './grid-adapter' | ||
| import { useDataExplorer } from './useDataExplorer' | ||
|
|
||
| export function DataExplorer() { |
| return type as FieldType | ||
| } | ||
|
|
||
| function optionsFor(prop: PropertyDefinition): GridFieldOption[] | undefined { |
| 'general' | ||
| ] | ||
|
|
||
| export function LogsPanel() { |
| setLogs([]) | ||
| }, []) | ||
|
|
||
| const filtered = logs.filter((entry) => { |
| ) | ||
| } | ||
|
|
||
| function StorageSection() { |
| ) | ||
| } | ||
|
|
||
| function PlanInspector({ plan }: { plan: PlanMeta }) { |
| } | ||
| }, [selectedSchema]) | ||
|
|
||
| const runQuery = useCallback(async () => { |
| ) | ||
| } | ||
|
|
||
| function ActiveQueryRow({ query }: { query: ActiveQuery }) { |
| ] | ||
|
|
||
| /** Read the boot marks back off the Performance timeline. */ | ||
| export function readBootMarks(): BootMarks { |
|
|
||
| // ⌘/Ctrl+Shift+P opens the panel palette (only while devtools is mounted). | ||
| useEffect(() => { | ||
| const handler = (e: KeyboardEvent) => { |
Add an opt-in 'edit' toggle to the Data panel (shown only when a specific schema is selected). When on, inline-editable property columns (text/number/ checkbox/select/multiSelect/date/dateRange/url/email/phone) become editable and write back via store.update; system columns, computed/auto types, relation/ person/file, and option-less selects stay locked. Edits coerce to the property type (date accepts ISO strings) and failures surface inline. The 'All schemas' view stays read-only (synthesized columns). - GridSurface now honors a column's structural `readonly` flag (it was set by schema-to-grid-fields but never enforced) — both the keyboard and double-click edit paths skip locked columns, including checkbox toggles. - Schema resolution is now robust to the registry's versioned-vs-bare keying (try the selected IRI and its base, sync then lazy get()), so editing activates for built-in schemas. Verified live: edited a Database title in-grid and it persisted via store.update + live re-query. 153 views grid tests + devtools suite green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
🖼️ UI changes in this PRNo visual differences detected in the changed UI. |
| return | ||
| } | ||
| const candidates = Array.from(new Set([selectedSchema, baseSchemaIri(selectedSchema)])) | ||
| void (async () => { |
| // Write a single edited cell back to the store. The live subscribe above | ||
| // refreshes the grid; system columns (@@…) are never editable. | ||
| const updateCell = useCallback( | ||
| async (rowId: string, fieldId: string, fieldType: FieldType, value: CellValue) => { |
crs48
added a commit
that referenced
this pull request
Jun 25, 2026
## What The dev tool's **Data** panel (Ctrl/Cmd+Shift+D) now reflects your real authorization per cell. When editing is on, cells you're not allowed to write are **locked** instead of failing on save, and a row's detail pane shows exactly how your read/write permissions are derived. Implements exploration **0219**. ## Why Inline editing (added in #252) let you double-click any cell and call `store.update` — but the grid had no idea what authorization said. Editing a cell you couldn't write surfaced an avoidable `PermissionError` only *after* you tried to save. This wires the existing `store.auth` policy engine into the grid so the UI tells the truth up front. ## How - **`@xnetjs/views` — `GridSurface`/`GridCell`:** new optional `cellLockReasons?: ReadonlyMap<string, string>` (key `rowId:fieldId` → human reason). A shared `isCellLocked` predicate guards **every** write path — edit-start, double-click, paste, fill-down, cut/clear (`refsInRect`), and file-drop — not just the editor. Locked cells render a subtle `Lock` glyph with the reason as the cell `title`. Default-absent ⇒ unchanged behavior, so `DatabaseView` in the app is unaffected. - **`@xnetjs/devtools` — `useCellPermissions`:** lazy, edit-mode-only per-node `write` decisions over the visible window (guarded on `store.auth`, optimistic until resolved). Locks a cell when the node isn't writable *or* a restricted `fieldRule` denies the field. Pure `deriveCellLocks` extracted for testing. - **`AuthTraceView`** extracted from `AuthZPanel` and reused in a `NodePermissions` section of the node detail pane (`store.auth.explain` for read + write → roles, grants, reasons, steps). - **No-authz fallback:** stores without an `authEvaluator` keep type-based editing and show a "permissions not enforced in this store" note — no spurious locks. - **Authz cache fix:** `DefaultPolicyEvaluator.can` no longer reads/writes the decision cache when a `patch` is present. The cache is keyed only by subject/action/node, so a prior node-level write check could otherwise mask a field rule for the rest of the TTL — silently bypassing field-level restrictions. ## Tests - `GridSurface`: a locked cell blocks Enter + double-click editing (shows title); sibling row still edits. - `deriveCellLocks` / `restrictedFieldNames`: node-deny locks all editable fields, writable unlocked, field-rule lock, optimistic-undecided. - `evaluator`: regression — a cached node-level decision no longer masks a field rule. Verified live in the browser preview (the dev store has no `authEvaluator`, so the preview exercises the graceful "not enforced" fallback; the enforced lock path is covered by the unit tests above). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements
docs/explorations/0217. The in-app dev tool (@xnetjs/devtools, Ctrl/Cmd+Shift+D) went from a 20-tab horizontally-scrolling strip to a cozy, high-signal surface — and the genuinely useful panels got rebuilt on infrastructure that already existed.What changed
Information architecture — four hero panels (Data · Changes · Logs · Performance) in a compact primary row; everything else tucked into a grouped More ▾ menu and a ⌘⇧P command palette (fuzzy by label + keywords). Driven by a typed panel registry (
{ id, label, icon, group, tier, keywords }) instead of a flat array + 18-case switch. Persisted state now includes panel height (was lost on reload) and the persisted-panel validation derives from the registry — no more stale allowlist — and migrates the oldnodesid →data.Data panel (rebuilt) — the old NodeExplorer polled
store.list()every 2s and synthesized a fake text-only schema. It's now the real database grid (GridSurface, read-only) driven bystore.query(): schema picker, schema-aware typed columns, livestore.subscribeupdates (no polling), a query-plan inspector (strategy / indexes / duration / SQL), a row detail pane, and aPanelErrorBoundaryso malformed node data can never take down the surface.Logs panel (new) — toggle the five debug channels (
sync/sqlite/query/boot/trace) that were previously console-onlylocalStorageflags, plus a searchable, level/channel-filterable console capture.Performance panel (new) — unifies signals that were scattered across four places: cold-start boot-timeline waterfall (read from
performancemarks), live FPS + JS heap, storage stats (nodes / lamport / OPFS quota), active queries, and recent slow traces.Quality
domproject), repo typecheck green,pnpm --filter xnet-web buildgreen (prod still tree-shakes@xnetjs/devtoolsto the no-op — zero bytes shipped to users).count:'estimate'not populating the row total, and mis-tagged log channels) — all fixed with regression tests.🤖 Generated with Claude Code