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
+49 -1
View File
@@ -18,6 +18,7 @@ nobody has published yet, which no panel can be shown.
from __future__ import annotations
import threading
from datetime import datetime
from pathlib import Path
from typing import Any, Literal
@@ -25,7 +26,7 @@ from pydantic import BaseModel, Field, field_validator, model_validator
from fluksio.flow.messages import DType, qualify
from fluksio.flow.schemas import FlowDef, _validate_name
from fluksio.flow.store import FlowStore, StaleVersion
from fluksio.flow.store import FlowStore, StaleVersion, _updated_at
#: Sibling of the shared-node library, and likewise not a flow.
DASHBOARD_DIR = "_dashboards"
@@ -177,6 +178,18 @@ class Placement(BaseModel):
h: int = 2
class WidgetFootprint(Placement):
"""A widget reduced to the shape it draws: what it is, and where it sits.
The whole of what sketching a dashboard's outline needs — a mosaic tile
shades a block by kind and puts it in the grid, and reads nothing else. So
no id (the drawing has no use for one), no title, no config, and one
placement rather than the layout's breakpoint per screen size.
"""
type: WidgetType
class WidgetDef(BaseModel):
"""One tile: what it shows or does, and where it sits.
@@ -517,6 +530,25 @@ class DashboardDef(BaseModel):
"""Every message a bound setting reads. Empty for a static dashboard."""
return [s.message for s in self.settings.values() if s.message]
@property
def footprint(self) -> list[WidgetFootprint]:
"""Every widget's shape, at the width the dashboard is arranged for.
``lg`` is that arrangement; the narrower breakpoints are derived from
it, and a widget nobody has placed falls back to the grid defaults —
the same order a panel resolves a layout in.
"""
shapes = []
for widget in self.widgets:
at = (
widget.layout.get("lg")
or widget.layout.get("md")
or widget.layout.get("sm")
or Placement()
)
shapes.append(WidgetFootprint(type=widget.type, **at.model_dump()))
return shapes
class DashboardSummary(BaseModel):
"""A dashboard in a list, without its contents."""
@@ -530,6 +562,14 @@ class DashboardSummary(BaseModel):
has_draft: bool = False
#: Of the working copy, so publishing from a list needs no second read.
version: int = 1
#: The grid `footprint` is placed in — see ``DashboardDef.columns``.
columns: int = 12
#: Every widget's shape, so a list can draw the dashboard's outline
#: without a document read per tile.
footprint: list[WidgetFootprint] = Field(default_factory=list)
#: When the working copy was last written, so a list can be ordered by
#: what was worked on rather than by how often it has been saved.
updated_at: datetime | None = None
class DashboardsPublic(BaseModel):
@@ -591,6 +631,9 @@ class DashboardStore:
widget_count=len(defn.widgets),
has_draft=defn.has_draft,
version=defn.version,
columns=defn.columns,
footprint=defn.footprint,
updated_at=self.updated_at(name),
)
)
return summaries
@@ -606,6 +649,10 @@ class DashboardStore:
"""Are there unpublished changes to this dashboard?"""
return self._draft_file(name).exists()
def updated_at(self, name: str) -> datetime | None:
"""When this dashboard was last written, draft or published."""
return _updated_at([self._file(name), self._draft_file(name)])
def read(self, name: str, draft: bool = False) -> DashboardDef:
"""The published dashboard, or with ``draft`` the working copy."""
path = self._draft_file(name) if draft else self._file(name)
@@ -915,5 +962,6 @@ __all__ = [
"Placement",
"SettingDef",
"WidgetDef",
"WidgetFootprint",
"default_dashboard",
]