Give both summaries a modified time, and the dashboard list a footprint

Home's "recently modified" order was a proxy — drafts first, then the
version counter, then the name. Both summaries now carry `updated_at`,
read as the mtime of the working copy: every write in the store commits
immediately, so a file's mtime is its commit time, and `git log -1 --
<path>` costs ~990ms across this instance's 15 documents (it walks the
history back to the last commit touching each one, so it is slowest for
the stalest) against ~1.8ms for the stats. No cache needed.

`DashboardSummary` also carries `footprint`: each widget as its type plus
the placement a panel resolves, which is the whole of what the mosaic
draws. That drops the document fetch per tile and with it the cap of
eight, past which tiles showed a name and nothing else. Verified against
this instance's 8 dashboards: the blocks are identical to what the old
client-side derivation produced.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CL9zvnnvcp1mvA8o7impxk
This commit is contained in:
2026-09-06 15:26:48 +02:00
co-authored by Claude Opus 5
parent 8cb843eb25
commit ff612623a1
9 changed files with 271 additions and 86 deletions
+50
View File
@@ -1,11 +1,14 @@
"""Dashboards: documents beside the flows, and the values their widgets move."""
from datetime import UTC, datetime
import pytest
from fluksio.flow.dashboards import (
DashboardDef,
DashboardNotFound,
DashboardStore,
Placement,
SettingDef,
WidgetDef,
default_dashboard,
@@ -389,6 +392,53 @@ def test_a_querying_chart_keeps_no_ring():
assert widget.history_points == 0
def test_a_summary_carries_the_footprint_of_the_document(store: DashboardStore):
"""The mosaic draws from the list, so the shapes have to survive it."""
saved = store.write(
DashboardDef(
name="house",
columns=8,
widgets=[
WidgetDef(
id="temp",
type="chart",
layout={"lg": Placement(x=2, y=1, w=6, h=4)},
),
# Only a narrower breakpoint, and none at all: both resolve the
# way a panel resolves them.
WidgetDef(id="power", type="gauge", layout={"md": Placement(y=5, w=2)}),
WidgetDef(id="lamp", type="switch"),
],
)
)
(summary,) = store.list()
assert summary.columns == saved.columns
assert [(f.type, f.x, f.y, f.w, f.h) for f in summary.footprint] == [
("chart", 2, 1, 6, 4),
("gauge", 0, 5, 2, 2),
("switch", 0, 0, 3, 2),
]
def test_a_summary_carries_when_the_working_copy_was_written(store: DashboardStore):
published = store.write(default_dashboard("house"))
document = store.root / "house" / "dashboard.json"
assert store.list()[0].updated_at == datetime.fromtimestamp(
document.stat().st_mtime, UTC
)
# An unpublished edit is the working copy, so it is what the time is of.
store.write_draft(published.model_copy(update={"title": "Kitchen"}), 1)
draft = store.root / "house" / "dashboard.draft.json"
assert store.list()[0].updated_at == datetime.fromtimestamp(
draft.stat().st_mtime, UTC
)
def test_a_dashboard_is_cut_into_a_sane_number_of_columns():
for columns in (0, 49):
with pytest.raises(ValueError):