Dashboard improvements - #51
Conversation
A plain "hi" in the dashboard builder chat unconditionally emitted the
full build-pipeline trace ("Handing off to the DeepSQL agent…" →
"Agent is grounding, writing SQL, and coding the dashboard…"), because
DashboardAgentService emitted the first step before classifying intent.
Move the chat-only classification ahead of any step emission so a
chat-only turn shows nothing but the generic "Working" spinner; the
real build steps still fire for actual dashboard requests. Also drop
the frontend's placeholder trace line ("Consulting the brain…") shown
while no steps have arrived yet, since it's the same kind of
misleading progress text for a turn that isn't building anything.
The documented "admin/admin" dev login doesn't exist: AuthController requires a real User row matched by email (not username), and SECURITY_AUTH_ENABLED=false only bypasses JWT/MCP token validation, not the login form. A fresh database has no admin account at all. Document the actual bootstrap flow (POST /users/admin/bootstrap with ADMIN_BOOTSTRAP_SECRET) instead.
…t-side A dashboard generation used to be saved only by the FRONTEND, on receiving the SSE done/chat event — so a closed or reloaded tab discarded a turn the backend had already finished computing, even though the agent turn itself (a detached virtual thread) already kept running to completion regardless of client disconnection. Move persistence into the backend code path itself: SavedDashboard gains a generationStatus (IDLE/RUNNING) and generationStartedAt, and DashboardGenerationController now calls into new SavedDashboardService methods (beginGenerationTurn/appendAgentReply/completeBuildTurn/ appendErrorReply) at the same points it already branches on the outcome — recording the user's message and flipping to RUNNING before the slow agent work starts, and persisting the result when it finishes, independent of whether the SSE client is still connected. A new `created` SSE event fires immediately so even a brand-new, never-saved dashboard is durably addressable within one fast round-trip, well before a build finishes. Guards a real generation failure from being confused with the client simply being gone by the time the final SSE send is attempted (that must never turn an already-persisted success into a recorded error), rejects a dashboardId that doesn't belong to the request's connectionId, and treats a RUNNING status stale beyond 20 minutes as abandoned so a crashed backend can't block a legitimate retry forever.
…Source view Generation state (chat, streaming steps, the built config) moves out of DashboardWorkspace's component-local useState into a new useDashboardChatStore, keyed by dashboard id (or new:<connectionId> before the first save, with an alias/rekey scheme for the transition). Navigating away no longer aborts the in-flight SSE stream, and on mount, if the persisted dashboard's generationStatus is RUNNING (a turn was in flight when this tab wasn't around, per the paired backend commit), the store polls until it resolves instead of assuming nothing is happening. Also adds: - A Source/Preview toggle with an uncontrolled Monaco HTML editor (defaultValue + remount key, not a controlled value — a controlled value rewrites Monaco's model on every keystroke, resetting the caret mid-word). Apply re-derives the dashboard's title from the edited <title>/<h1> so a manual rename actually propagates to the breadcrumb/gallery, and is disabled while a generation is in flight to avoid racing the backend's own write of the same config. - A Queries panel: DashboardArtifact now reports every query the artifact runs (SQL, row count or error, timing) via a new onQuery prop, wired to a side panel with copy-to-clipboard. dashboardQueryAPI.run now surfaces the backend's actual error message instead of axios's generic "Request failed with status code 400" (the interceptor only reads response.data.message; this endpoint's payload uses `error`). - generateStream's fetch is aborted proactively on the page's `pagehide` event and the resulting failure is dropped rather than surfaced as a chat-history error: a reload/close tears down the request as a bare TypeError, indistinguishable in shape from a dead backend, which would otherwise get permanently written into the saved chat on every reopen.
|
You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool. What Enabling Code Scanning Means:
For more information about GitHub Code Scanning, check out the documentation. |
…racter sanitization' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
ReviewSolid product cut — server-owned turn persistence + resumable chat is the right fix for “built but lost on reload,” and the Source/Queries UX is clean. CI required checks look good (frontend/backend/mcp green). Approve directionally — please address before merge
Nits (non-blocking)
What looks good
Verdict: request the three items above, then this is merge-ready for main. |
There was a problem hiding this comment.
@notSumit25 Nice work — the comments explain why rather than what, the migration is idempotent and explicitly acknowledges that schema here is Hibernate-managed, and DashboardArtifact.jsx adds the query telemetry without touching sandbox, the CSP, or the bridge's trust boundary.
Two things that are easy to get wrong and are right here:
- Existing rows are safe.
generationStatushas a Java default, a@PrePersistnull guard, andisFreshlyRunning()is null-safe on both fields — so rows predating the column neither NPE nor falsely read as RUNNING. // Persist BEFORE attempting to notify the clientis the correct ordering. A client that has already disconnected can't turn an successful result into a recorded failure.
Findings below, most important first.
1. Check-then-act race in beginGenerationTurn — medium
if (isFreshlyRunning(dashboard)) throw new IllegalStateException(...);
...
dashboard.setGenerationStatus("RUNNING");
return savedDashboardRepository.save(dashboard);The read and the write aren't atomic, and SavedDashboard has no @Version. Two submits against the same dashboard — a double-click, or two open tabs — both read IDLE and both proceed. That's two concurrent agent runs, each costing LLM spend, racing to write the artifact. The guard reads like a mutex but isn't one.
2. Lost update on chatMessages — medium, same root cause
parseMessages → add → writeMessages is a read-modify-write of a whole JSON array in a text column. Under the race above, one turn's user message and reply are silently overwritten by the other.
Both 1 and 2 close with a single @Version field on SavedDashboard — the losing transaction gets an OptimisticLockException instead of silently clobbering.
3. A crash locks the dashboard for 20 minutes — low/medium
STALE_RUNNING_THRESHOLD is the right instinct, but the failure surfaces as a hard IllegalStateException: "A generation is already running for this dashboard." After a backend restart mid-generation the user can't retry for up to 20 minutes, and the message asserts something is running when nothing is. Worth either a shorter window or an explicit override.
Same shape as the db-scheduler dead-execution gap in #39 — a staleness window longer than a user's patience, where "stuck" is indistinguishable from "working".
4. A read permission now gates a write — a decision, not a blocker
assertCanReadConnectionContent previously guarded a path that only computed. This PR makes that same path create rows and mutate chat history, so a user with read-only access to a connection can now create dashboards on it.
Worth noting this cuts the right way overall: POST /api/saved-dashboards — the path this replaces — has no access check at all, so this PR is strictly stricter than what it supersedes. Filing that separately.
Happy to see this land once @Version is in; everything else is follow-up material.
Adds @Version to SavedDashboard so concurrent chat turns / favorite / share / update writes on the same row fail cleanly (409) instead of racing or leaking a raw Hibernate error message. Also tightens generate/stream to require manage (not just read) access, since it creates/mutates saved dashboards. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Merge attemptPR is approved (
Blocked from merging by repo ruleset: required status check is literally named
(unexpanded matrix expression). Real CodeQL jobs are To unblock (repo admin)Rules → Protect main branch → Required status checks — replace
with:
Then squash-merge #51 (or I can retry the merge once that’s fixed). Follow-up after merge: don’t create a gallery row for chat-only turns when |
<!-- CURSOR_AGENT_PR_BODY_BEGIN --> ## Summary Prepares and documents the **v1.1.0** product cut from `main` since `v1.0.0`, and locks in a **weekly release cadence: Saturday 09:00 America/Los_Angeles**. ### Since v1.0.0 - Progressive dashboards (#57), dashboard improvements (#51) - Multi-schema UI (#55), Performance hub (#52) - CI CodeQL unblock (#56), cloud env caveats (#53) ### This PR - Bump `backend/pom.xml` → `1.1.0` - `CHANGELOG.md` + `docs/releases/RELEASE_NOTES-v1.1.0.md` - Cadence in `docs/oss-ux/RELEASE.md` - New `docs/oss-ux/WEEKLY_RELEASE_AUTOMATION.md` (cron + paste-ready prompt) - Daily triage doc clarified as optional (not the release cut) ### After merge 1. Tag `v1.1.0` on the merge commit and push → `.github/workflows/release.yml` publishes the GitHub Release. 2. Create the Cursor Automation once from `WEEKLY_RELEASE_AUTOMATION.md` (cannot be created via API). ### Pre-flight `scripts/self-host/e2e-agent-check.py` → `AGENT_OK True`, `DASH_OK True` on the current stack before this bump. <!-- CURSOR_AGENT_PR_BODY_END --> <div><a href="https://cursor.com/agents/bc-8ce91e70-c67b-48c6-84b3-05bb9d06231a?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a> <a href="https://cursor.com/background-agent?bcId=bc-8ce91e70-c67b-48c6-84b3-05bb9d06231a&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a> </div> Co-authored-by: Cursor Agent <cursoragent@cursor.com>
No description provided.