Hold a new dashboard back until someone publishes it

A dashboard went live the moment it was created — an empty document straight to
the panels — while a new flow starts as a draft. It now works the way flows do:
published means `dashboard.json` exists, so every dashboard on every running
installation is already published and nothing needs migrating. Only the ones
created from here on start as drafts.

Mirroring FlowStore turned up a latent 500: discarding the draft of a dashboard
that had never been published unlinked its only file, and the read that followed
raised out of a 200 handler. It answers 400 now, the way a flow does.

Publishing all of them was 2N requests, because a publish has to name the
version it expects and the summaries did not carry one. They do now — and so do
the flow summaries, which had the same defect nobody had written down.

A panel had no way to hear about any of this. A publish, or a change to which
dashboards a panel carries, now puts one event on the bus and the screen
refetches what changed: no reload, so a wall display never blanks or asks for
its credential again. The subtle half is that a socket's message allowlist was
computed once at handshake — a reassigned panel would have fetched its new
document and then shown tiles that never updated.

The panels dialog logged non-superusers out. Every write in it needs a
superuser, not only the checkboxes the report mentioned, so the dialog is
read-only for everyone else. The logout itself was `main.tsx` treating 403 as a
dead session, against the contract deps.py spells out: only a 401 ends a
session, and a 403 now says so rather than silently signing someone out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uq8mtNb97A7praJLyeEYgs
This commit is contained in:
2026-08-21 14:32:57 +02:00
co-authored by Claude Opus 5
parent 000c5abf91
commit c3ea884d72
14 changed files with 268 additions and 115 deletions
@@ -60,19 +60,16 @@ function Dashboards() {
onError: handleError.bind(showErrorToast),
})
// A summary carries no version, and publishing needs the one it is based on —
// so each dashboard's working copy is read right before it is published.
// The summary carries the working copy's version, which is the one publishing
// is based on — so the list already holds everything this needs.
const publishAll = usePublishAll(
async (dashboard) => {
const current = await DashboardsService.readDashboard({
(dashboard) =>
DashboardsService.publishDashboard({
name: dashboard,
draft: true,
})
return DashboardsService.publishDashboard({
name: dashboard,
requestBody: { version: current.version ?? 1 },
})
},
requestBody: {
version: data?.data.find((d) => d.name === dashboard)?.version ?? 1,
},
}),
"dashboard",
() => queryClient.invalidateQueries({ queryKey: dashboardKeys.all }),
)
+8 -8
View File
@@ -53,16 +53,16 @@ function Flows() {
onError: handleError.bind(showErrorToast),
})
// A summary carries no version, and publishing needs the one it is based
// on — so each flow's current version is read right before it is published.
// The summary carries the working copy's version, which is the one publishing
// is based on — so the list already holds everything this needs.
const publishAll = usePublishAll(
async (flow) => {
const detail = await FlowsService.readFlow({ name: flow })
return FlowsService.publishFlow({
(flow) =>
FlowsService.publishFlow({
name: flow,
requestBody: { version: detail.definition.version ?? 1 },
})
},
requestBody: {
version: data?.data.find((f) => f.name === flow)?.version ?? 1,
},
}),
"flow",
() => queryClient.invalidateQueries({ queryKey: flowKeys.all }),
)