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.
This commit is contained in:
2026-09-06 15:26:48 +02:00
parent 8c097d9222
commit fa5ce4f694
9 changed files with 271 additions and 86 deletions
+75
View File
@@ -575,6 +575,30 @@ export const DashboardSummarySchema = {
type: 'integer',
title: 'Version',
default: 1
},
columns: {
type: 'integer',
title: 'Columns',
default: 12
},
footprint: {
items: {
'$ref': '#/components/schemas/WidgetFootprint'
},
type: 'array',
title: 'Footprint'
},
updated_at: {
anyOf: [
{
type: 'string',
format: 'date-time'
},
{
type: 'null'
}
],
title: 'Updated At'
}
},
type: 'object',
@@ -1303,6 +1327,18 @@ export const FlowSummarySchema = {
type: 'integer',
title: 'Version',
default: 1
},
updated_at: {
anyOf: [
{
type: 'string',
format: 'date-time'
},
{
type: 'null'
}
],
title: 'Updated At'
}
},
type: 'object',
@@ -3834,6 +3870,45 @@ The first two are a \`\`list\`\` message, the third a \`\`str\`\`, which is what
\`\`COLOR_DTYPES\`\` records and the check below holds a binding to.`
} as const;
export const WidgetFootprintSchema = {
properties: {
x: {
type: 'integer',
title: 'X',
default: 0
},
y: {
type: 'integer',
title: 'Y',
default: 0
},
w: {
type: 'integer',
title: 'W',
default: 3
},
h: {
type: 'integer',
title: 'H',
default: 2
},
type: {
type: 'string',
enum: ['stat', 'gauge', 'chart', 'markdown', 'agenda', 'notification', 'bar', 'icon', 'forecast', 'clock', 'media', 'player', 'embed', 'button', 'switch', 'slider', 'input', 'dropdown', 'color'],
title: 'Type'
}
},
type: 'object',
required: ['type'],
title: 'WidgetFootprint',
description: `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.`
} as const;
export const WorkerInfoSchema = {
properties: {
name: {
+20
View File
@@ -158,6 +158,9 @@ export type DashboardSummary = {
widget_count?: number;
has_draft?: boolean;
version?: number;
columns?: number;
footprint?: Array<WidgetFootprint>;
updated_at?: (string | null);
};
export type DeadLetter = {
@@ -408,6 +411,7 @@ export type FlowSummary = {
paused?: boolean;
quarantined?: boolean;
version?: number;
updated_at?: (string | null);
};
export type fluksio__api__routes__dashboards__PublishRequest = {
@@ -1314,6 +1318,22 @@ export type WidgetDef = {
export type type = 'stat' | 'gauge' | 'chart' | 'markdown' | 'agenda' | 'notification' | 'bar' | 'icon' | 'forecast' | 'clock' | 'media' | 'player' | 'embed' | 'button' | 'switch' | 'slider' | 'input' | 'dropdown' | 'color';
/**
* 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.
*/
export type WidgetFootprint = {
x?: number;
y?: number;
w?: number;
h?: number;
type: 'stat' | 'gauge' | 'chart' | 'markdown' | 'agenda' | 'notification' | 'bar' | 'icon' | 'forecast' | 'clock' | 'media' | 'player' | 'embed' | 'button' | 'switch' | 'slider' | 'input' | 'dropdown' | 'color';
};
export type WorkerInfo = {
name: string;
labels?: Array<(string)>;