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:
@@ -14,6 +14,22 @@ def _panels(client: TestClient, headers: dict[str, str], config: dict) -> None:
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
|
||||
def _dashboard(client: TestClient, headers: dict[str, str], name: str) -> dict:
|
||||
"""Create a dashboard and publish it.
|
||||
|
||||
A new one is a draft, and a panel is only ever shown what is published.
|
||||
"""
|
||||
created = client.post(f"{DASHBOARDS}/{name}", headers=headers)
|
||||
assert created.status_code == 200, created.text
|
||||
published = client.post(
|
||||
f"{DASHBOARDS}/{name}/publish",
|
||||
headers=headers,
|
||||
json={"version": created.json()["version"]},
|
||||
)
|
||||
assert published.status_code == 200, published.text
|
||||
return published.json()
|
||||
|
||||
|
||||
def _pair(client: TestClient, headers: dict[str, str], panel: str) -> dict[str, str]:
|
||||
"""Walk a device through pairing and return the header it ends up with."""
|
||||
started = client.post(f"{PREFIX}/pair").json()
|
||||
@@ -122,7 +138,7 @@ def test_paired_panel_reaches_only_what_it_shows(
|
||||
client: TestClient, superuser_token_headers: dict[str, str]
|
||||
) -> None:
|
||||
for name in ("panel_shown", "panel_hidden"):
|
||||
client.post(f"{DASHBOARDS}/{name}", headers=superuser_token_headers)
|
||||
_dashboard(client, superuser_token_headers, name)
|
||||
_panels(
|
||||
client,
|
||||
superuser_token_headers,
|
||||
@@ -187,7 +203,7 @@ def test_paired_panel_reaches_only_what_it_shows(
|
||||
def test_removing_the_panel_revokes_its_credential(
|
||||
client: TestClient, superuser_token_headers: dict[str, str]
|
||||
) -> None:
|
||||
client.post(f"{DASHBOARDS}/panel_gone", headers=superuser_token_headers)
|
||||
_dashboard(client, superuser_token_headers, "panel_gone")
|
||||
_panels(
|
||||
client,
|
||||
superuser_token_headers,
|
||||
@@ -368,10 +384,7 @@ def test_a_panels_socket_carries_only_what_it_draws(
|
||||
snapshot_payload,
|
||||
)
|
||||
|
||||
client.post(f"{DASHBOARDS}/panel_socket", headers=superuser_token_headers)
|
||||
saved = client.get(
|
||||
f"{DASHBOARDS}/panel_socket", headers=superuser_token_headers
|
||||
).json()
|
||||
saved = _dashboard(client, superuser_token_headers, "panel_socket")
|
||||
saved["pages"] = [
|
||||
{
|
||||
"id": "main",
|
||||
@@ -435,6 +448,9 @@ def test_a_panels_socket_carries_only_what_it_draws(
|
||||
{"type": "message_value", "name": "house.safe.code"}, only
|
||||
)
|
||||
assert not event_for_panel({"type": "node_log", "text": "a traceback"}, only)
|
||||
# Except a dashboard being published: that is how a screen hears the
|
||||
# document it draws — or the set of them it was given — has moved.
|
||||
assert event_for_panel({"type": "dashboard_changed", "dashboard": "x"}, only)
|
||||
|
||||
# A person's credential is not bounded at all.
|
||||
assert panel_scope(superuser_token_headers["Authorization"][7:], client.app) is None
|
||||
|
||||
Reference in New Issue
Block a user