Skip to content

chore(release): consolidate release/1.5 into dev - #1464

Merged
alfredo1996 merged 16 commits into
devfrom
release/1.5
Aug 4, 2026
Merged

chore(release): consolidate release/1.5 into dev#1464
alfredo1996 merged 16 commits into
devfrom
release/1.5

Conversation

@alfredo1996

@alfredo1996 alfredo1996 commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Consolidates release/1.5 into dev, following the same path as #1233 (release/1.3) and #1366 (release/1.4).

This is a release-timing decision, not just a flake fix — v1.5 is still in progress (milestone #31 has 40 open issues). It brings 16 commits of in-flight v1.5 work into dev earlier than a normal end-of-release consolidation would.

Why now

dev fails E2E shard 1 roughly one run in three, on charts.spec.ts graph tests, with expect(locator).not.toBeVisible(). The cause was diagnosed and fixed in #1462 (#1458): the ten Radix overlay primitives never honoured prefers-reduced-motion, and Radix keeps an overlay mounted until its exit animation reports animationend — which stalls when an NVL/WebGL widget mounts alongside the close.

That fix landed on release/1.5, so dev kept the flake. It blocked #1463, a dependency PR with nothing to do with overlays, and a re-run of the failed shard failed again. Every PR targeting dev was paying this toll, and each failure needed a human to confirm it was the known flake rather than a real regression.

What comes with it

fix(component) reduced-motion across all overlays — the flake fix (#1458, #1462)
fix(dashboard) parameter URL sync made opt-in (#1388) — breaking for shared links
feat(dashboard) bulk connector reassignment + import count fix (#1376, #1377)
feat(widget-editor) query editor maximize toggle (#1382)
fix(dashboard) Cmd-E no longer remounts the dashboard (#1370, #1371)
fix(dashboard) narrow-window save no longer squashes the layout (#1375)
fix(charts) Leaflet zoom-transition timer disarmed before teardown (#1384)
fix(charts) graph widgets stay mounted until the WebGL budget is spent (#1381)
fix(component) dialog + AlertDialog centring compensation (#1373, #1380)
fix(cli) neoboard status health probe and container count (#1379)
chore CLAUDE.md moved into .claude/ (#1393)
docs CHANGELOG reconstructed for v1.1.0–v1.4.0 (#1391)

Note #1388 is breaking for existing shared links: a URL carrying ?param_… for a widget that never opted into URL sync will no longer reproduce those values. That is documented in the CHANGELOG entry it shipped with.

Merge check

Merges cleanly into dev — verified locally with git merge --no-commit --no-ff, zero conflicts. .github/workflows/ci.yml and package.json auto-merged.

Follow-on

#1463 (five validated dependency bumps) currently fails E2E shard 1 on this exact flake. Once this lands it should go green on a re-run.

Summary by CodeRabbit

  • New Features

    • Dashboard viewing and editing now preserve pages, widget state, and scroll position when switching modes.
    • Imported dashboards identify unassigned connections and offer bulk or manual reassignment.
    • Added dashboard-specific connection reassignment controls with permission checks.
    • Query editors can be maximized for easier editing of long queries.
    • URL synchronization now supports explicitly selected widget parameters.
    • Large graph dashboards better manage resources while keeping visible content responsive.
  • Bug Fixes

    • Improved responsive dashboard layouts, map cleanup, dialog centering, health reporting, and reduced-motion accessibility.

alfredo1996 and others added 16 commits July 29, 2026 23:58
…ed 1 container (#1379)

* fix(cli): parse both shapes of docker compose ps --format json

Current Compose emits a single line holding a JSON array; composePs only
handled the older one-object-per-line shape. It failed silently rather than
throwing: an array is valid JSON, so JSON.parse of the whole "line" succeeded,
.map returned exactly ONE entry, and Name/State/Status were undefined on an
array. So `neoboard status` reported "running (1 containers)" no matter how
many were up, with empty name/state/status on the typed ContainerInfo that any
future caller would trust.

Parse the whole output and use it directly when it is an array, falling back
to newline-delimited parsing otherwise — works across Compose versions instead
of trading one version-specific bug for another.

Closes #1369

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(cli): probe /api/health in status, not the auth-gated /

getAppHealth curled `/` and treated only 200 as healthy. But `/` is auth-gated,
so the sessionless request curl makes is *supposed* to 307 to /login — a
correctly working instance reported "unhealthy (HTTP 307)" every single time.
There was no state in which that line was right, and it invited operators to go
debugging a non-problem or to stop trusting the one command that answers "is it
working?".

Probe /api/health instead: no session needed, and it is already what
docker-compose.full.yml's healthcheck targets, so the CLI and Docker now agree.
Because the probe now keeps the body as well as the status code, a non-empty
`errors` array reports unhealthy WITH the reason — an app that is up but
degraded used to read identically to a healthy one. An unparseable 200 body
stays healthy: the app answered, and inventing a failure from a payload we
merely failed to read would reintroduce the same false negative.

Closes #1368

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: alfredorubin96 <alfredo.rubin@neotechnology.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…yout (#1375) (#1378)

* fix(component): a save on a narrow window permanently squashed the layout (#1375)

One layout is stored per dashboard page, but `ResponsiveGridLayout` was handed
that single layout with four different column counts (lg:12, md:10, sm:6, xs:4).
Below `lg` the grid clamps every item into the narrower count — and `onDragStop`
hands the clamped result back, which is then persisted as THE layout. The
authored 12-column arrangement got overwritten by its own squashed projection,
and since nothing ever widened it again, each save on a narrow window ratcheted
it further toward a single column. Irreversibly: the stored layout is the only
one there is.

The container is the viewport minus the sidebar, so a 1280px window already
measures below the lg:1200 breakpoint. This fired on ordinary laptops, which is
why it read as intermittent — "sometimes when saving".

Fix: one column count at every breakpoint. If you store one layout, you have to
author at one column count; the grid now scales instead of reflowing, so a drag
can never return fewer columns than it was given. Responsive stacking, if it is
ever wanted, needs somewhere to store per-breakpoint layouts — it cannot be a
side effect that destroys the only layout we keep.

The existing `handleUserLayoutChange` guard is untouched and was never
sufficient here: it correctly stops incidental reflow from persisting, but a
real drag at `md` is a real user drag and still returned a 10-column layout. It
fixed the trigger, not the projection.

Deliberately NOT added: a store-level "a save must never reduce the layout's
column span" guard, which the issue proposed. At the store layer a user
legitimately resizing widgets narrower is indistinguishable from a breakpoint
projection, so that rule would reject real edits. The breakpoint context only
exists in the grid, which is where this belongs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(component): assert the exact breakpoint→cols mapping, not just that values agree

CodeRabbit was right: `new Set(Object.values(cols)).size === 1` is satisfied by
`{ lg: 12 }` alone, so the test would have passed a fix that mapped only `lg` and
left md/sm/xs unmapped — which is the bug itself. Exact object assertions close
that hole, and the narrow-container case now pins the measured width too, so it
cannot silently stop exercising the sub-lg path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: alfredorubin96 <alfredo.rubin@neotechnology.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…browser (#1380)

DialogContent centres with translate(-50%,-50%), but tailwindcss-animate's
`enter` keyframe is `from`-only and `exit` is `to`-only, and both build a single
`transform` from --tw-enter/exit-translate-x/y, which default to 0. With no
slide-* utility setting them, every dialog interpolated between
translate3d(0,0,0) — its top-left corner on the centre anchor — and the resting
translate(-50%,-50%): flying in from the bottom-right on open, back out to it on
close. zoom-in-95 did not remove a slide, it introduced one.

Add the four slide-*-1/2 compensation classes, matching the sibling AlertDialog
which still had them and was never broken. `1/2` on both axes rather than
upstream shadcn's top-[48%]: 48% is a deliberate 2%-of-height rise, whereas -50%
on both axes keeps the visual centre mathematically invariant for the whole
animation. The comment block says plainly that these are centring compensation
and not motion, since they have now been deleted twice (d723a12, PR #1173).

jsdom cannot catch this: no layout engine, no getAnimations(). That is why the
old class-presence test — asserting the *absence* of slide-in-from-bottom —
passed both before and after the bug. It now positively asserts the four
classes, and three new stories scrub the paused animation in real chromium via
the previously unused `storybook` Vitest browser project, asserting the box
centre never leaves the viewport centre. Worst-sample drift, size=full:
584.0px before, 0.00006px after. Wired into CI as a `test:visual` step;
deliberately not into `npm run verify`, where a chromium download does not
belong.

Closes #1373

Co-authored-by: alfredorubin96 <alfredo.rubin@neotechnology.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…nt (#1381)

Scrolling a graph tile out of view and back re-ran NVL's force layout from
scratch, so the nodes landed in a different arrangement each time. LazyVisible
unmounted its children unconditionally once they left the viewport, even on a
page holding 8 graphs — half the browser's ~16 WebGL contexts — where nothing
needed releasing.

Mount stays gated on intersection, so a graph-dense dashboard still doesn't
build every context on initial load (#1052). Unmount is now gated on a
page-wide budget: an off-screen slot only gives up its context once more than
WEBGL_WIDGET_BUDGET slots are live, and then only the oldest off-screen ones,
one per slot over budget.

Closes #1367

Co-authored-by: alfredorubin96 <alfredo.rubin@neotechnology.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Closes #1374.

The issue's stated mechanism is wrong, and so was the follow-up analysis.
Measured in Chromium at 1280x720:

  empty / 3 lines   173px editing surface, no internal scroll
  30 lines          627px, no internal scroll
  120 lines        2391px, no internal scroll

The editor is neither capped nor starved. It has no definite height at all:
`.cm-editor { height: 100% }` resolves against an indefinite-height flex
parent, so it computes to `auto` and the column grows without bound with the
document. Nothing scrolls internally — what scrolls is the whole left settings
column, dragging the Run toolbar, tabs and chart selectors out of view with it.

The preview was never the cause either. The modal body is `display: grid` with
`minmax(0,1fr) minmax(0,1fr)`, so the panes sit side by side and never compete
for height. `flex-shrink-0` on the preview was inert (its flex parent has no
definite height, so nothing ever applies shrink pressure) — removed. `h-[500px]`
stays: chart and graph renderers measure that container.

The toggle collapses the grid to one column, unmounts the preview and swaps the
editor to `h-[70vh]`. Measured effect:

  short query   173px -> 457px height (2.64x), 553px -> 1140px width
  120 lines     width doubles; scrolling moves into the editor
                (2391/2391 -> 2391/457) with the toolbar pinned

Height cannot grow for a long query: at 720px the modal body is capped at
calc(90vh - 180px) = 468px, so ~457px is the ceiling any toggle can offer. The
modal itself is the remaining constraint — issue option (c).

Notes on the implementation:

- The preview is UNMOUNTED, not hidden. ECharts recovers via ResizeObserver and
  TableRenderer guards on containerHeight <= 0, but NVL's WebGL canvas is
  fragile at a 0-height mount; `display: none` would keep every renderer alive
  at 0x0. Unmounting makes the first measurement on the way back correct.
- Classes change, the tree does not. QueryEditorPanel is not reparented, so
  CodeMirror never remounts and the cursor position and undo history survive
  the toggle. The E2E asserts the live CM6 doc length across both directions.
- The toggle lives in the editor header, not the preview header — maximizing
  hides the preview, so a control there would leave no way back (and #1372 adds
  its own toggle to that header).
- The maximize request is ignored for parameter-select and content-only widgets:
  they render no query editor, so honouring it would strand the user in a
  one-column layout with no preview and no control to escape.

Co-authored-by: alfredorubin96 <alfredo.rubin@neotechnology.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…s remounting it (#1370, #1371) (#1387)

* chore: ignore the E2E server pid file

Every `playwright test` run writes app/e2e/.server-pid for globalTeardown
to kill the Next server with. Its sibling .containers-state.json is already
ignored; this one was not, so it showed up as untracked after every run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(dashboard): clamp a negative ?page= and retire the dead editMode flag (#1371)

setLayout clamped only the upper bound, so `?page=-1` reached the store as -1
and rendered a blank dashboard — no page matched the active index. Clamp the
lower bound too; `?page=99` was already covered.

editMode/setEditMode had zero consumers outside the store and its own tests
(#1370 called this out). The URL segment is the mode now, so a store mirror
would be a second source of truth that can desync. Removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(dashboard): hoist the dashboard UI into [id]/layout so Cmd-E stops remounting it (#1370, #1371)

View and edit were two Next page segments, so Cmd-E unmounted the entire
dashboard tree and mounted the other one: every widget re-ran its query, and
while the outgoing tree was gone the document collapsed to near-zero height
and the browser clamped the scroll offset — the reported "screen flicks,
trying to scroll up". The local activePageIndex re-initialised to 0 on the way
back, so the page you were editing was discarded (#1371).

Both URLs stay. The UI moves up into a new `[id]/layout.tsx` and both
`page.tsx` files become `return null`. Next preserves a layout across
navigation into a child segment, and `edit` IS a child of `[id]`, so the
toggle re-renders only the empty page slot: same DOM nodes, same chart
instances, same in-flight queries. `editable` was already a pure prop down to
card-container, so flipping the mode remounts and re-queries nothing.

Also here:

- The active page moves from the view route's local useState to
  dashboard-store, which the layout outlives — so it survives the toggle in
  both directions. `?page=` is still read once on first load for existing
  /[id]/edit?page=N links, and is deliberately NOT appended to the exit
  navigation: ~8 specs match the view URL with /\/[\w-]+$/.
- The setLayout effect is keyed on `id:version` instead of the `dashboard`
  object. useUpdateDashboard invalidates ["dashboards", id] on every save, so
  saving while on page 3 already threw you back to page 1 and clobbered
  savedLayout/_dirty; it now bails when the store is dirty and keeps the
  current index on reloads.
- View mode renders the server layout, edit mode the store's working copy, so
  discarded edits do not leak into the view. While clean they are the same
  object (migrateLayout returns a v2 layout verbatim), so nothing remounts.
- The auto-refresh countdown moves into the view toolbar, confining its 1Hz
  re-render to the toolbar instead of the widget tree.
- `enabled: editMode` on useConnections/useWidgetTemplates keeps view mode
  from fetching the editor's data (#913 removed that fetch; the merge would
  have reintroduced it). useConnections takes an options bag now.
- Missing `{ scroll: false }` added to the empty-page CTA and the reader
  redirect.

Tests: a jsdom probe counts DashboardContainer unmounts in its effect CLEANUP
and asserts the same instance survives the toggle — the only test that can
regress-proof #1370. Two E2E specs tag a widget card with a data attribute
that a remount would destroy, and assert scroll equality within 2px and that
page 3 is still selected after a round trip. Both fail on the parent commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: alfredorubin96 <alfredo.rubin@neotechnology.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The CHANGELOG stopped at [1.0.0] — 2026-05-17. Four releases and roughly 200
closed issues had shipped since with no entry, so anyone reading the file
concluded the project went quiet in May. That is the opposite of the truth, and
it is the first thing a visitor checks to decide whether a project is alive.

Reconstructed from the GitHub milestones, not from memory: every bullet traces
to a closed issue and cites it. All 152 distinct issue references were verified
to exist and be closed.

Release dates are the dates each release branch was consolidated into `dev`,
because no 1.x version has ever been git-tagged — the repo has two tags and
neither belongs to this line. Rather than imply four tagged releases exist, a
note at the top states that plainly and points at #1216, which tracks cutting
the first one.

Two things deliberately not smoothed over:

- 1.1.0 moved the interaction accent to indigo (#1104) and 1.3.0 moved it back
  to citrine amber (#1125). Both entries say so. A changelog that hides a
  reversal is less useful than one that shows it.
- #1155 is described as replacing the modal slide-up with a scale-and-fade, NOT
  as centring the entrance. It did not centre it: dialogs still animated in
  212.5px off-centre until #1373. Claiming otherwise would have put a false
  statement in a public document for the third time.

Also adds an [Unreleased] section for the v1.5 work already on release/1.5, so
the file is current rather than ending at the last consolidation.

Co-authored-by: alfredorubin96 <alfredo.rubin@neotechnology.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…import count (#1376, #1377) (#1392)

* fix(dashboard): bump version/updatedAt on reassign so the optimistic lock sees it

reassignConnectionWidgets rewrote layoutJson and nothing else, while
PUT /api/dashboards/[id] bumps version, updatedAt and updated_by and clients
send expectedVersion as an optimistic lock.

A reassign was therefore invisible to that lock: a browser with the editor
open still held a matching version, so its next save silently REVERTED the
reassign instead of conflicting. Bumping version turns that lost write into
the 409 it should always have been.

This changes the existing global reassign's behaviour too — desirably.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(dashboard): scope connection reassignment to one dashboard (#1376, #1377)

Reassignment was connection-scoped and global: repointing one imported
dashboard silently repointed every other dashboard using that connection.

Lib
- reassignConnectionWidgets takes an options object instead of five positional
  strings. Adding a sixth positional meant transposing userId and tenantId
  type-checked cleanly and silently wrote across tenants.
- optional dashboardId adds an ADDITIVE `AND d.id = $x` to both the count and
  the UPDATE. It never replaces editableDashboardsScope — `d.id = $x` is a
  filter, not an authorization check, so the owner/editor-share test still
  gates the raw UPDATE even though the route checks too.
- fromConnectionId "" now means "unassigned and needs a connector" (#1377).
  Two traps handled: `widget->>'connectionId' = ''` is NULL-blind for a widget
  with no connectionId key, so COALESCE folds missing and empty together; and
  "" is OVERLOADED because dashboard-export rewrites markdown/iframe widgets
  to connectionId:"" as well, so content-only types are excluded or a bulk
  assign stamps a connector onto text widgets. `NOT IN` is NULL when chartType
  is absent, so an unclassifiable widget is skipped rather than stamped.
- the match predicate is extracted once and shared by all three sites (count
  LATERAL, UPDATE CASE, UPDATE EXISTS) so the reported count cannot drift from
  what was actually rewritten.
- CONTENT_ONLY_CHART_TYPES is exported so the SQL mirrors one source of truth.

Route
- POST /api/dashboards/{id}/reassign-connection. The source lives in the body,
  not a path segment, because a path segment cannot express "no connection" —
  that is the whole reason this sits alongside the connection-scoped endpoint.
- guards: canWrite, editor on the dashboard, and a target the caller can
  actually QUERY (in-tenant AND owned/shared/admin), mirroring /api/query
  rather than the weaker "exists in tenant". Source ownership is deliberately
  not required; the write target is the dashboard.
- type check kept for a real source, skipped when the source is empty since the
  original connector type is unrecoverable after an import.
- audited as connection.reassign against resourceType "dashboard".

Unit tests assert the SHAPE of the emitted SQL via PgDialect.sqlToQuery, since
a dropped tenant predicate or ORDER BY is invisible to a result-count
assertion. Documented in openapi-spec.ts; the existing route and the
delete-dialog flow are untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(dashboard): add the Change connection dialog and fix the import count (#1376, #1377)

UI
- "Change connection…" joins the dashboard card's options menu between
  Duplicate and Export, gated by canEdit. The bulk reassign is no longer
  reachable only by starting to delete a connection.
- new DashboardConnectionDialog, in its own file so jsdom can render it. It
  derives {connectionId → widgetCount} plus an "Unassigned (N)" row client-side
  from useDashboard's layoutJson — no new GET endpoint — and takes names/types
  from useConnections. ONE SOURCE AT A TIME: pick source, pick target, apply,
  repeat. A mapping table would need every source resolved before anything
  could be applied, which is worse for the common case of one wrong connection.
- the target picker copies the delete-dialog's same-type filter, and drops it
  for the Unassigned source where there is no source type to match.
- the same dialog serves #1377's post-import prompt and #1376's menu item,
  mounted once on the page both entry points already live on.

Import count
- the response now carries unassignedWidgetCount, so the offered count and the
  count named in the note are the same number by construction.
- that count now EXCLUDES content-only widgets, which fixes a pre-existing
  false alarm: dashboard-export writes markdown/iframe widgets with
  connectionId "", so importing a correctly-mapped dashboard containing 3 text
  widgets already reported "3 widgets imported without a connection".
- when the skipped connections have different connector types the shortcut is
  withheld and today's per-widget note stands, since one target cannot be right
  for both. That decision is a pure helper (importFollowUp) rather than inline
  JSX so it is covered by unit tests.

E2E proves the isolation invariant a mocked driver cannot: the same export is
imported twice against connection A, dashboard #1 is reassigned to B through
the menu, and #2 is asserted to still be on A and still loading. Also covers
the skipped-import bulk fix without opening a widget editor, that a markdown
widget is neither counted nor reassigned, and that a viewer-share user gets
neither the menu item nor the API.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: alfredorubin96 <alfredo.rubin@neotechnology.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…window save (#1375) (#1385)

#1378 fixed the ratchet (one column count at every breakpoint) with component
tests only. Three E2E attempts were removed with it because none could be shown
to fail against the bug. This is the one that can, and it was verified red with
`dashboard-grid.tsx` reverted and green with it restored.

The two things that made the earlier attempts blind:

Width cannot detect this. Clamping a 12-column layout into 10 keeps `w:6` and
moves `x:6` → `x:4`, so the items overlap and the vertical compactor stacks
them. Position is what changes. The reverted run reports it exactly:

    left  = {x:274, y:120, width:641}
    right = {x:274, y:480, width:641}

Same width, 360px apart vertically. Any width assertion passes there — and the
stored layout and the breakpoint rendering it shrink together anyway, so a
`6-of-12` item saved as `6-of-10` measures identically at a fixed viewport.

And the seeded dashboards are effectively single-column, so they cannot exhibit
the symptom regardless of what is asserted. Hence a purpose-built fixture: two
markdown widgets at x:0,w:6 and x:6,w:6 on one row, seeded over the API.

The drag and save happen at 1280px (container ~1040, below lg); the assertions
happen at 1600px (container ~1360, at lg) where the authored 12 columns are
actually rendered as 12. Both container widths are asserted rather than assumed,
so a future sidebar change fails the test loudly instead of quietly making it
vacuous — as does the pre-save check that the fixture really is two-up.

Co-authored-by: alfredorubin96 <alfredo.rubin@neotechnology.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…wn (#1384) (#1386)

Leaflet's `_animateZoom` arms a bare `setTimeout(_onZoomTransitionEnd, 250)`
that `Map.remove()` never cancels — `remove()` runs `_stop()` (pan animation
only) and then `delete this._mapPane`. Unmounting a map mid-zoom let that
callback fire against the destroyed map: `_onZoomTransitionEnd` -> `_move` ->
`_getMapPanePos` -> `getPosition(undefined)` -> `undefined._leaflet_pos`.

`_onZoomTransitionEnd` early-returns on a falsy `_animatingZoom`, so clearing
the flag in the init-effect cleanup disarms both the timer and the
`transitionend` path. MapChart is the only Leaflet map in the repo, so the
one cleanup covers every caller.

With teardown clean, `test:visual` now runs the whole `storybook` project
(via the existing `component` workspace script) instead of the single
dialog story file, so all 393 story smoke tests gate CI.

Before: 393 passed, 4 unhandled errors, exit 1.
After:  393 passed, 0 errors, exit 0.

Closes #1384

Co-authored-by: alfredorubin96 <alfredo.rubin@neotechnology.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
`review:local` still diffed against release/1.4, so anything branched from
release/1.5 was reviewed against the previous release — every commit already
consolidated into 1.5 showed up as "changed".

Co-authored-by: alfredorubin96 <alfredo.rubin@neotechnology.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…1393)

The repo root held 13KB of internal agent instructions, visible to every GitHub
visitor alongside the README. `.claude/` is where the rest of that machinery
already lives — hooks, skills, agents, settings — so the doc belongs with them.

Verified before moving, because a wrong answer here would silently disable every
project rule with no error: Claude Code loads project memory from BOTH
`./CLAUDE.md` and `./.claude/CLAUDE.md` (per the official memory docs, which name
the two as equivalent). So the TDD, package-boundary, query-safety and credential
rules keep loading.

Two references were load-bearing rather than prose and would have broken:

- `app/src/lib/__tests__/docs-accuracy.test.ts` reads the doc by path and asserts
  its claims — the path-existence check, the MIGRATE_ON_START claim and the
  tenant-guard path. It would have failed on a missing file.
- `package.json`'s `review:local` passes `-c CLAUDE.md` to the CodeRabbit CLI.

The remaining mentions were comments and skill/agent prose; those now point at the
new path so nobody goes looking for a root file that no longer exists.

Also adds #1376 and #1377 to the [Unreleased] section, including the two bugs that
surfaced during that work: the reassignment being invisible to the optimistic lock,
and the import's unassigned-widget count counting markdown and iframe widgets.

Co-authored-by: alfredorubin96 <alfredo.rubin@neotechnology.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…1373) (#1389)

#1373 fixed DialogContent to compensate the centring translate on both axes with
`slide-*-1/2`. Its sibling AlertDialogContent still carried upstream shadcn's
`top-[48%]`, a deliberate 2%-of-height lift, so the two modal components animated
differently for no design reason: the confirmation dialog rose into place while
the editor dialog scaled in place. Measured in chromium, the drift is 3.24px on a
162px-tall confirmation — small, but it is travel, and travel is what got these
classes deleted from Dialog twice (d723a12, PR #1173) by someone reading them as
motion.

Both axes are `1/2` now, and AlertDialogContent carries the same DO-NOT-DELETE
comment: these classes are centring compensation, not motion. The comment also
records the `1/2`-over-`[48%]` decision so the next person does not "restore
upstream".

Tests, both layers, Red verified by putting `[48%]` back:
- `stories/ui/alert-dialog.stories.tsx` — CentredOnEnter/CentredOnExit scrub the
  paused animation in real chromium and assert the box centre stays within 1.5px
  of the viewport centre. With `[48%]`: dy=3.2px at t=0 on enter, t=200ms on
  exit. With `1/2`: passes.
- `src/components/ui/__tests__/alert-dialog.test.tsx` — jsdom class-presence
  guard, including `not.toContain("48%")`. Fails fast if the classes go; cannot
  see the geometry, which is why the story exists.

The scrub helper moved to `stories/ui/animation-centring.ts` rather than being
copied — the two modals are held to one rule, so they share one measurement and
cannot drift apart. `test:visual` was pinned to dialog.stories.tsx and now runs
both files.

Co-authored-by: alfredorubin96 <alfredo.rubin@neotechnology.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…1388)

* fix(dashboard): make parameter URL sync opt-in and honour the toggle

The dashboard viewer called buildUrlParams() with no exclude set, so every
parameter landed in the address bar whatever the widget's "Sync to URL" option
said — extractNoSyncParams() had zero production callers.

Wiring it up was not enough on its own. The chart option defaults to false and
is never persisted until an author flips it, so a widget nobody touched showed
the toggle off while still publishing its value. Sync is now opt-in: only
syncToUrl === true reaches the URL, and the allow-list argument is required
rather than optional, so the compiler catches the omission that caused this.

Also covers the companion keys range widgets write (_from/_to/_min/_max), and
strips a non-syncing param from an inbound URL instead of merely omitting it
from later updates.

Note: existing deep-links stop populating until the author enables "Sync to
URL" on the widget and saves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(dashboard): port the opt-in URL sync onto the hoisted workspace

#1387 moved the dashboard body out of [id]/page.tsx into dashboard-workspace.tsx
while this branch was open, so the parameter->URL sync effect this PR rewrites no
longer lived where it patched. page.tsx is now a return-null routing stub.

Ported the opt-in effect onto dashboard-workspace.tsx. This was not optional
housekeeping: buildUrlParams' `syncable` argument became REQUIRED, and the
workspace still called it with one argument, so merging as-is would not have
type-checked.

The effect had to move down the component body — it needs `dashboard`, which is
fetched after the point where the old effect sat.

Guarded the replace on the URL actually changing, tracked against what we last
wrote rather than window.location:

- Without any guard, the up-front strip replaces the URL we are already on. That
  is indistinguishable from a redirect, which made the existing "a reader is not
  redirected in view mode" test unfalsifiable — it asserts on exactly that call.
- Reading window.location instead would look correct and silently break the
  clear-parameters test, because a mocked router never updates the location.

Three tests in dashboard-workspace.test.tsx encoded the old opt-out contract and
were rewritten to the new one rather than deleted:

- "mirrors an opted-in parameter into the URL" — now uses a parameter-select
  widget with syncToUrl: true, since that is what makes a param syncable.
- "does NOT mirror a parameter whose widget never opted in" — new. Without it an
  implementation that synced everything would still pass the positive case.
- "drops the query string once every opted-in parameter is cleared" — now puts
  the param in the URL first. The drop is a transition; asserting the bare
  pathname from a standing start passes even if sync is broken entirely.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: alfredorubin96 <alfredo.rubin@neotechnology.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Six PRs landed after the CHANGELOG was reconstructed and were not represented:
#1384, #1373's AlertDialog follow-up, #1388, plus the CLAUDE.md move and the
review:local retarget.

The #1388 entry leads with the actual defect rather than the design change,
because the defect is the part a user felt: `extractNoSyncParams()` had ZERO
production callers, so the "Sync to URL" toggle on a parameter-select widget did
nothing at all — switching it off still published the value to the address bar.
The opt-in inversion is the fix, and it is flagged as breaking for shared links,
since a URL carrying `?param_…` for a widget that never opted in will no longer
reproduce those values.

The #1384 entry records the knock-on that matters more than the crash: four
unhandled `_leaflet_pos` errors made the whole Storybook browser project exit 1
even though every story passed, which is why the visual gate could only be aimed
at two files. With the timer disarmed, `test:visual` covers the whole project —
which is what #1389 then relied on.

All 13 issue references in the section verified to exist and be closed. #1384
itself was still open despite its PR merging; closed with the same note.

Co-authored-by: alfredorubin96 <alfredo.rubin@neotechnology.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
) (#1462)

* fix(component): honour prefers-reduced-motion across all overlays (#1458)

Ten Radix primitives animated unconditionally while spinner, progress and
skeleton had honoured the preference since the component audit. The obvious
per-component fix does not work: the overlays animate through data-attribute
variants such as `data-[state=open]:animate-in`, which compile to
`.class[data-state=open]` — specificity (0,2,0) — whereas a
`motion-reduce:animate-none` utility is (0,1,0), and a media query contributes
no specificity. The guard loses the cascade at every one of the fourteen
animated surfaces.

The reset therefore lives once in `design-tokens.css`, the only stylesheet
that both `component/src/index.css` and `app/src/app/globals.css` import.

It uses `animation: none` rather than the usual `animation-duration: 0.01ms`
because Radix's presence machinery reads `getComputedStyle().animationName`
and unmounts synchronously when it is `none`, but waits for an `animationend`
when it is not — and that wait is what made `dev` red. Radix keeps an overlay
mounted until the exit animation reports back; when an NVL/WebGL widget mounts
alongside the close, the 150ms animation stalls and the dialog stays in the
DOM well past a 5s budget. Every "flaky graph chart" failure was that same
`expect(dialog).not.toBeVisible()` assertion — never a graph assertion — which
is why the failing test rotated across the block and retries never helped.

Consequences of `animation: none` are handled rather than assumed:
`scrollAndHighlight` removed its highlight class only on `animationend`, so
under reduced motion the class would have stuck forever. It now also listens
for `animationcancel` (an interrupted pulse dispatches that instead, a latent
leak independent of this change) and keeps a timer as the backstop.

Playwright runs the suite as a reduced-motion user, so the accessibility
branch is exercised in CI instead of merely declared.

Tests: the mock-based `scrollAndHighlight` cases asserted only that a stubbed
`addEventListener` had invoked its own callback, staying green whether or not
the highlight was ever cleared. They are replaced by jsdom tests against a
real DOM; the node file keeps the parts that genuinely need no DOM.

Verified: full E2E 359 passed / 0 failed, and the graph block 30/30 across
three repeats with no flakes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(component): scope highlight cleanup to the widget's own animation

Addresses two CodeRabbit findings on #1462.

`animationend` and `animationcancel` bubble, and a widget card is full of
things that animate independently (skeletons, spinners, ECharts). The first
descendant animation to finish would have invoked the cleanup and stripped the
highlight early. The handler now runs only when `event.target` is the widget
itself.

Also moves the node-environment test next to its source, per the repo rule that
tests live in `__tests__/` beside the file under test, and stops its fake
element from invoking listener callbacks with no argument — no real listener
does that, and nothing left in that file needs the event to fire.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(e2e): wait for the save to land, not for a button that was never disabled

`await expect(Save).toBeEnabled()` is not a wait. The button is enabled both
before and after the mutation, so the assertion passes on its first poll and
the test races on while the dashboard store is still dirty. Clicking "Back"
then trips the unsaved-changes guard, and its AlertDialog overlay swallows the
pointer events the test is about to send.

Animation timing was hiding this: the incidental delays gave the save time to
land. With animations disabled (#1458) the race is exposed, and
`charts.spec.ts:872` failed 1 run in 5.

`parameters.spec.ts` and `form-widget.spec.ts` had each already met this and
worked around it locally by clicking "Leave" if the dialog happened to appear
— one of them even names it "grid compaction race". The dialog is not the
problem; not waiting for the save is.

`dashboard-workspace.tsx` calls `markSaved()` and only then raises the
"Dashboard saved" toast, so the toast is the first observable moment at which
the store is genuinely clean. `saveDashboard()` in fixtures waits for it, so
the dialog never appears rather than being dismissed after the fact.

Applied to the three Save-then-Back sites that had no workaround
(charts.spec.ts ×2, styling-rules.spec.ts). The already-guarded sites are left
alone.

Verified: charts.spec.ts:872 6/6 with retries disabled (was 1 failure in 5),
and the full suite 360 passed / 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(component): scope the reduced-motion assertions to the media block

Addresses the third CodeRabbit finding on #1462.

The property assertions searched the whole stylesheet, so they would have
passed on an `animation: none !important` sitting anywhere else in the file —
including one applied unconditionally, which is the opposite of the intent.
They now run against the extracted media block only (179 chars of 9,240), plus
a check that the extraction has not silently swallowed the whole file and made
every assertion vacuous.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: alfredorubin96 <alfredo.rubin@neotechnology.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@alfredo1996 alfredo1996 added chore Maintenance and housekeeping area:release Release process and packaging labels Aug 4, 2026
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1cb95f87-119e-4372-8629-33d121918670

📥 Commits

Reviewing files that changed from the base of the PR and between c84c594 and b2717ec.

📒 Files selected for processing (85)
  • .claude/CLAUDE.md
  • .claude/agents/project-architect.md
  • .claude/hooks/check-boundaries.sh
  • .claude/hooks/check-migration-guard.sh
  • .claude/skills/next/SKILL.md
  • .github/workflows/ci.yml
  • .gitignore
  • CHANGELOG.md
  • app/e2e/charts.spec.ts
  • app/e2e/dashboard-connection-reassign.spec.ts
  • app/e2e/edit-page-preservation.spec.ts
  • app/e2e/edit-scroll-position.spec.ts
  • app/e2e/editor-maximize.spec.ts
  • app/e2e/fixtures.ts
  • app/e2e/grid.spec.ts
  • app/e2e/heavy-widgets.spec.ts
  • app/e2e/parameter-url-sync.spec.ts
  • app/e2e/styling-rules.spec.ts
  • app/playwright.config.ts
  • app/src/__tests__/test-environment-boundary.test.ts
  • app/src/app/(dashboard)/[id]/__tests__/layout.test.tsx
  • app/src/app/(dashboard)/[id]/edit/page.tsx
  • app/src/app/(dashboard)/[id]/layout.tsx
  • app/src/app/(dashboard)/[id]/page.tsx
  • app/src/app/(dashboard)/page.tsx
  • app/src/app/api/connections/[id]/reassign/__tests__/route.test.ts
  • app/src/app/api/connections/[id]/reassign/route.ts
  • app/src/app/api/dashboards/[id]/reassign-connection/__tests__/route.test.ts
  • app/src/app/api/dashboards/[id]/reassign-connection/route.ts
  • app/src/app/api/dashboards/import/__tests__/route.test.ts
  • app/src/app/api/dashboards/import/route.ts
  • app/src/components/__tests__/dashboard-connection-dialog.test.tsx
  • app/src/components/__tests__/dashboard-workspace.test.tsx
  • app/src/components/__tests__/lazy-visible.test.tsx
  • app/src/components/__tests__/widget-editor-modal-maximize.test.tsx
  • app/src/components/dashboard-connection-dialog.tsx
  • app/src/components/dashboard-edit-toolbar.tsx
  • app/src/components/dashboard-view-toolbar.tsx
  • app/src/components/dashboard-workspace.tsx
  • app/src/components/lazy-visible.tsx
  • app/src/components/widget-editor-modal.tsx
  • app/src/components/widget-editor/__tests__/query-editor-panel.test.tsx
  • app/src/components/widget-editor/__tests__/widget-preview-panel.test.tsx
  • app/src/components/widget-editor/query-editor-panel.tsx
  • app/src/components/widget-editor/widget-preview-panel.tsx
  • app/src/hooks/__tests__/use-connections.test.ts
  • app/src/hooks/use-connections.ts
  • app/src/hooks/use-dashboards.ts
  • app/src/hooks/use-widget-templates.ts
  • app/src/lib/__tests__/db/connection-reassign.test.ts
  • app/src/lib/__tests__/docs-accuracy.test.ts
  • app/src/lib/__tests__/shared/url-params.test.ts
  • app/src/lib/api/openapi-spec.ts
  • app/src/lib/dashboard/__tests__/import-follow-up.test.ts
  • app/src/lib/dashboard/import-follow-up.ts
  • app/src/lib/db/__tests__/tenant-scope.test.ts
  • app/src/lib/db/connection-reassign.ts
  • app/src/lib/shared/url-params.ts
  • app/src/lib/widget/__tests__/scroll-to-widget.test.ts
  • app/src/lib/widget/__tests__/scroll-to-widget.test.tsx
  • app/src/lib/widget/__tests__/webgl-budget.test.ts
  • app/src/lib/widget/content-only-chart.ts
  • app/src/lib/widget/scroll-to-widget.ts
  • app/src/lib/widget/webgl-budget.ts
  • app/src/plugins/graph/component.tsx
  • app/src/stores/__tests__/dashboard-store.test.ts
  • app/src/stores/dashboard-store.ts
  • cli/src/__tests__/commands/status.test.ts
  • cli/src/__tests__/lib/docker.test.ts
  • cli/src/commands/status.ts
  • cli/src/lib/docker.ts
  • component/design-tokens.css
  • component/src/__tests__/reduced-motion.test.ts
  • component/src/charts/__tests__/map-chart.test.tsx
  • component/src/charts/map-chart.tsx
  • component/src/components/composed/__tests__/dashboard-grid.test.tsx
  • component/src/components/composed/dashboard-grid.tsx
  • component/src/components/ui/__tests__/alert-dialog.test.tsx
  • component/src/components/ui/__tests__/dialog.test.tsx
  • component/src/components/ui/alert-dialog.tsx
  • component/src/components/ui/dialog.tsx
  • component/stories/ui/alert-dialog.stories.tsx
  • component/stories/ui/animation-centring.ts
  • component/stories/ui/dialog.stories.tsx
  • package.json

Walkthrough

The pull request centralizes dashboard view and edit rendering in DashboardWorkspace, adds dashboard-scoped connection reassignment, updates widget lifecycle and editor behavior, improves component motion and layout handling, extends CLI health parsing, and expands automated validation.

Changes

Dashboard workspace and routing

Layer / File(s) Summary
Persistent dashboard workspace
app/src/app/(dashboard)/[id]/*, app/src/components/dashboard-workspace.tsx, app/src/stores/dashboard-store.ts
Dashboard routes now delegate rendering to a persistent workspace that preserves pages, parameters, edit state, saves, refresh settings, and navigation state.
Dashboard workspace coverage
app/src/components/__tests__/dashboard-workspace.test.tsx, app/e2e/edit-page-preservation.spec.ts, app/e2e/edit-scroll-position.spec.ts
Tests cover page preservation, route transitions, active-page clamping, scroll position, and widget DOM identity.

Dashboard connection reassignment

Layer / File(s) Summary
Import recovery and reassignment flow
app/src/app/(dashboard)/page.tsx, app/src/components/dashboard-connection-dialog.tsx, app/src/hooks/use-dashboards.ts
Imports report unassigned connection-backed widgets and provide bulk or manual follow-up actions. Editors can reassign dashboard connections through a shared dialog.
Scoped reassignment API and database logic
app/src/app/api/dashboards/[id]/reassign-connection/*, app/src/lib/db/connection-reassign.ts, app/src/lib/api/openapi-spec.ts
The API validates permissions, tenancy, connection visibility, connector compatibility, dashboard scope, auditing, and reassignment results.
Reassignment validation
app/src/app/api/dashboards/[id]/reassign-connection/__tests__/route.test.ts, app/src/lib/__tests__/db/connection-reassign.test.ts, app/e2e/dashboard-connection-reassign.spec.ts
Unit and E2E tests cover authorization, tenant isolation, content-only widgets, empty sources, dashboard isolation, import recovery, and viewer restrictions.

Widget editor and rendering

Layer / File(s) Summary
Maximized query editor
app/src/components/widget-editor-modal.tsx, app/src/components/widget-editor/query-editor-panel.tsx, app/e2e/editor-maximize.spec.ts
Query editors can maximize into a single-column layout with internal scrolling. The preview unmounts during maximization and returns when restored.
WebGL and widget lifecycle
app/src/lib/widget/webgl-budget.ts, app/src/components/lazy-visible.tsx, app/src/lib/widget/scroll-to-widget.ts
Visible widgets claim bounded WebGL slots. The oldest off-screen slots can be evicted. Highlight cleanup handles animation completion, cancellation, and timeout fallback.

Component and CLI updates

Layer / File(s) Summary
Component layout and motion
component/design-tokens.css, component/src/components/ui/*, component/src/components/composed/dashboard-grid.tsx, component/src/charts/map-chart.tsx, component/stories/ui/*
Reduced-motion styles disable animation and smooth scrolling. Dialog centering, responsive grid columns, and Leaflet teardown behavior are updated and tested.
CLI health and Compose parsing
cli/src/commands/status.ts, cli/src/lib/docker.ts
Status checks use /api/health and parse reported errors. Compose parsing accepts JSON arrays, objects, and newline-delimited records.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested labels: pkg:app, pkg:component, pkg:cli, testing, area:ci

Suggested reviewers: alfredorubin96

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 51.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the primary change: consolidating release/1.5 into dev.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch release/1.5

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (20)
app/src/lib/db/connection-reassign.ts (1)

94-105: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Wrap the composite predicate in parentheses.

For an empty source, widgetMatchesSource returns two predicates joined by AND without an enclosing group. All three current call sites place the fragment last in a WHERE or WHEN, so the result is correct today. If a future edit combines the fragment with OR, or appends another predicate, the AND binds unexpectedly and the content-only exclusion is silently lost. Parentheses make the fragment self-contained.

♻️ Proposed change
   return sql`
-    COALESCE(widget->>'connectionId', '') = ''
-    AND widget->>'chartType' NOT IN (${sql.join(
-      CONTENT_ONLY_CHART_TYPES.map((t) => sql`${t}`),
-      sql`, `,
-    )})
+    (
+      COALESCE(widget->>'connectionId', '') = ''
+      AND widget->>'chartType' NOT IN (${sql.join(
+        CONTENT_ONLY_CHART_TYPES.map((t) => sql`${t}`),
+        sql`, `,
+      )})
+    )
   `;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/lib/db/connection-reassign.ts` around lines 94 - 105, Update
widgetMatchesSource so the empty fromConnectionId branch wraps its combined
COALESCE and chart-type exclusion predicates in an enclosing parenthesized SQL
expression, keeping both conditions grouped as one self-contained fragment while
leaving the non-empty branch unchanged.
app/e2e/dashboard-connection-reassign.spec.ts (1)

85-96: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace any with the Playwright Page type.

The coding guidelines require a comment that explains every any. The eslint-disable line suppresses the rule but does not state a reason. Page is already available from @playwright/test, so the annotation is not needed.

♻️ Proposed change
-const openCardMenu = async (
-  // eslint-disable-next-line `@typescript-eslint/no-explicit-any`
-  page: any,
-  name: string,
-) => {
+const openCardMenu = async (page: Page, name: string) => {

Add the type import:

+import type { Page } from "`@playwright/test`";

As per coding guidelines: "Use TypeScript strict mode and do not use any without a comment explaining why."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/e2e/dashboard-connection-reassign.spec.ts` around lines 85 - 96, Update
the openCardMenu helper to use Playwright’s Page type for its page parameter,
importing Page from `@playwright/test`. Remove the unnecessary eslint-disable
directive and any annotation while preserving the existing menu interaction.

Source: Coding guidelines

app/src/app/api/dashboards/import/__tests__/route.test.ts (1)

403-420: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a status assertion to the mixed-case test.

The other two new tests assert the response status. This one reads body.data.unassignedWidgetCount without checking that the import succeeded, so a validation regression surfaces as a confusing property-access failure rather than a status mismatch.

💚 Proposed change
     const body = await res.json();
+    expect(res.status).toBe(201);
     // w1 was skipped; w2/w3 are markdown+iframe and never wanted a connection.
     expect(body.data.unassignedWidgetCount).toBe(1);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/app/api/dashboards/import/__tests__/route.test.ts` around lines 403 -
420, Add a response status assertion in the mixed-case test before reading
body.data.unassignedWidgetCount, matching the success-status assertions in the
neighboring import tests. Keep the existing unassignedWidgetCount assertion
unchanged.
app/src/app/api/dashboards/[id]/reassign-connection/__tests__/route.test.ts (1)

173-181: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

This test cannot prove the visibility guarantee it names.

The where clause is mocked away, so makeSelectChain([]) only re-tests the same "target lookup returned no row" path as the test on lines 159-168. It does not exercise the owner/visibility='shared'/admin predicate. Consider asserting on the built predicate instead, or cover the visibility rule in the DB-level test suite.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/app/api/dashboards/`[id]/reassign-connection/__tests__/route.test.ts
around lines 173 - 181, The private-connection test currently mocks away the
visibility predicate and duplicates the missing-target case. Update the test
around POST and makeSelectChain to verify the owner/shared/admin filtering
predicate, or move this visibility guarantee to the database-level test suite,
while keeping the 404 response and no-widget-reassignment assertions.
app/src/components/dashboard-edit-toolbar.tsx (1)

89-104: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the shared Filters toggle button. Both new toolbars contain the same parameter-toggle button with the same aria-label logic and the same arbitrary text-[10px] count badge. One shared component removes the copy and keeps the badge styling consistent.

  • app/src/components/dashboard-edit-toolbar.tsx#L89-L104: replace this block with the shared ParameterToggleButton and pass hasParameters, parameterCount, showParameterBar, and onToggleParameterBar.
  • app/src/components/dashboard-view-toolbar.tsx#L130-L145: replace this identical block with the same shared component, and move the count badge styling into it so both toolbars use one token set.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/dashboard-edit-toolbar.tsx` around lines 89 - 104, Extract
a shared ParameterToggleButton component containing the Filters toggle,
aria-label logic, and count badge styling. In
app/src/components/dashboard-edit-toolbar.tsx lines 89-104 and
app/src/components/dashboard-view-toolbar.tsx lines 130-145, replace the
duplicated blocks with ParameterToggleButton, passing hasParameters,
parameterCount, showParameterBar, and onToggleParameterBar; move the badge
styling into the shared component.
app/e2e/edit-page-preservation.spec.ts (2)

42-49: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Check the layout PUT response.

If this request fails, the test fails later at an unrelated visibility assertion. The createTestDashboard fixture in app/e2e/fixtures.ts already throws on a non-OK response. Apply the same check here for a clear failure message.

♻️ Proposed fix
-      await page.request.put(`/api/dashboards/${id}`, {
+      const res = await page.request.put(`/api/dashboards/${id}`, {
         data: {
           layoutJson: {
             version: 2,
             pages: [1, 2, 3, 4].map(pageWith),
           },
         },
       });
+      expect(res.ok(), `layout PUT failed: ${res.status()}`).toBe(true);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/e2e/edit-page-preservation.spec.ts` around lines 42 - 49, Check the
response returned by the layout PUT request in the edit-page preservation test
and fail immediately with the same non-OK response handling used by
createTestDashboard in the fixture. Preserve the existing request payload and
make the response validation occur before subsequent visibility assertions.

110-118: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the upper clamp too.

The comment states that 99 clamps to the last page. The loop only proves some tab is selected. Add the name-level assertion for Page 4 so a regression that clamps 99 to the first page still fails.

♻️ Proposed addition
       await page.goto(`/${id}/edit?page=abc`);
       await expect(
         page.getByRole("tab", { name: "Page 1", selected: true }),
       ).toBeVisible({ timeout: 15_000 });
+      await page.goto(`/${id}/edit?page=99`);
+      await expect(
+        page.getByRole("tab", { name: "Page 4", selected: true }),
+      ).toBeVisible({ timeout: 15_000 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/e2e/edit-page-preservation.spec.ts` around lines 110 - 118, Update the
pagination assertions in the edit-page preservation test to verify that
navigating with page=99 selects the last page, specifically the “Page 4” tab.
Keep the existing assertions for -1 and abc resolving to “Page 1” unchanged, and
ensure the upper-clamp case checks the tab name rather than only selected-tab
visibility.

Source: Coding guidelines

app/src/components/__tests__/dashboard-connection-dialog.test.tsx (2)

137-139: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Explain each any, or drop it.

The coding guidelines require a comment that explains why any is used. The eslint-disable lines suppress the rule but do not state a reason. Either add the reason, or type the fixtures as Partial<DashboardDetail> (already the declared type of mockDashboard) and the override map as Partial<typeof props>, which removes the need for any.

Also applies to: 150-151

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/__tests__/dashboard-connection-dialog.test.tsx` around
lines 137 - 139, Update the renderDialog test helper and the related override
fixture at the additional location to avoid unexplained any usage: type
dashboard fixtures as Partial<DashboardDetail> and override maps as
Partial<typeof props>, removing the eslint-disable comments while preserving the
existing render behavior.

Source: Coding guidelines


203-206: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The assertion does not prove the test name.

The name claims the dashboard query is not fired. The body only asserts that the title is absent. Add a spy on the mocked useDashboard and assert it received an empty id, or rename the test to describe what it checks.

♻️ Proposed change
+const useDashboardSpy = vi.fn();
 vi.mock("`@/hooks/use-dashboards`", () => ({
-  useDashboard: (id: string) => ({
+  useDashboard: (id: string) => (useDashboardSpy(id), {
     data: id ? mockDashboard : undefined,
     isLoading: false,
   }),
   it("stays inert while closed so the dashboard query is not fired", () => {
     renderDialog({ open: false, dashboardId: "" });
     expect(screen.queryByText("Change connection")).not.toBeInTheDocument();
+    expect(useDashboardSpy).toHaveBeenCalledWith("");
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/__tests__/dashboard-connection-dialog.test.tsx` around
lines 203 - 206, Update the test “stays inert while closed so the dashboard
query is not fired” to verify its stated behavior by spying on the mocked
useDashboard call and asserting it receives an empty dashboard id when rendered
closed; retain the existing visibility assertion if useful, or rename the test
only if that query assertion cannot be added.
app/src/components/dashboard-view-toolbar.tsx (1)

90-96: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Give feedback when the custom interval is rejected.

handleCustomApply returns silently for a non-numeric value or for a value below 5. The user sees no change and no message. Show a short inline hint, or disable the Set button while the value is invalid.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/dashboard-view-toolbar.tsx` around lines 90 - 96, Update
handleCustomApply to provide user feedback when customSeconds is non-numeric or
below the 5-second minimum, using a short inline hint or disabling the Set
button while invalid. Preserve the existing onApplyInterval, reset, and
dropdown-closing behavior for valid values.
app/src/hooks/__tests__/use-connections.test.ts (1)

67-67: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the new enabled flag.

The call site is correctly migrated to the options object, but no test asserts the new enabled behavior. Add two cases against the mocked useQuery config: enabled defaults to true when no options are passed, and useConnections({ enabled: false }) forwards enabled: false. This is the flag DashboardWorkspace relies on to skip the connections request in view mode.

As per coding guidelines: "Every new behavior, bug fix, and edge case must have a test, written before implementation, following Red → Green → Refactor."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/hooks/__tests__/use-connections.test.ts` at line 67, Add coverage in
the useConnections tests for the new enabled option: assert the mocked useQuery
configuration defaults enabled to true when no options are provided, and assert
useConnections({ enabled: false }) forwards enabled: false. Keep the assertions
focused on the generated query config and follow the existing test structure.

Source: Coding guidelines

component/src/components/composed/dashboard-grid.tsx (1)

132-136: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Give the cols prop a stable identity.

colsForEveryBreakpoint(cols) builds a fresh object on every render, while the sibling layouts prop is memoized and defaultBreakpoints is a module constant. Memoize it for consistency, and to avoid handing ResponsiveGridLayout a new config reference on each render.

♻️ Memoize the column map
   const layouts = React.useMemo(
     () => ({ lg: layout, md: layout, sm: layout, xs: layout }),
     [layout],
   );
+
+  const gridCols = React.useMemo(() => colsForEveryBreakpoint(cols), [cols]);
-          cols={colsForEveryBreakpoint(cols)}
+          cols={gridCols}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@component/src/components/composed/dashboard-grid.tsx` around lines 132 - 136,
Memoize the result of colsForEveryBreakpoint(cols) before rendering
ResponsiveGridLayout, using the existing component memoization pattern and cols
as its dependency. Pass the memoized column map to the cols prop so its object
identity remains stable when cols is unchanged.
app/src/app/(dashboard)/[id]/layout.tsx (1)

20-22: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Derive editMode from the exact segment.

pathname.endsWith("/edit") also matches the view route of a dashboard whose id is literally edit, because that pathname is /edit. IDs are server-generated, so this is unlikely, but an exact comparison removes the ambiguity and expresses the intent directly.

♻️ Compare against the concrete segment
   const { id } = useParams<{ id: string }>();
   const pathname = usePathname();
-  const editMode = pathname.endsWith("/edit");
+  const editMode = pathname === `/${id}/edit`;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/app/`(dashboard)/[id]/layout.tsx around lines 20 - 22, Update the
editMode derivation in the dashboard layout to use an exact pathname comparison
with the edit route, rather than pathname.endsWith("/edit"). Preserve editMode
as true only for the concrete edit pathname and false when the dashboard id
itself is "edit".
app/src/components/__tests__/dashboard-workspace.test.tsx (1)

897-901: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the positive Cmd+S case.

The test name says "saves in edit mode only", but the body only pins the view-mode negative. No test fires Cmd+S in edit mode, so a regression that disables the shortcut wiring (for example a wrong disabled expression at dashboard-workspace.tsx Line 581) still passes. The Save button test at Line 663 does not exercise the shortcut path.

💚 Proposed addition
   it("Cmd+S saves in edit mode only", () => {
     render(<DashboardWorkspace id="d1" editMode={false} />);
     fireEvent.keyDown(document, { key: "s", metaKey: true });
     expect(mockMutateAsync).not.toHaveBeenCalled();
   });
+
+  it("Cmd+S saves in edit mode", async () => {
+    pathname = "/d1/edit";
+    render(<DashboardWorkspace id="d1" editMode={true} />);
+    fireEvent.keyDown(document, { key: "s", metaKey: true });
+    await vi.waitFor(() =>
+      expect(mockMutateAsync).toHaveBeenCalledWith(
+        expect.objectContaining({ id: "d1", expectedVersion: 1 }),
+      ),
+    );
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/__tests__/dashboard-workspace.test.tsx` around lines 897 -
901, Extend the “Cmd+S saves in edit mode only” test to render
DashboardWorkspace with editMode enabled, fire the same Cmd+S key event, and
assert mockMutateAsync is called. Keep the existing view-mode assertion so both
enabled and disabled shortcut behavior are covered.
app/src/lib/shared/url-params.ts (1)

93-94: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the stray ponytail: marker from the comment.

The explanation itself is useful. The ponytail: prefix reads as an internal scratch marker and has no meaning to a future reader.

♻️ Proposed tweak
-        // ponytail: add every companion key regardless of parameterType —
+        // Add every companion key regardless of parameterType —
         // a `select` simply never writes them, so the extra entries are inert.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/lib/shared/url-params.ts` around lines 93 - 94, Update the comment
near the companion-key handling to remove the stray “ponytail:” prefix while
preserving the explanation about adding every companion key regardless of
parameterType.
app/src/stores/__tests__/dashboard-store.test.ts (1)

63-86: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the upper clamp and the NaN branch.

setLayout clamps both ends and has an explicit isNaN guard (dashboard-store.ts Line 104). The new tests only pin the lower bound and the zero case. Add an above-range index and a NaN index so the other two branches cannot regress silently.

💚 Proposed addition
+  it("setLayout clamps an initialPageIndex past the last page", () => {
+    const newLayout = {
+      version: 2 as const,
+      pages: [
+        { id: "p1", title: "A", widgets: [], gridLayout: [] },
+        { id: "p2", title: "B", widgets: [], gridLayout: [] },
+      ],
+    };
+    useDashboardStore.getState().setLayout(newLayout, 99);
+    expect(useDashboardStore.getState().activePageIndex).toBe(1);
+  });
+
+  it("setLayout treats a NaN initialPageIndex as 0", () => {
+    const newLayout = {
+      version: 2 as const,
+      pages: [{ id: "p1", title: "A", widgets: [], gridLayout: [] }],
+    };
+    useDashboardStore.getState().setLayout(newLayout, Number.NaN);
+    expect(useDashboardStore.getState().activePageIndex).toBe(0);
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/stores/__tests__/dashboard-store.test.ts` around lines 63 - 86, Add
tests for setLayout covering an initialPageIndex above the available page range,
asserting activePageIndex clamps to the last page, and a NaN initialPageIndex,
asserting it follows the explicit NaN fallback behavior. Keep the existing
negative and zero cases unchanged.
app/e2e/parameter-url-sync.spec.ts (1)

11-26: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move the seeded connection id into fixtures.ts.

fixtures.ts already exports shared seed constants such as ALICE, TEST_NEO4J_BOLT_URL, and TEST_PG_PORT. Add the Neo4j conn-neo4j-001 constant there and reuse it from paramWidget so seed-ID changes stay central.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/e2e/parameter-url-sync.spec.ts` around lines 11 - 26, Move the seeded
Neo4j connection ID into the shared constants in fixtures.ts, alongside ALICE,
TEST_NEO4J_BOLT_URL, and TEST_PG_PORT, then import and reuse that constant in
paramWidget instead of the inline "conn-neo4j-001" value.
app/src/components/dashboard-workspace.tsx (1)

317-353: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Send expectedVersion with the auto-refresh settings write.

applyInterval sends {...serverLayout, settings: newSettings} without expectedVersion, so the API does not apply the optimistic-lock guard. If another save changes the dashboard after serverLayout was loaded, this write overwrites that layout.

Forward expectedVersion: dashboard?.version and handle the resulting conflict through the existing save-error toast path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/dashboard-workspace.tsx` around lines 317 - 353, Update
applyInterval to include expectedVersion: dashboard?.version in the
updateDashboard payload, using the current dashboard version for optimistic
locking. Preserve the existing persist queue and route mutation failures,
including version conflicts, through the existing classifySaveError toast path.
app/e2e/editor-maximize.spec.ts (1)

31-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Type the test-only CodeMirror property.

The lint suppression identifies the rule but does not explain why HTMLElement requires any. Model __cmView with a local structural type, or document a specific reason if that type cannot represent the runtime shape. Verify the property shape against the active CodeMirror wrapper.

As per coding guidelines, do not use any without a comment explaining why.

Proposed type-safe replacement
+type CodeMirrorHost = HTMLElement & {
+  __cmView?: { state: { doc: { length: number } } };
+};
+
 function docLength(editor: import("`@playwright/test`").Locator) {
   return editor.evaluate(
-    // eslint-disable-next-line `@typescript-eslint/no-explicit-any`
-    (el: HTMLElement) => (el as any).__cmView?.state.doc.length ?? -1,
+    (el: HTMLElement) =>
+      (el as CodeMirrorHost).__cmView?.state.doc.length ?? -1,
   );
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/e2e/editor-maximize.spec.ts` around lines 31 - 36, Update docLength to
replace the eslint-suppressed any cast with a local structural type matching the
active CodeMirror wrapper’s __cmView property and its state.doc.length shape.
Access the typed property from the HTMLElement while preserving the existing -1
fallback, and verify the modeled runtime shape against the wrapper.

Source: Coding guidelines

app/src/components/__tests__/widget-editor-modal-maximize.test.tsx (1)

252-266: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the parameter-select exit branch.

isParamSelect is an independent maximize guard from isContentOnly. This test switches only to "markdown". Add a test that maximizes the editor, switches to "parameter-select", and verifies that the preview mounts and the grid returns to two columns.

As per coding guidelines, every new behavior, bug fix, and edge case must have a test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/__tests__/widget-editor-modal-maximize.test.tsx` around
lines 252 - 266, Add a dedicated test alongside the existing maximize behavior
test that expands the editor, switches the chart type to "parameter-select" via
useWidgetEditorStore, and verifies widget-preview is rendered and gridColumns()
returns the two-column layout. Keep the setup and assertions consistent with the
existing markdown test.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.claude/CLAUDE.md:
- Around line 22-24: Update the review:local description in the command list to
reference release/1.5, matching the script configuration in package.json instead
of release/1.4.

In `@app/e2e/grid.spec.ts`:
- Around line 187-204: The drag in the test must produce a real grid layout
change before clicking Save. Update the drag coordinates in the flow around
item, boundingBox, and page.mouse.move so the movement exceeds half a column and
changes gridLayout, preserving the existing drag-and-save assertions.

In `@app/src/components/dashboard-view-toolbar.tsx`:
- Line 123: Update the Badge rendering in the dashboard toolbar so it is
conditionally rendered only when the optional role value is set; preserve the
existing secondary variant and role text when a value is available.

In `@app/src/components/dashboard-workspace.tsx`:
- Around line 796-806: Update the DashboardContainer props in the page-rendering
block so refetchInterval is enabled only when isActive; preserve editMode’s
false interval behavior and pass viewRefetchInterval only for the active page,
matching the existing onLayoutChange gating.

In `@app/src/components/lazy-visible.tsx`:
- Around line 84-88: Update the useEffect claim flow in
app/src/components/lazy-visible.tsx: after claimSlot(slot), immediately invoke
the existing eviction path or enforce the budget within claimSlot, while
preserving cleanup via dropSlot(slot). Add the regression in
app/e2e/heavy-widgets.spec.ts to leave at-budget slots off-screen, claim a
replacement above the budget, and verify that the over-budget eviction occurs.

In `@app/src/lib/api/openapi-spec.ts`:
- Around line 365-384: Update the reassign-connection endpoint response schema
to wrap dashboardsUpdated and widgetsReassigned under a data object, matching
apiSuccess’s { data, error, meta } envelope and the API-key endpoint style. Add
the missing 500 response using R.serverError alongside the existing 400–404
responses.

---

Nitpick comments:
In `@app/e2e/dashboard-connection-reassign.spec.ts`:
- Around line 85-96: Update the openCardMenu helper to use Playwright’s Page
type for its page parameter, importing Page from `@playwright/test`. Remove the
unnecessary eslint-disable directive and any annotation while preserving the
existing menu interaction.

In `@app/e2e/edit-page-preservation.spec.ts`:
- Around line 42-49: Check the response returned by the layout PUT request in
the edit-page preservation test and fail immediately with the same non-OK
response handling used by createTestDashboard in the fixture. Preserve the
existing request payload and make the response validation occur before
subsequent visibility assertions.
- Around line 110-118: Update the pagination assertions in the edit-page
preservation test to verify that navigating with page=99 selects the last page,
specifically the “Page 4” tab. Keep the existing assertions for -1 and abc
resolving to “Page 1” unchanged, and ensure the upper-clamp case checks the tab
name rather than only selected-tab visibility.

In `@app/e2e/editor-maximize.spec.ts`:
- Around line 31-36: Update docLength to replace the eslint-suppressed any cast
with a local structural type matching the active CodeMirror wrapper’s __cmView
property and its state.doc.length shape. Access the typed property from the
HTMLElement while preserving the existing -1 fallback, and verify the modeled
runtime shape against the wrapper.

In `@app/e2e/parameter-url-sync.spec.ts`:
- Around line 11-26: Move the seeded Neo4j connection ID into the shared
constants in fixtures.ts, alongside ALICE, TEST_NEO4J_BOLT_URL, and
TEST_PG_PORT, then import and reuse that constant in paramWidget instead of the
inline "conn-neo4j-001" value.

In `@app/src/app/`(dashboard)/[id]/layout.tsx:
- Around line 20-22: Update the editMode derivation in the dashboard layout to
use an exact pathname comparison with the edit route, rather than
pathname.endsWith("/edit"). Preserve editMode as true only for the concrete edit
pathname and false when the dashboard id itself is "edit".

In `@app/src/app/api/dashboards/`[id]/reassign-connection/__tests__/route.test.ts:
- Around line 173-181: The private-connection test currently mocks away the
visibility predicate and duplicates the missing-target case. Update the test
around POST and makeSelectChain to verify the owner/shared/admin filtering
predicate, or move this visibility guarantee to the database-level test suite,
while keeping the 404 response and no-widget-reassignment assertions.

In `@app/src/app/api/dashboards/import/__tests__/route.test.ts`:
- Around line 403-420: Add a response status assertion in the mixed-case test
before reading body.data.unassignedWidgetCount, matching the success-status
assertions in the neighboring import tests. Keep the existing
unassignedWidgetCount assertion unchanged.

In `@app/src/components/__tests__/dashboard-connection-dialog.test.tsx`:
- Around line 137-139: Update the renderDialog test helper and the related
override fixture at the additional location to avoid unexplained any usage: type
dashboard fixtures as Partial<DashboardDetail> and override maps as
Partial<typeof props>, removing the eslint-disable comments while preserving the
existing render behavior.
- Around line 203-206: Update the test “stays inert while closed so the
dashboard query is not fired” to verify its stated behavior by spying on the
mocked useDashboard call and asserting it receives an empty dashboard id when
rendered closed; retain the existing visibility assertion if useful, or rename
the test only if that query assertion cannot be added.

In `@app/src/components/__tests__/dashboard-workspace.test.tsx`:
- Around line 897-901: Extend the “Cmd+S saves in edit mode only” test to render
DashboardWorkspace with editMode enabled, fire the same Cmd+S key event, and
assert mockMutateAsync is called. Keep the existing view-mode assertion so both
enabled and disabled shortcut behavior are covered.

In `@app/src/components/__tests__/widget-editor-modal-maximize.test.tsx`:
- Around line 252-266: Add a dedicated test alongside the existing maximize
behavior test that expands the editor, switches the chart type to
"parameter-select" via useWidgetEditorStore, and verifies widget-preview is
rendered and gridColumns() returns the two-column layout. Keep the setup and
assertions consistent with the existing markdown test.

In `@app/src/components/dashboard-edit-toolbar.tsx`:
- Around line 89-104: Extract a shared ParameterToggleButton component
containing the Filters toggle, aria-label logic, and count badge styling. In
app/src/components/dashboard-edit-toolbar.tsx lines 89-104 and
app/src/components/dashboard-view-toolbar.tsx lines 130-145, replace the
duplicated blocks with ParameterToggleButton, passing hasParameters,
parameterCount, showParameterBar, and onToggleParameterBar; move the badge
styling into the shared component.

In `@app/src/components/dashboard-view-toolbar.tsx`:
- Around line 90-96: Update handleCustomApply to provide user feedback when
customSeconds is non-numeric or below the 5-second minimum, using a short inline
hint or disabling the Set button while invalid. Preserve the existing
onApplyInterval, reset, and dropdown-closing behavior for valid values.

In `@app/src/components/dashboard-workspace.tsx`:
- Around line 317-353: Update applyInterval to include expectedVersion:
dashboard?.version in the updateDashboard payload, using the current dashboard
version for optimistic locking. Preserve the existing persist queue and route
mutation failures, including version conflicts, through the existing
classifySaveError toast path.

In `@app/src/hooks/__tests__/use-connections.test.ts`:
- Line 67: Add coverage in the useConnections tests for the new enabled option:
assert the mocked useQuery configuration defaults enabled to true when no
options are provided, and assert useConnections({ enabled: false }) forwards
enabled: false. Keep the assertions focused on the generated query config and
follow the existing test structure.

In `@app/src/lib/db/connection-reassign.ts`:
- Around line 94-105: Update widgetMatchesSource so the empty fromConnectionId
branch wraps its combined COALESCE and chart-type exclusion predicates in an
enclosing parenthesized SQL expression, keeping both conditions grouped as one
self-contained fragment while leaving the non-empty branch unchanged.

In `@app/src/lib/shared/url-params.ts`:
- Around line 93-94: Update the comment near the companion-key handling to
remove the stray “ponytail:” prefix while preserving the explanation about
adding every companion key regardless of parameterType.

In `@app/src/stores/__tests__/dashboard-store.test.ts`:
- Around line 63-86: Add tests for setLayout covering an initialPageIndex above
the available page range, asserting activePageIndex clamps to the last page, and
a NaN initialPageIndex, asserting it follows the explicit NaN fallback behavior.
Keep the existing negative and zero cases unchanged.

In `@component/src/components/composed/dashboard-grid.tsx`:
- Around line 132-136: Memoize the result of colsForEveryBreakpoint(cols) before
rendering ResponsiveGridLayout, using the existing component memoization pattern
and cols as its dependency. Pass the memoized column map to the cols prop so its
object identity remains stable when cols is unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1cb95f87-119e-4372-8629-33d121918670

📥 Commits

Reviewing files that changed from the base of the PR and between c84c594 and b2717ec.

📒 Files selected for processing (85)
  • .claude/CLAUDE.md
  • .claude/agents/project-architect.md
  • .claude/hooks/check-boundaries.sh
  • .claude/hooks/check-migration-guard.sh
  • .claude/skills/next/SKILL.md
  • .github/workflows/ci.yml
  • .gitignore
  • CHANGELOG.md
  • app/e2e/charts.spec.ts
  • app/e2e/dashboard-connection-reassign.spec.ts
  • app/e2e/edit-page-preservation.spec.ts
  • app/e2e/edit-scroll-position.spec.ts
  • app/e2e/editor-maximize.spec.ts
  • app/e2e/fixtures.ts
  • app/e2e/grid.spec.ts
  • app/e2e/heavy-widgets.spec.ts
  • app/e2e/parameter-url-sync.spec.ts
  • app/e2e/styling-rules.spec.ts
  • app/playwright.config.ts
  • app/src/__tests__/test-environment-boundary.test.ts
  • app/src/app/(dashboard)/[id]/__tests__/layout.test.tsx
  • app/src/app/(dashboard)/[id]/edit/page.tsx
  • app/src/app/(dashboard)/[id]/layout.tsx
  • app/src/app/(dashboard)/[id]/page.tsx
  • app/src/app/(dashboard)/page.tsx
  • app/src/app/api/connections/[id]/reassign/__tests__/route.test.ts
  • app/src/app/api/connections/[id]/reassign/route.ts
  • app/src/app/api/dashboards/[id]/reassign-connection/__tests__/route.test.ts
  • app/src/app/api/dashboards/[id]/reassign-connection/route.ts
  • app/src/app/api/dashboards/import/__tests__/route.test.ts
  • app/src/app/api/dashboards/import/route.ts
  • app/src/components/__tests__/dashboard-connection-dialog.test.tsx
  • app/src/components/__tests__/dashboard-workspace.test.tsx
  • app/src/components/__tests__/lazy-visible.test.tsx
  • app/src/components/__tests__/widget-editor-modal-maximize.test.tsx
  • app/src/components/dashboard-connection-dialog.tsx
  • app/src/components/dashboard-edit-toolbar.tsx
  • app/src/components/dashboard-view-toolbar.tsx
  • app/src/components/dashboard-workspace.tsx
  • app/src/components/lazy-visible.tsx
  • app/src/components/widget-editor-modal.tsx
  • app/src/components/widget-editor/__tests__/query-editor-panel.test.tsx
  • app/src/components/widget-editor/__tests__/widget-preview-panel.test.tsx
  • app/src/components/widget-editor/query-editor-panel.tsx
  • app/src/components/widget-editor/widget-preview-panel.tsx
  • app/src/hooks/__tests__/use-connections.test.ts
  • app/src/hooks/use-connections.ts
  • app/src/hooks/use-dashboards.ts
  • app/src/hooks/use-widget-templates.ts
  • app/src/lib/__tests__/db/connection-reassign.test.ts
  • app/src/lib/__tests__/docs-accuracy.test.ts
  • app/src/lib/__tests__/shared/url-params.test.ts
  • app/src/lib/api/openapi-spec.ts
  • app/src/lib/dashboard/__tests__/import-follow-up.test.ts
  • app/src/lib/dashboard/import-follow-up.ts
  • app/src/lib/db/__tests__/tenant-scope.test.ts
  • app/src/lib/db/connection-reassign.ts
  • app/src/lib/shared/url-params.ts
  • app/src/lib/widget/__tests__/scroll-to-widget.test.ts
  • app/src/lib/widget/__tests__/scroll-to-widget.test.tsx
  • app/src/lib/widget/__tests__/webgl-budget.test.ts
  • app/src/lib/widget/content-only-chart.ts
  • app/src/lib/widget/scroll-to-widget.ts
  • app/src/lib/widget/webgl-budget.ts
  • app/src/plugins/graph/component.tsx
  • app/src/stores/__tests__/dashboard-store.test.ts
  • app/src/stores/dashboard-store.ts
  • cli/src/__tests__/commands/status.test.ts
  • cli/src/__tests__/lib/docker.test.ts
  • cli/src/commands/status.ts
  • cli/src/lib/docker.ts
  • component/design-tokens.css
  • component/src/__tests__/reduced-motion.test.ts
  • component/src/charts/__tests__/map-chart.test.tsx
  • component/src/charts/map-chart.tsx
  • component/src/components/composed/__tests__/dashboard-grid.test.tsx
  • component/src/components/composed/dashboard-grid.tsx
  • component/src/components/ui/__tests__/alert-dialog.test.tsx
  • component/src/components/ui/__tests__/dialog.test.tsx
  • component/src/components/ui/alert-dialog.tsx
  • component/src/components/ui/dialog.tsx
  • component/stories/ui/alert-dialog.stories.tsx
  • component/stories/ui/animation-centring.ts
  • component/stories/ui/dialog.stories.tsx
  • package.json

Comment thread app/e2e/grid.spec.ts
Comment on lines +187 to +204
// Nudge the left widget by less than half a column so the drag registers
// (threshold is 3px) without intentionally moving anything: any layout
// change from here is the grid's own doing, not the user's.
const handle = item(page, "left").locator(".drag-handle");
const grip = (await handle.boundingBox())!;
const [cx, cy] = [grip.x + grip.width / 2, grip.y + grip.height / 2];
await page.mouse.move(cx, cy);
await page.mouse.down();
await page.mouse.move(cx + 12, cy, { steps: 6 });
await page.mouse.up();

const saved = page.waitForResponse(
(r) =>
r.url().includes(`/api/dashboards/${id}`) &&
r.request().method() === "PUT",
);
await page.getByRole("button", { name: "Save" }).click();
expect((await saved).ok(), "the save request failed").toBe(true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Is the store dirtied by an identical grid layout, and is Save gated on dirty state?
ast-grep outline app/src/stores/dashboard-store.ts --items all
rg -nP -C 8 'updateGridLayout|hasUnsavedChanges' app/src/stores/dashboard-store.ts
rg -nP -C 6 'name: "Save"|>Save<|disabled=' app/src/components/dashboard-edit-toolbar.tsx

Repository: alfredo1996/neoboard

Length of output: 3491


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== dashboard store relevant lines =="
sed -n '1,150p' app/src/stores/dashboard-store.ts
sed -n '210,270p' app/src/stores/dashboard-store.ts

echo "== grid spec relevant lines =="
sed -n '1,240p' app/e2e/grid.spec.ts

echo "== dashboard edit toolbar Save search =="
rg -n -C 8 'Save|hasUnsavedChanges|onClick|disabled|form' app/src/components dashboard-edit-toolbar.tsx app/src 2>/dev/null || true

Repository: alfredo1996/neoboard

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== exact dashboard store relevant lines =="
sed -n '1,140p' app/src/stores/dashboard-store.ts
sed -n '230,260p' app/src/stores/dashboard-store.ts

echo "== exact grid spec relevant lines =="
sed -n '160,220p' app/e2e/grid.spec.ts

echo "== files containing dashboard-edit-toolbar or grid workspace components =="
rg -n "dashboard-edit-toolbar|dashboard-workspace|ReactGridLayout|onLayoutChange|onDragStop|updateGridLayout|hasUnsavedChanges" app/src -g '*.{tsx,ts}' | head -n 120

Repository: alfredo1996/neoboard

Length of output: 16317


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== dashboard workspace save/unsaved and grid handlers =="
sed -n '120,175p' app/src/components/dashboard-workspace.tsx
sed -n '160,170p' app/src/components/dashboard-workspace.tsx
sed -n '788,830p' app/src/components/dashboard-workspace.tsx

echo "== dashboard edit toolbar save button =="
rg -n -C 12 'Save|hasUnsavedChanges|onClick|disabled|type="button"' app/src/components/dashboard-edit-toolbar.tsx

echo "== dashboard edit toolbar file =="
sed -n '1,180p' app/src/components/dashboard-edit-toolbar.tsx

Repository: alfredo1996/neoboard

Length of output: 12726


Drag far enough to register a real grid change before saving.

The small nudge still leaves the active gridLayout equal to the previous layout, so updateGridLayout returns the store unchanged. The edit toolbar is not gated on hasUnsavedChanges, but waitForResponse can still time out because the save handler is no-op when the store layout did not change. Move the mouse more than half a column, or mark the grid drag as an edit even when snap/normalize produces the same positions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/e2e/grid.spec.ts` around lines 187 - 204, The drag in the test must
produce a real grid layout change before clicking Save. Update the drag
coordinates in the flow around item, boundingBox, and page.mouse.move so the
movement exceeds half a column and changes gridLayout, preserving the existing
drag-and-save assertions.

</ToolbarSection>
<ToolbarSection className="flex-1">
<h1 className="text-lg font-bold">{name}</h1>
<Badge variant="secondary">{role}</Badge>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not render an empty badge.

role is optional. When it is undefined, this renders a badge with no text. Render the badge only when role is set.

🐛 Proposed fix
-        <Badge variant="secondary">{role}</Badge>
+        {role && <Badge variant="secondary">{role}</Badge>}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Badge variant="secondary">{role}</Badge>
{role && <Badge variant="secondary">{role}</Badge>}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/dashboard-view-toolbar.tsx` at line 123, Update the Badge
rendering in the dashboard toolbar so it is conditionally rendered only when the
optional role value is set; preserve the existing secondary variant and role
text when a value is available.

Comment on lines +796 to +806
if (!isActive && !visitedPages.has(index)) return null;
return (
<div
key={page.id}
className={isActive ? undefined : "hidden"}
aria-hidden={!isActive}
>
<DashboardContainer
page={page}
editable={editMode}
refetchInterval={editMode ? false : viewRefetchInterval}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Stop auto-refresh polling on hidden pages.

Visited pages stay mounted and are only visually hidden (Line 796 and Line 800). Line 806 passes viewRefetchInterval to every mounted page, so each visited page keeps refetching its widget queries in the background. After a user visits several pages, auto-refresh multiplies concurrent queries for content nobody can see.

Gate the interval on isActive, the same way onLayoutChange is gated at Line 813.

⚡ Proposed fix
                 <DashboardContainer
                   page={page}
                   editable={editMode}
-                  refetchInterval={editMode ? false : viewRefetchInterval}
+                  refetchInterval={
+                    editMode || !isActive ? false : viewRefetchInterval
+                  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!isActive && !visitedPages.has(index)) return null;
return (
<div
key={page.id}
className={isActive ? undefined : "hidden"}
aria-hidden={!isActive}
>
<DashboardContainer
page={page}
editable={editMode}
refetchInterval={editMode ? false : viewRefetchInterval}
if (!isActive && !visitedPages.has(index)) return null;
return (
<div
key={page.id}
className={isActive ? undefined : "hidden"}
aria-hidden={!isActive}
>
<DashboardContainer
page={page}
editable={editMode}
refetchInterval={
editMode || !isActive ? false : viewRefetchInterval
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/dashboard-workspace.tsx` around lines 796 - 806, Update
the DashboardContainer props in the page-rendering block so refetchInterval is
enabled only when isActive; preserve editMode’s false interval behavior and pass
viewRefetchInterval only for the active page, matching the existing
onLayoutChange gating.

Comment on lines +84 to +88
useEffect(() => {
if (!visible) return;
claimSlot(slot);
return () => dropSlot(slot);
}, [visible, slot]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate relevant files"
fd -a 'lazy-visible\.tsx|heavy-widgets\.spec\.ts|webgl-budget\.ts|.*budget.*|.*visible.*' . | sed 's#^\./##' | sort | head -200

echo
echo "Inspect lazy-visible outline and relevant lines"
file=$(fd 'lazy-visible\.tsx$' . | head -1)
echo "FILE=$file"
wc -l "$file"
ast-grep outline "$file" --view compact || true
sed -n '1,140p' "$file" | cat -n

echo
echo "Inspect budget implementation"
budget_file=$(fd 'webgl-budget\.ts$' . | head -1)
echo "BUDGET_FILE=$budget_file"
wc -l "$budget_file"
sed -n '1,140p' "$budget_file" | cat -n

echo
echo "Inspect e2e relevant section"
e2e_file=$(fd 'heavy-widgets\.spec\.ts$' . | head -1)
echo "E2E_FILE=$e2e_file"
wc -l "$e2e_file"
sed -n '300,355p' "$e2e_file" | cat -n

Repository: alfredo1996/neoboard

Length of output: 11584


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Search for liveSlotCount usages/tests"
rg -n "liveSlotCount|claimSlot|dropSlot|evictOverBudget|WEBGL_WIDGET_BUDGET|resetSlotRegistry" app/src app/e2e -S

echo
echo "LazyVisible tests"
fd 'lazy-visible\.test\.tsx$' . | while read -r f; do
  echo "FILE=$f"
  wc -l "$f"
  sed -n '1,260p' "$f" | cat -n
done

echo
echo "Budget tests relevant sections"
fd 'webgl-budget\.test\.ts$' . | while read -r f; do
  echo "FILE=$f"
  wc -l "$f"
  sed -n '1,260p' "$f" | cat -n
done

echo
echo "Read-only behavioral model verifier"
python3 - <<'PY'
WEBGL_WIDGET_BUDGET = 8

class Slot:
    def __init__(self, name):
        self.name = name
        self.on_screen = True
        self.ejected = False
    def evict(self):
        self.ejected = True

live = []  # preserves insertion order like Set iteration

def claim_slot(slot):
    if slot not in live:
        live.append(slot)

def drop_slot(slot):
    live.remove(slot)

def evict_over_budget():
    over = len(live) - WEBGL_WIDGET_BUDGET
    idx = 0
    while idx < len(live) and over > 0:
        slot = live[idx]
        if slot.on_screen:
            idx += 1
            continue
        slot_on_screen = slot.on_screen
        live.remove(slot)
        # slot.evict() would happen outside live
        slot.ejected = True
        over -= 1
    return idx

# 2 visible slots at budget
for i in range(WEBGL_WIDGET_BUDGET):
    claim_slot(Slot(f"old-{i}"))
    live[i].on_screen = False

# Simulate existing slots leaving viewport and then replacement visible slot claiming.
for s in live:
    s.on_screen = False

claim_slot(Slot("replacement"))

# In current LazyVisible behavior, claimSlot is not followed by evictOverBudget here.
print({
    "budget": WEBGL_WIDGET_BUDGET,
    "live_before_after_leave_and_claim": len(live),
    "old_offscreen_mounted": sum(not s.on_screen and not s.ejected for s in live),
    "replacement_mounted": any(s.name == "replacement" and not s.ejected for s in live) and not live[-1].ejected,
    # The observed invariant violation: replacement claim happens at or above budget with no eviction.
    "current_claim_enforces_budget": len(live) - WEBGL_WIDGET_BUDGET <= 0 and not any(not s.ejected and not s.on_screen for s in live),
})

# Apply eviction after claim to demonstrate proposed invariant.
replacement = next(s for s in live if s.name == "replacement")
replacement.on_screen = False
claim_slot(Slot("replacement2"))
evicted_count = evict_over_budget()
dead_offscreen_after_proposed = sum(not s.on_screen and s.ejected for s in live)
print({
    "evicted_count_after_claim_and_evict": evicted_count,
    "replacement2_ejected": any(s.name == "replacement2" and s.ejected for s in live),
    "live_size_after_proposed": len(live),
    "dead_offscreen_after_proposed": dead_offscreen_after_proposed,
})
PY

Repository: alfredo1996/neoboard

Length of output: 17412


Enforce the WebGL budget when a slot is claimed.

Current claim-time behavior can hold at or above the budget and keep old off-screen widgets mounted until another observer event calls evictOverBudget(). Run eviction immediately after claimSlot(slot), or enforce the budget inside claimSlot.

Add a regression that first leaves existing at-budget slots off-screen, then claims a replacement slot above the budget and verifies the eviction path.

📍 Affects 2 files
  • app/src/components/lazy-visible.tsx#L84-L88 (this comment)
  • app/e2e/heavy-widgets.spec.ts#L330-L332
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/lazy-visible.tsx` around lines 84 - 88, Update the
useEffect claim flow in app/src/components/lazy-visible.tsx: after
claimSlot(slot), immediately invoke the existing eviction path or enforce the
budget within claimSlot, while preserving cleanup via dropSlot(slot). Add the
regression in app/e2e/heavy-widgets.spec.ts to leave at-budget slots off-screen,
claim a replacement above the budget, and verify that the over-budget eviction
occurs.

Source: Coding guidelines

Comment on lines +365 to +384
responses: {
200: {
description: "Widgets re-assigned",
content: {
"application/json": {
schema: {
type: "object",
properties: {
dashboardsUpdated: { type: "integer" },
widgetsReassigned: { type: "integer" },
},
},
},
},
},
400: R.badRequest,
401: R.unauthorized,
403: R.forbidden,
404: R.notFound,
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document the 500 response and the envelope wrapper.

Two gaps against the route behavior:

  1. The route wraps failures in handleRouteError, which returns 500. The route test at app/src/app/api/dashboards/[id]/reassign-connection/__tests__/route.test.ts lines 284-293 asserts 500. Add 500: R.serverError, as /api/query does on line 472.
  2. The route returns apiSuccess(result), which emits { data, error, meta }. The documented 200 schema places the counts at the top level. Nest them under data, matching the "Envelope containing …" style used for the API-key endpoints.
📘 Proposed change
         responses: {
             description: "Widgets re-assigned",
             content: {
               "application/json": {
                 schema: {
                   type: "object",
                   properties: {
-                    dashboardsUpdated: { type: "integer" },
-                    widgetsReassigned: { type: "integer" },
+                    data: {
+                      type: "object",
+                      properties: {
+                        dashboardsUpdated: { type: "integer" },
+                        widgetsReassigned: { type: "integer" },
+                      },
+                    },
+                    error: { $ref: "`#/components/schemas/EnvelopeError`" },
+                    meta: { type: "object", nullable: true },
                   },
                 },
               },
             },
           },
+          500: R.serverError,
         },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
responses: {
200: {
description: "Widgets re-assigned",
content: {
"application/json": {
schema: {
type: "object",
properties: {
dashboardsUpdated: { type: "integer" },
widgetsReassigned: { type: "integer" },
},
},
},
},
},
400: R.badRequest,
401: R.unauthorized,
403: R.forbidden,
404: R.notFound,
},
responses: {
200: {
description: "Widgets re-assigned",
content: {
"application/json": {
schema: {
type: "object",
properties: {
data: {
type: "object",
properties: {
dashboardsUpdated: { type: "integer" },
widgetsReassigned: { type: "integer" },
},
},
error: { $ref: "`#/components/schemas/EnvelopeError`" },
meta: { type: "object", nullable: true },
},
},
},
},
},
400: R.badRequest,
401: R.unauthorized,
403: R.forbidden,
404: R.notFound,
500: R.serverError,
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/lib/api/openapi-spec.ts` around lines 365 - 384, Update the
reassign-connection endpoint response schema to wrap dashboardsUpdated and
widgetsReassigned under a data object, matching apiSuccess’s { data, error, meta
} envelope and the API-key endpoint style. Add the missing 500 response using
R.serverError alongside the existing 400–404 responses.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 6

🧹 Nitpick comments (20)
app/src/lib/db/connection-reassign.ts (1)

94-105: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Wrap the composite predicate in parentheses.

For an empty source, widgetMatchesSource returns two predicates joined by AND without an enclosing group. All three current call sites place the fragment last in a WHERE or WHEN, so the result is correct today. If a future edit combines the fragment with OR, or appends another predicate, the AND binds unexpectedly and the content-only exclusion is silently lost. Parentheses make the fragment self-contained.

♻️ Proposed change
   return sql`
-    COALESCE(widget->>'connectionId', '') = ''
-    AND widget->>'chartType' NOT IN (${sql.join(
-      CONTENT_ONLY_CHART_TYPES.map((t) => sql`${t}`),
-      sql`, `,
-    )})
+    (
+      COALESCE(widget->>'connectionId', '') = ''
+      AND widget->>'chartType' NOT IN (${sql.join(
+        CONTENT_ONLY_CHART_TYPES.map((t) => sql`${t}`),
+        sql`, `,
+      )})
+    )
   `;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/lib/db/connection-reassign.ts` around lines 94 - 105, Update
widgetMatchesSource so the empty fromConnectionId branch wraps its combined
COALESCE and chart-type exclusion predicates in an enclosing parenthesized SQL
expression, keeping both conditions grouped as one self-contained fragment while
leaving the non-empty branch unchanged.
app/e2e/dashboard-connection-reassign.spec.ts (1)

85-96: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace any with the Playwright Page type.

The coding guidelines require a comment that explains every any. The eslint-disable line suppresses the rule but does not state a reason. Page is already available from @playwright/test, so the annotation is not needed.

♻️ Proposed change
-const openCardMenu = async (
-  // eslint-disable-next-line `@typescript-eslint/no-explicit-any`
-  page: any,
-  name: string,
-) => {
+const openCardMenu = async (page: Page, name: string) => {

Add the type import:

+import type { Page } from "`@playwright/test`";

As per coding guidelines: "Use TypeScript strict mode and do not use any without a comment explaining why."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/e2e/dashboard-connection-reassign.spec.ts` around lines 85 - 96, Update
the openCardMenu helper to use Playwright’s Page type for its page parameter,
importing Page from `@playwright/test`. Remove the unnecessary eslint-disable
directive and any annotation while preserving the existing menu interaction.

Source: Coding guidelines

app/src/app/api/dashboards/import/__tests__/route.test.ts (1)

403-420: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a status assertion to the mixed-case test.

The other two new tests assert the response status. This one reads body.data.unassignedWidgetCount without checking that the import succeeded, so a validation regression surfaces as a confusing property-access failure rather than a status mismatch.

💚 Proposed change
     const body = await res.json();
+    expect(res.status).toBe(201);
     // w1 was skipped; w2/w3 are markdown+iframe and never wanted a connection.
     expect(body.data.unassignedWidgetCount).toBe(1);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/app/api/dashboards/import/__tests__/route.test.ts` around lines 403 -
420, Add a response status assertion in the mixed-case test before reading
body.data.unassignedWidgetCount, matching the success-status assertions in the
neighboring import tests. Keep the existing unassignedWidgetCount assertion
unchanged.
app/src/app/api/dashboards/[id]/reassign-connection/__tests__/route.test.ts (1)

173-181: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

This test cannot prove the visibility guarantee it names.

The where clause is mocked away, so makeSelectChain([]) only re-tests the same "target lookup returned no row" path as the test on lines 159-168. It does not exercise the owner/visibility='shared'/admin predicate. Consider asserting on the built predicate instead, or cover the visibility rule in the DB-level test suite.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/app/api/dashboards/`[id]/reassign-connection/__tests__/route.test.ts
around lines 173 - 181, The private-connection test currently mocks away the
visibility predicate and duplicates the missing-target case. Update the test
around POST and makeSelectChain to verify the owner/shared/admin filtering
predicate, or move this visibility guarantee to the database-level test suite,
while keeping the 404 response and no-widget-reassignment assertions.
app/src/components/dashboard-edit-toolbar.tsx (1)

89-104: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the shared Filters toggle button. Both new toolbars contain the same parameter-toggle button with the same aria-label logic and the same arbitrary text-[10px] count badge. One shared component removes the copy and keeps the badge styling consistent.

  • app/src/components/dashboard-edit-toolbar.tsx#L89-L104: replace this block with the shared ParameterToggleButton and pass hasParameters, parameterCount, showParameterBar, and onToggleParameterBar.
  • app/src/components/dashboard-view-toolbar.tsx#L130-L145: replace this identical block with the same shared component, and move the count badge styling into it so both toolbars use one token set.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/dashboard-edit-toolbar.tsx` around lines 89 - 104, Extract
a shared ParameterToggleButton component containing the Filters toggle,
aria-label logic, and count badge styling. In
app/src/components/dashboard-edit-toolbar.tsx lines 89-104 and
app/src/components/dashboard-view-toolbar.tsx lines 130-145, replace the
duplicated blocks with ParameterToggleButton, passing hasParameters,
parameterCount, showParameterBar, and onToggleParameterBar; move the badge
styling into the shared component.
app/e2e/edit-page-preservation.spec.ts (2)

42-49: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Check the layout PUT response.

If this request fails, the test fails later at an unrelated visibility assertion. The createTestDashboard fixture in app/e2e/fixtures.ts already throws on a non-OK response. Apply the same check here for a clear failure message.

♻️ Proposed fix
-      await page.request.put(`/api/dashboards/${id}`, {
+      const res = await page.request.put(`/api/dashboards/${id}`, {
         data: {
           layoutJson: {
             version: 2,
             pages: [1, 2, 3, 4].map(pageWith),
           },
         },
       });
+      expect(res.ok(), `layout PUT failed: ${res.status()}`).toBe(true);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/e2e/edit-page-preservation.spec.ts` around lines 42 - 49, Check the
response returned by the layout PUT request in the edit-page preservation test
and fail immediately with the same non-OK response handling used by
createTestDashboard in the fixture. Preserve the existing request payload and
make the response validation occur before subsequent visibility assertions.

110-118: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the upper clamp too.

The comment states that 99 clamps to the last page. The loop only proves some tab is selected. Add the name-level assertion for Page 4 so a regression that clamps 99 to the first page still fails.

♻️ Proposed addition
       await page.goto(`/${id}/edit?page=abc`);
       await expect(
         page.getByRole("tab", { name: "Page 1", selected: true }),
       ).toBeVisible({ timeout: 15_000 });
+      await page.goto(`/${id}/edit?page=99`);
+      await expect(
+        page.getByRole("tab", { name: "Page 4", selected: true }),
+      ).toBeVisible({ timeout: 15_000 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/e2e/edit-page-preservation.spec.ts` around lines 110 - 118, Update the
pagination assertions in the edit-page preservation test to verify that
navigating with page=99 selects the last page, specifically the “Page 4” tab.
Keep the existing assertions for -1 and abc resolving to “Page 1” unchanged, and
ensure the upper-clamp case checks the tab name rather than only selected-tab
visibility.

Source: Coding guidelines

app/src/components/__tests__/dashboard-connection-dialog.test.tsx (2)

137-139: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Explain each any, or drop it.

The coding guidelines require a comment that explains why any is used. The eslint-disable lines suppress the rule but do not state a reason. Either add the reason, or type the fixtures as Partial<DashboardDetail> (already the declared type of mockDashboard) and the override map as Partial<typeof props>, which removes the need for any.

Also applies to: 150-151

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/__tests__/dashboard-connection-dialog.test.tsx` around
lines 137 - 139, Update the renderDialog test helper and the related override
fixture at the additional location to avoid unexplained any usage: type
dashboard fixtures as Partial<DashboardDetail> and override maps as
Partial<typeof props>, removing the eslint-disable comments while preserving the
existing render behavior.

Source: Coding guidelines


203-206: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The assertion does not prove the test name.

The name claims the dashboard query is not fired. The body only asserts that the title is absent. Add a spy on the mocked useDashboard and assert it received an empty id, or rename the test to describe what it checks.

♻️ Proposed change
+const useDashboardSpy = vi.fn();
 vi.mock("`@/hooks/use-dashboards`", () => ({
-  useDashboard: (id: string) => ({
+  useDashboard: (id: string) => (useDashboardSpy(id), {
     data: id ? mockDashboard : undefined,
     isLoading: false,
   }),
   it("stays inert while closed so the dashboard query is not fired", () => {
     renderDialog({ open: false, dashboardId: "" });
     expect(screen.queryByText("Change connection")).not.toBeInTheDocument();
+    expect(useDashboardSpy).toHaveBeenCalledWith("");
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/__tests__/dashboard-connection-dialog.test.tsx` around
lines 203 - 206, Update the test “stays inert while closed so the dashboard
query is not fired” to verify its stated behavior by spying on the mocked
useDashboard call and asserting it receives an empty dashboard id when rendered
closed; retain the existing visibility assertion if useful, or rename the test
only if that query assertion cannot be added.
app/src/components/dashboard-view-toolbar.tsx (1)

90-96: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Give feedback when the custom interval is rejected.

handleCustomApply returns silently for a non-numeric value or for a value below 5. The user sees no change and no message. Show a short inline hint, or disable the Set button while the value is invalid.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/dashboard-view-toolbar.tsx` around lines 90 - 96, Update
handleCustomApply to provide user feedback when customSeconds is non-numeric or
below the 5-second minimum, using a short inline hint or disabling the Set
button while invalid. Preserve the existing onApplyInterval, reset, and
dropdown-closing behavior for valid values.
app/src/hooks/__tests__/use-connections.test.ts (1)

67-67: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the new enabled flag.

The call site is correctly migrated to the options object, but no test asserts the new enabled behavior. Add two cases against the mocked useQuery config: enabled defaults to true when no options are passed, and useConnections({ enabled: false }) forwards enabled: false. This is the flag DashboardWorkspace relies on to skip the connections request in view mode.

As per coding guidelines: "Every new behavior, bug fix, and edge case must have a test, written before implementation, following Red → Green → Refactor."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/hooks/__tests__/use-connections.test.ts` at line 67, Add coverage in
the useConnections tests for the new enabled option: assert the mocked useQuery
configuration defaults enabled to true when no options are provided, and assert
useConnections({ enabled: false }) forwards enabled: false. Keep the assertions
focused on the generated query config and follow the existing test structure.

Source: Coding guidelines

component/src/components/composed/dashboard-grid.tsx (1)

132-136: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Give the cols prop a stable identity.

colsForEveryBreakpoint(cols) builds a fresh object on every render, while the sibling layouts prop is memoized and defaultBreakpoints is a module constant. Memoize it for consistency, and to avoid handing ResponsiveGridLayout a new config reference on each render.

♻️ Memoize the column map
   const layouts = React.useMemo(
     () => ({ lg: layout, md: layout, sm: layout, xs: layout }),
     [layout],
   );
+
+  const gridCols = React.useMemo(() => colsForEveryBreakpoint(cols), [cols]);
-          cols={colsForEveryBreakpoint(cols)}
+          cols={gridCols}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@component/src/components/composed/dashboard-grid.tsx` around lines 132 - 136,
Memoize the result of colsForEveryBreakpoint(cols) before rendering
ResponsiveGridLayout, using the existing component memoization pattern and cols
as its dependency. Pass the memoized column map to the cols prop so its object
identity remains stable when cols is unchanged.
app/src/app/(dashboard)/[id]/layout.tsx (1)

20-22: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Derive editMode from the exact segment.

pathname.endsWith("/edit") also matches the view route of a dashboard whose id is literally edit, because that pathname is /edit. IDs are server-generated, so this is unlikely, but an exact comparison removes the ambiguity and expresses the intent directly.

♻️ Compare against the concrete segment
   const { id } = useParams<{ id: string }>();
   const pathname = usePathname();
-  const editMode = pathname.endsWith("/edit");
+  const editMode = pathname === `/${id}/edit`;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/app/`(dashboard)/[id]/layout.tsx around lines 20 - 22, Update the
editMode derivation in the dashboard layout to use an exact pathname comparison
with the edit route, rather than pathname.endsWith("/edit"). Preserve editMode
as true only for the concrete edit pathname and false when the dashboard id
itself is "edit".
app/src/components/__tests__/dashboard-workspace.test.tsx (1)

897-901: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the positive Cmd+S case.

The test name says "saves in edit mode only", but the body only pins the view-mode negative. No test fires Cmd+S in edit mode, so a regression that disables the shortcut wiring (for example a wrong disabled expression at dashboard-workspace.tsx Line 581) still passes. The Save button test at Line 663 does not exercise the shortcut path.

💚 Proposed addition
   it("Cmd+S saves in edit mode only", () => {
     render(<DashboardWorkspace id="d1" editMode={false} />);
     fireEvent.keyDown(document, { key: "s", metaKey: true });
     expect(mockMutateAsync).not.toHaveBeenCalled();
   });
+
+  it("Cmd+S saves in edit mode", async () => {
+    pathname = "/d1/edit";
+    render(<DashboardWorkspace id="d1" editMode={true} />);
+    fireEvent.keyDown(document, { key: "s", metaKey: true });
+    await vi.waitFor(() =>
+      expect(mockMutateAsync).toHaveBeenCalledWith(
+        expect.objectContaining({ id: "d1", expectedVersion: 1 }),
+      ),
+    );
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/__tests__/dashboard-workspace.test.tsx` around lines 897 -
901, Extend the “Cmd+S saves in edit mode only” test to render
DashboardWorkspace with editMode enabled, fire the same Cmd+S key event, and
assert mockMutateAsync is called. Keep the existing view-mode assertion so both
enabled and disabled shortcut behavior are covered.
app/src/lib/shared/url-params.ts (1)

93-94: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the stray ponytail: marker from the comment.

The explanation itself is useful. The ponytail: prefix reads as an internal scratch marker and has no meaning to a future reader.

♻️ Proposed tweak
-        // ponytail: add every companion key regardless of parameterType —
+        // Add every companion key regardless of parameterType —
         // a `select` simply never writes them, so the extra entries are inert.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/lib/shared/url-params.ts` around lines 93 - 94, Update the comment
near the companion-key handling to remove the stray “ponytail:” prefix while
preserving the explanation about adding every companion key regardless of
parameterType.
app/src/stores/__tests__/dashboard-store.test.ts (1)

63-86: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the upper clamp and the NaN branch.

setLayout clamps both ends and has an explicit isNaN guard (dashboard-store.ts Line 104). The new tests only pin the lower bound and the zero case. Add an above-range index and a NaN index so the other two branches cannot regress silently.

💚 Proposed addition
+  it("setLayout clamps an initialPageIndex past the last page", () => {
+    const newLayout = {
+      version: 2 as const,
+      pages: [
+        { id: "p1", title: "A", widgets: [], gridLayout: [] },
+        { id: "p2", title: "B", widgets: [], gridLayout: [] },
+      ],
+    };
+    useDashboardStore.getState().setLayout(newLayout, 99);
+    expect(useDashboardStore.getState().activePageIndex).toBe(1);
+  });
+
+  it("setLayout treats a NaN initialPageIndex as 0", () => {
+    const newLayout = {
+      version: 2 as const,
+      pages: [{ id: "p1", title: "A", widgets: [], gridLayout: [] }],
+    };
+    useDashboardStore.getState().setLayout(newLayout, Number.NaN);
+    expect(useDashboardStore.getState().activePageIndex).toBe(0);
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/stores/__tests__/dashboard-store.test.ts` around lines 63 - 86, Add
tests for setLayout covering an initialPageIndex above the available page range,
asserting activePageIndex clamps to the last page, and a NaN initialPageIndex,
asserting it follows the explicit NaN fallback behavior. Keep the existing
negative and zero cases unchanged.
app/e2e/parameter-url-sync.spec.ts (1)

11-26: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move the seeded connection id into fixtures.ts.

fixtures.ts already exports shared seed constants such as ALICE, TEST_NEO4J_BOLT_URL, and TEST_PG_PORT. Add the Neo4j conn-neo4j-001 constant there and reuse it from paramWidget so seed-ID changes stay central.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/e2e/parameter-url-sync.spec.ts` around lines 11 - 26, Move the seeded
Neo4j connection ID into the shared constants in fixtures.ts, alongside ALICE,
TEST_NEO4J_BOLT_URL, and TEST_PG_PORT, then import and reuse that constant in
paramWidget instead of the inline "conn-neo4j-001" value.
app/src/components/dashboard-workspace.tsx (1)

317-353: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Send expectedVersion with the auto-refresh settings write.

applyInterval sends {...serverLayout, settings: newSettings} without expectedVersion, so the API does not apply the optimistic-lock guard. If another save changes the dashboard after serverLayout was loaded, this write overwrites that layout.

Forward expectedVersion: dashboard?.version and handle the resulting conflict through the existing save-error toast path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/dashboard-workspace.tsx` around lines 317 - 353, Update
applyInterval to include expectedVersion: dashboard?.version in the
updateDashboard payload, using the current dashboard version for optimistic
locking. Preserve the existing persist queue and route mutation failures,
including version conflicts, through the existing classifySaveError toast path.
app/e2e/editor-maximize.spec.ts (1)

31-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Type the test-only CodeMirror property.

The lint suppression identifies the rule but does not explain why HTMLElement requires any. Model __cmView with a local structural type, or document a specific reason if that type cannot represent the runtime shape. Verify the property shape against the active CodeMirror wrapper.

As per coding guidelines, do not use any without a comment explaining why.

Proposed type-safe replacement
+type CodeMirrorHost = HTMLElement & {
+  __cmView?: { state: { doc: { length: number } } };
+};
+
 function docLength(editor: import("`@playwright/test`").Locator) {
   return editor.evaluate(
-    // eslint-disable-next-line `@typescript-eslint/no-explicit-any`
-    (el: HTMLElement) => (el as any).__cmView?.state.doc.length ?? -1,
+    (el: HTMLElement) =>
+      (el as CodeMirrorHost).__cmView?.state.doc.length ?? -1,
   );
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/e2e/editor-maximize.spec.ts` around lines 31 - 36, Update docLength to
replace the eslint-suppressed any cast with a local structural type matching the
active CodeMirror wrapper’s __cmView property and its state.doc.length shape.
Access the typed property from the HTMLElement while preserving the existing -1
fallback, and verify the modeled runtime shape against the wrapper.

Source: Coding guidelines

app/src/components/__tests__/widget-editor-modal-maximize.test.tsx (1)

252-266: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the parameter-select exit branch.

isParamSelect is an independent maximize guard from isContentOnly. This test switches only to "markdown". Add a test that maximizes the editor, switches to "parameter-select", and verifies that the preview mounts and the grid returns to two columns.

As per coding guidelines, every new behavior, bug fix, and edge case must have a test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/__tests__/widget-editor-modal-maximize.test.tsx` around
lines 252 - 266, Add a dedicated test alongside the existing maximize behavior
test that expands the editor, switches the chart type to "parameter-select" via
useWidgetEditorStore, and verifies widget-preview is rendered and gridColumns()
returns the two-column layout. Keep the setup and assertions consistent with the
existing markdown test.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.claude/CLAUDE.md:
- Around line 22-24: Update the review:local description in the command list to
reference release/1.5, matching the script configuration in package.json instead
of release/1.4.

In `@app/e2e/grid.spec.ts`:
- Around line 187-204: The drag in the test must produce a real grid layout
change before clicking Save. Update the drag coordinates in the flow around
item, boundingBox, and page.mouse.move so the movement exceeds half a column and
changes gridLayout, preserving the existing drag-and-save assertions.

In `@app/src/components/dashboard-view-toolbar.tsx`:
- Line 123: Update the Badge rendering in the dashboard toolbar so it is
conditionally rendered only when the optional role value is set; preserve the
existing secondary variant and role text when a value is available.

In `@app/src/components/dashboard-workspace.tsx`:
- Around line 796-806: Update the DashboardContainer props in the page-rendering
block so refetchInterval is enabled only when isActive; preserve editMode’s
false interval behavior and pass viewRefetchInterval only for the active page,
matching the existing onLayoutChange gating.

In `@app/src/components/lazy-visible.tsx`:
- Around line 84-88: Update the useEffect claim flow in
app/src/components/lazy-visible.tsx: after claimSlot(slot), immediately invoke
the existing eviction path or enforce the budget within claimSlot, while
preserving cleanup via dropSlot(slot). Add the regression in
app/e2e/heavy-widgets.spec.ts to leave at-budget slots off-screen, claim a
replacement above the budget, and verify that the over-budget eviction occurs.

In `@app/src/lib/api/openapi-spec.ts`:
- Around line 365-384: Update the reassign-connection endpoint response schema
to wrap dashboardsUpdated and widgetsReassigned under a data object, matching
apiSuccess’s { data, error, meta } envelope and the API-key endpoint style. Add
the missing 500 response using R.serverError alongside the existing 400–404
responses.

---

Nitpick comments:
In `@app/e2e/dashboard-connection-reassign.spec.ts`:
- Around line 85-96: Update the openCardMenu helper to use Playwright’s Page
type for its page parameter, importing Page from `@playwright/test`. Remove the
unnecessary eslint-disable directive and any annotation while preserving the
existing menu interaction.

In `@app/e2e/edit-page-preservation.spec.ts`:
- Around line 42-49: Check the response returned by the layout PUT request in
the edit-page preservation test and fail immediately with the same non-OK
response handling used by createTestDashboard in the fixture. Preserve the
existing request payload and make the response validation occur before
subsequent visibility assertions.
- Around line 110-118: Update the pagination assertions in the edit-page
preservation test to verify that navigating with page=99 selects the last page,
specifically the “Page 4” tab. Keep the existing assertions for -1 and abc
resolving to “Page 1” unchanged, and ensure the upper-clamp case checks the tab
name rather than only selected-tab visibility.

In `@app/e2e/editor-maximize.spec.ts`:
- Around line 31-36: Update docLength to replace the eslint-suppressed any cast
with a local structural type matching the active CodeMirror wrapper’s __cmView
property and its state.doc.length shape. Access the typed property from the
HTMLElement while preserving the existing -1 fallback, and verify the modeled
runtime shape against the wrapper.

In `@app/e2e/parameter-url-sync.spec.ts`:
- Around line 11-26: Move the seeded Neo4j connection ID into the shared
constants in fixtures.ts, alongside ALICE, TEST_NEO4J_BOLT_URL, and
TEST_PG_PORT, then import and reuse that constant in paramWidget instead of the
inline "conn-neo4j-001" value.

In `@app/src/app/`(dashboard)/[id]/layout.tsx:
- Around line 20-22: Update the editMode derivation in the dashboard layout to
use an exact pathname comparison with the edit route, rather than
pathname.endsWith("/edit"). Preserve editMode as true only for the concrete edit
pathname and false when the dashboard id itself is "edit".

In `@app/src/app/api/dashboards/`[id]/reassign-connection/__tests__/route.test.ts:
- Around line 173-181: The private-connection test currently mocks away the
visibility predicate and duplicates the missing-target case. Update the test
around POST and makeSelectChain to verify the owner/shared/admin filtering
predicate, or move this visibility guarantee to the database-level test suite,
while keeping the 404 response and no-widget-reassignment assertions.

In `@app/src/app/api/dashboards/import/__tests__/route.test.ts`:
- Around line 403-420: Add a response status assertion in the mixed-case test
before reading body.data.unassignedWidgetCount, matching the success-status
assertions in the neighboring import tests. Keep the existing
unassignedWidgetCount assertion unchanged.

In `@app/src/components/__tests__/dashboard-connection-dialog.test.tsx`:
- Around line 137-139: Update the renderDialog test helper and the related
override fixture at the additional location to avoid unexplained any usage: type
dashboard fixtures as Partial<DashboardDetail> and override maps as
Partial<typeof props>, removing the eslint-disable comments while preserving the
existing render behavior.
- Around line 203-206: Update the test “stays inert while closed so the
dashboard query is not fired” to verify its stated behavior by spying on the
mocked useDashboard call and asserting it receives an empty dashboard id when
rendered closed; retain the existing visibility assertion if useful, or rename
the test only if that query assertion cannot be added.

In `@app/src/components/__tests__/dashboard-workspace.test.tsx`:
- Around line 897-901: Extend the “Cmd+S saves in edit mode only” test to render
DashboardWorkspace with editMode enabled, fire the same Cmd+S key event, and
assert mockMutateAsync is called. Keep the existing view-mode assertion so both
enabled and disabled shortcut behavior are covered.

In `@app/src/components/__tests__/widget-editor-modal-maximize.test.tsx`:
- Around line 252-266: Add a dedicated test alongside the existing maximize
behavior test that expands the editor, switches the chart type to
"parameter-select" via useWidgetEditorStore, and verifies widget-preview is
rendered and gridColumns() returns the two-column layout. Keep the setup and
assertions consistent with the existing markdown test.

In `@app/src/components/dashboard-edit-toolbar.tsx`:
- Around line 89-104: Extract a shared ParameterToggleButton component
containing the Filters toggle, aria-label logic, and count badge styling. In
app/src/components/dashboard-edit-toolbar.tsx lines 89-104 and
app/src/components/dashboard-view-toolbar.tsx lines 130-145, replace the
duplicated blocks with ParameterToggleButton, passing hasParameters,
parameterCount, showParameterBar, and onToggleParameterBar; move the badge
styling into the shared component.

In `@app/src/components/dashboard-view-toolbar.tsx`:
- Around line 90-96: Update handleCustomApply to provide user feedback when
customSeconds is non-numeric or below the 5-second minimum, using a short inline
hint or disabling the Set button while invalid. Preserve the existing
onApplyInterval, reset, and dropdown-closing behavior for valid values.

In `@app/src/components/dashboard-workspace.tsx`:
- Around line 317-353: Update applyInterval to include expectedVersion:
dashboard?.version in the updateDashboard payload, using the current dashboard
version for optimistic locking. Preserve the existing persist queue and route
mutation failures, including version conflicts, through the existing
classifySaveError toast path.

In `@app/src/hooks/__tests__/use-connections.test.ts`:
- Line 67: Add coverage in the useConnections tests for the new enabled option:
assert the mocked useQuery configuration defaults enabled to true when no
options are provided, and assert useConnections({ enabled: false }) forwards
enabled: false. Keep the assertions focused on the generated query config and
follow the existing test structure.

In `@app/src/lib/db/connection-reassign.ts`:
- Around line 94-105: Update widgetMatchesSource so the empty fromConnectionId
branch wraps its combined COALESCE and chart-type exclusion predicates in an
enclosing parenthesized SQL expression, keeping both conditions grouped as one
self-contained fragment while leaving the non-empty branch unchanged.

In `@app/src/lib/shared/url-params.ts`:
- Around line 93-94: Update the comment near the companion-key handling to
remove the stray “ponytail:” prefix while preserving the explanation about
adding every companion key regardless of parameterType.

In `@app/src/stores/__tests__/dashboard-store.test.ts`:
- Around line 63-86: Add tests for setLayout covering an initialPageIndex above
the available page range, asserting activePageIndex clamps to the last page, and
a NaN initialPageIndex, asserting it follows the explicit NaN fallback behavior.
Keep the existing negative and zero cases unchanged.

In `@component/src/components/composed/dashboard-grid.tsx`:
- Around line 132-136: Memoize the result of colsForEveryBreakpoint(cols) before
rendering ResponsiveGridLayout, using the existing component memoization pattern
and cols as its dependency. Pass the memoized column map to the cols prop so its
object identity remains stable when cols is unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1cb95f87-119e-4372-8629-33d121918670

📥 Commits

Reviewing files that changed from the base of the PR and between c84c594 and b2717ec.

📒 Files selected for processing (85)
  • .claude/CLAUDE.md
  • .claude/agents/project-architect.md
  • .claude/hooks/check-boundaries.sh
  • .claude/hooks/check-migration-guard.sh
  • .claude/skills/next/SKILL.md
  • .github/workflows/ci.yml
  • .gitignore
  • CHANGELOG.md
  • app/e2e/charts.spec.ts
  • app/e2e/dashboard-connection-reassign.spec.ts
  • app/e2e/edit-page-preservation.spec.ts
  • app/e2e/edit-scroll-position.spec.ts
  • app/e2e/editor-maximize.spec.ts
  • app/e2e/fixtures.ts
  • app/e2e/grid.spec.ts
  • app/e2e/heavy-widgets.spec.ts
  • app/e2e/parameter-url-sync.spec.ts
  • app/e2e/styling-rules.spec.ts
  • app/playwright.config.ts
  • app/src/__tests__/test-environment-boundary.test.ts
  • app/src/app/(dashboard)/[id]/__tests__/layout.test.tsx
  • app/src/app/(dashboard)/[id]/edit/page.tsx
  • app/src/app/(dashboard)/[id]/layout.tsx
  • app/src/app/(dashboard)/[id]/page.tsx
  • app/src/app/(dashboard)/page.tsx
  • app/src/app/api/connections/[id]/reassign/__tests__/route.test.ts
  • app/src/app/api/connections/[id]/reassign/route.ts
  • app/src/app/api/dashboards/[id]/reassign-connection/__tests__/route.test.ts
  • app/src/app/api/dashboards/[id]/reassign-connection/route.ts
  • app/src/app/api/dashboards/import/__tests__/route.test.ts
  • app/src/app/api/dashboards/import/route.ts
  • app/src/components/__tests__/dashboard-connection-dialog.test.tsx
  • app/src/components/__tests__/dashboard-workspace.test.tsx
  • app/src/components/__tests__/lazy-visible.test.tsx
  • app/src/components/__tests__/widget-editor-modal-maximize.test.tsx
  • app/src/components/dashboard-connection-dialog.tsx
  • app/src/components/dashboard-edit-toolbar.tsx
  • app/src/components/dashboard-view-toolbar.tsx
  • app/src/components/dashboard-workspace.tsx
  • app/src/components/lazy-visible.tsx
  • app/src/components/widget-editor-modal.tsx
  • app/src/components/widget-editor/__tests__/query-editor-panel.test.tsx
  • app/src/components/widget-editor/__tests__/widget-preview-panel.test.tsx
  • app/src/components/widget-editor/query-editor-panel.tsx
  • app/src/components/widget-editor/widget-preview-panel.tsx
  • app/src/hooks/__tests__/use-connections.test.ts
  • app/src/hooks/use-connections.ts
  • app/src/hooks/use-dashboards.ts
  • app/src/hooks/use-widget-templates.ts
  • app/src/lib/__tests__/db/connection-reassign.test.ts
  • app/src/lib/__tests__/docs-accuracy.test.ts
  • app/src/lib/__tests__/shared/url-params.test.ts
  • app/src/lib/api/openapi-spec.ts
  • app/src/lib/dashboard/__tests__/import-follow-up.test.ts
  • app/src/lib/dashboard/import-follow-up.ts
  • app/src/lib/db/__tests__/tenant-scope.test.ts
  • app/src/lib/db/connection-reassign.ts
  • app/src/lib/shared/url-params.ts
  • app/src/lib/widget/__tests__/scroll-to-widget.test.ts
  • app/src/lib/widget/__tests__/scroll-to-widget.test.tsx
  • app/src/lib/widget/__tests__/webgl-budget.test.ts
  • app/src/lib/widget/content-only-chart.ts
  • app/src/lib/widget/scroll-to-widget.ts
  • app/src/lib/widget/webgl-budget.ts
  • app/src/plugins/graph/component.tsx
  • app/src/stores/__tests__/dashboard-store.test.ts
  • app/src/stores/dashboard-store.ts
  • cli/src/__tests__/commands/status.test.ts
  • cli/src/__tests__/lib/docker.test.ts
  • cli/src/commands/status.ts
  • cli/src/lib/docker.ts
  • component/design-tokens.css
  • component/src/__tests__/reduced-motion.test.ts
  • component/src/charts/__tests__/map-chart.test.tsx
  • component/src/charts/map-chart.tsx
  • component/src/components/composed/__tests__/dashboard-grid.test.tsx
  • component/src/components/composed/dashboard-grid.tsx
  • component/src/components/ui/__tests__/alert-dialog.test.tsx
  • component/src/components/ui/__tests__/dialog.test.tsx
  • component/src/components/ui/alert-dialog.tsx
  • component/src/components/ui/dialog.tsx
  • component/stories/ui/alert-dialog.stories.tsx
  • component/stories/ui/animation-centring.ts
  • component/stories/ui/dialog.stories.tsx
  • package.json
🛑 Comments failed to post (1)
.claude/CLAUDE.md (1)

22-24: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Align the documented review base with the script.

package.json now sets review:local to release/1.5. Line 24 still describes release/1.4. Update the description to the same base.

Proposed fix
- npm run review:local                 # CodeRabbit review of committed changes vs release/1.4
+ npm run review:local                 # CodeRabbit review of committed changes vs release/1.5
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

npm run verify                       # Local CI mirror: typecheck + lint + all unit suites
npm run sonar:local                  # Scan the current branch against SonarCloud (real gate)
npm run review:local                 # CodeRabbit review of committed changes vs release/1.5
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/CLAUDE.md around lines 22 - 24, Update the review:local description
in the command list to reference release/1.5, matching the script configuration
in package.json instead of release/1.4.

@sonarqubecloud

sonarqubecloud Bot commented Aug 4, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:release Release process and packaging chore Maintenance and housekeeping

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant