Skip to content

Commit 09b9329

Browse files
authored
Dashboard progressive rendering (#57)
Approved by Venkat
1 parent 5e08481 commit 09b9329

33 files changed

Lines changed: 3231 additions & 236 deletions

CLAUDE.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,12 @@ Dashboards are **generated by the embedded DeepSQL Agent acting as a coding agen
141141
- **Rendering + data access**: `DashboardArtifact.jsx` renders the HTML in a **sandboxed iframe** (`sandbox="allow-scripts"`, opaque origin + a strict CSP — no external network). The artifact fetches data only through an injected `deepsql.query(sql)` bridge that `postMessage`s to the parent; the parent calls **`POST /api/dashboards/query`** (`DashboardQueryController`), which is **read-only twice over** (`McpSqlGuardService.validateReadOnlySql` + `QueryExecutionContext.api` = `READ_ONLY_ONLY`) and access-scoped via `assertCanReadConnectionContent`. So the agent's code has full creative freedom while every query stays guarded and sandboxed. The bridge also auto-sizes the iframe and forwards runtime errors.
142142
- Generation endpoints unchanged (`POST /api/dashboards/generate` + `/generate/stream`). `DashboardBuilder.js`/`DashboardInputs.js` remain only because `tabs/Core/PreviewTab.js` still uses them — the dashboard *creation* path no longer touches them.
143143
- **Sharing**: both share types render a standalone read-only `DashboardViewer` (title + `DashboardArtifact` with an injected `queryFn`). Internal link `/dashboard-view/:id` (auth) uses the authed broker; public link `/share/dashboard/:token` (permitAll) uses `PublicDashboardController` (`GET /api/public/dashboards/{token}` + `/query`), which resolves only while `saved_dashboards.is_public` is true (revoke = flip it) and runs read-only + connection-scoped. `share_token`/`is_public` are set only via `POST|DELETE /api/saved-dashboards/{id}/share` (access-checked), never a general update. `ShareMenu.jsx` drives the UI. The public query path has its own nginx `dashq` limiter.
144+
- **Organization** (search/folders/favorites): `SavedDashboardController`'s search/folder/favorite endpoints existed for a while with no UI consumer. `DashboardsHome.jsx` now wires all of it — a search box (client-side filter over name/description), folder chips derived from `GET /connection/{id}/folders` with a per-card "move to folder" popover (`PUT /saved-dashboards/{id}` with `folder: ""` to clear — `updateDashboard` treats `null` as "field omitted" so blank is the explicit clear signal, same convention as `setSharePassword`), and a favorite star toggle (`POST /{id}/favorite`) with optimistic UI update.
145+
- **Clone**: `POST /saved-dashboards/{id}/clone` (`SavedDashboardService.cloneDashboard`) duplicates a dashboard's config/chat/tags/folder into a fresh row — not shared, not favorited. Exposed as a copy icon on each `DashboardsHome.jsx` card.
146+
- **Version history**: every real overwrite of `dashboardConfig` (agent build via `completeBuildTurn`, manual Source-tab edit via `updateDashboard`, or a restore) snapshots the *previous* config into `dashboard_versions` (`V113__create_dashboard_versions.sql`) before overwriting, tagged with a trigger (`AGENT_BUILD`/`MANUAL_EDIT`/`RESTORE`) — capped at 50 snapshots per dashboard, oldest pruned first. `GET /{id}/versions` lists them newest-first; `POST /{id}/versions/{versionId}/restore` swaps a snapshot back in as current (itself snapshotting whatever was live, so a restore is undoable too) and **dedupes**: after a restore, the restored row plus any other row with byte-identical `dashboard_config` are deleted, since that content is now "Current," not history — otherwise a restore-edit-restore cycle piles up an alternating chain of duplicate snapshots. `DashboardWorkspace.jsx`'s History panel shows a lightweight diff summary per entry (title/widget-count/size delta computed client-side, not a real line diff — the agent rewrites large chunks even for small logical changes) plus a Preview modal that renders that version's HTML live via `DashboardArtifact`.
147+
- **Refresh**: `DashboardArtifact`'s `useImperativeHandle` exposes `reload()`, which bumps an internal `reloadEpoch` state used as the `<iframe>`'s `key` — forcing a genuine remount (and re-running every widget's `deepsql.query()` call) even when `html` is referentially unchanged, which changing `html`/`srcDoc` alone can't guarantee. `DashboardWorkspace.jsx`'s canvas toolbar has a manual Refresh button plus an auto-refresh interval dropdown (Off/30s/5m/1h) that calls it on a timer, paused while a build is in flight (a completing build already replaces the iframe). `DashboardViewer.jsx` (both share surfaces) takes the same `autoRefreshMs` optionally, plus `hideChrome` for kiosk mode.
148+
- **TV/kiosk mode**: `PublicDashboardPage.jsx` reads `?kiosk=1&refresh=<seconds>` (chrome-less + auto-refresh, floor 10s) and `?tokens=tokA,tokB&advance=<seconds>` (cycles through multiple public share tokens, dwelling `advance` seconds each — the route's own `:token` is always the first slide). A password-protected dashboard mid-cycle is skipped (there's no one there to type a password) rather than parking the whole kiosk on a gate. `ShareMenu.jsx` surfaces a ready-made kiosk link (`?kiosk=1&refresh=60`) once a dashboard is public and unprotected.
149+
- **Alerts**: `dashboard_alerts` (`V114__create_dashboard_alerts.sql`) holds a natural-language condition per dashboard (e.g. "alert if the error rate exceeds 5% in the last hour"), evaluated on a schedule by `DashboardAlertService.evaluate()` — a **bounded agent session** (fresh `ensureSession`, no tools beyond `execute_sql`/schema lookups, a short task prompt asking for exactly `YES`/`NO` + a one-sentence reason grounded in a real query result) reusing the same agent plumbing as dashboard generation, just for a one-line answer instead of a whole HTML document. `DashboardAlertTaskConfig` registers one db-scheduler recurring task (`dashboard-alert-tick`, every minute) that evaluates whichever alerts are actually due per `DashboardAlertRepository.findDue` (each alert has its own `checkIntervalMinutes`) rather than one scheduled task per alert. A fired alert dispatches through `EmailService.sendDashboardAlert`/`WebhookService.sendDashboardAlert` (new methods, same pattern as the existing growth/slow-query alert methods) gated by a per-alert `cooldownMinutes` so a condition that stays true doesn't re-fire every tick. The alert runs **as whoever created it** (`createdByUsername`, captured at creation time) — there's no ambient "system" identity for a background job, and running every alert as an arbitrary admin would let one user's alert read data through someone else's access grant. `DashboardAlertController` is the CRUD surface (`/saved-dashboards/{id}/alerts`); `DashboardWorkspace.jsx`'s toolbar has an Alerts panel (composer + per-alert enable/disable/delete, last-check verdict shown inline).
144150

145151
## LLM Providers
146152

0 commit comments

Comments
 (0)