Repository navigation
feat: group a solution dashboard into declared sections a viewer can arrange - #958
Merged
antoinetoussaint-byte merged 32 commits intoOct 12, 2026
Merged
antoinetoussaint-byte merged 32 commits into
antoinetoussaint-byte merged 32 commits into
Conversation
crisfiorini-oss
force-pushed
the
feat/dashboard-sections
branch
from
September 28, 2026 16:34
0d8d3c3 to
1a1d7cf
Compare
crisfiorini-oss
force-pushed
the
feat/dashboard-sections
branch
from
October 2, 2026 17:50
2a22c76 to
0fe584d
Compare
A solution with many metrics wants an overview band above the detail, but the validator refused any grouping: a dashboard with `sections` failed `dashboard has unknown field 'sections'`, so the host answered the registration with 422 invalid_manifest. A dashboard may now declare `sections` (each an `id`, a `title` and an optional `description`, in the order they are drawn), and each widget then names its `section`. A section may hold no widgets. A dashboard without sections is unchanged, and none of its widgets may name one. - data-graph.ts: `DashboardSection`, `Dashboard.sections`, `MetricWidget.section`; the validator checks each section's shape, unique section ids per dashboard, and that a widget names a declared section exactly when its dashboard declares sections. - plugin.codefly.schema.json carries the same field rules, including the cross-field one (if/then/else on `sections`); the conformance suite pins that the schema and the validator agree. - saas-sdk's mirror of the shapes (src/schema.ts) and both packages' type exports gain the section types. saas-sdk 0.3.16. - The host's registry parse carries a sectioned graph through and refuses a widget that names an undeclared section. - docs/plugin-manifest-schema.md names the new keys and rules. No other validator reads these keys: accounts parses only a dashboard's `events` (for declared audit types), and the dashboard-authoring spec is a separate shape. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SortableGrid rearranges one list of tiles. A dashboard with sections needs the same drag across several groups, and groups that move themselves. SortableBoard is that, and SortableGrid is now the board with one group, so its behaviour and props are unchanged. - One drag context over every group: a tile dropped on a tile in another group swaps with it, and previews and glides like a swap within a group. - `onMove`: while a tile is dragged, every group ends with a tile-sized slot, and a drop there moves the tile to the end of that group. Without it the board offers no slot. - `onMoveGroup`: a group is dragged by a handle its header spreads (for instance a grip by its title). The groups are one column, so a drop inserts: the nearer half of a group decides before or after it, above the first group is before it and below the last after it, and a line marks the place. No tile is a drop target meanwhile. A `fixed` group stays put and nothing lands before it. - Every group and tile glides from where it was drawn when the order changes, a tile that moved to another group too; a tile glides with its group rather than on its own. - Screen-reader announcements name the group a tile moves to, and where a group lands. - `renderOverlay` and the content-keyed item list work as they do on SortableGrid. A SortableGroups story shows it, and the UI inventory lists the story. @codefly-dev/ui and @codefly-dev/saas-ui 0.11.5 (co-versioned). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd let a viewer arrange them A solution dashboard that declares sections is drawn as one titled group per section, in declared order, each with its description and its own + menu. Sections come from the declaration; a viewer only groups and arranges the tiles in their own view: - drag a tile onto a tile in another section to swap the two, or onto the slot that ends every section during a drag to move it there; the grip's arrow keys cross a section boundary; - rename a section (from its title or its options menu), remove one (its tiles move to the section above, or below for the first), reorder sections by dragging a section's grip or with its arrow keys, and add sections of their own after the last. A viewer's section starts empty and is filled by dragging tiles in. Each + menu, the dashboard's and a section's, offers only the declared widgets the viewer removed, and a section's + puts the widget back in that section. No menu offers a metric the dashboard has no widget for, and no section adds one (ADR 0007). A dashboard without sections draws and saves as before, with its untitled group first. The layout moves to version 2 in the same per-viewer key: sections, each with its tiles and the viewer's title if they gave one, plus the declared widgets and sections it was made against. So a section or widget the solution declares later still appears, while one the viewer removed stays removed. A version-1 layout (a flat list of tiles) is read by putting each tile in its declared section; an id the dashboard does not declare, such as a `metric:` tile saved before ADR 0007 applied, is dropped. A viewer's own section id is `custom:` and 16 hex characters, and its title is trimmed and capped at 60 characters. The layout still lives in browser storage, under the existing STOPGAP label. docs/plugin-manifest-schema.md says what a viewer can do with sections. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…no data yet A + menu lists the declared widgets the viewer removed, but nothing in it said whether a tile would have anything to show once added back. Each item now says so, muted, beside the widget's title, with the tile's own rule: - "No data yet." when the widget's metric returned no rows; - "Telemetry incomplete." when a derived metric lacks an input, where the tile says "Telemetry unavailable or incomplete." (or, for a number, "Incomplete telemetry; total unavailable."); - "Total unavailable." for a number with no total across groups; - nothing when there is data, while the answer is loading, or when the query fails, so an error never reads as "no data". The rule is one function the tile and the menu share. An item asks the tile's own query, through the same hook and query key, and only while its menu is open, as the tile's info popover does; so nothing is asked about a removed widget on page load, and a tile added back draws at once from the menu's answer. Items stay selectable either way, and the menus still offer only removed declared widgets (ADR 0007). The kit's DropdownMenuItem gains an optional `hint`: short muted text after the label. It describes the item rather than naming it, so the label alone stays the item's accessible name. A MenuHints story shows it, and the UI inventory lists the story. @codefly-dev/ui stays at 0.11.5, already bumped in this branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A rate on a solution dashboard read as a bare fraction: a win rate of four in ten showed "0.4", and 17 of 22 showed "0.773". The host could not write it as a percentage on its own, because a ratio is not always a share: "versions per memo" is a ratio too, and reads 2.2. A metric, source or derived, may now name a `format`. `number`, the default, writes the value as it is. `percent` reads it as a share from 0 to 1 and writes it as a percentage, to one decimal: 0.4 reads "40%", 17/22 reads "77.3%". - data-graph.ts and plugin.codefly.schema.json allow `format` on both metric kinds and refuse any other value; the conformance suite pins that they agree. saas-sdk mirrors the type (`MetricValueFormat`). - runDashboard puts each widget's metric format on the resolved widget, and the kit's fromDashboardData carries it to its view model. - The kit's StatChart, BarList and Line/AreaChart take an optional `formatValue`: the tile's number, each bar's value and the y-axis ticks. `formatShare` writes a share as a percentage. The kit's shared <Dashboard> and the host's solution dashboard both use it for a percent widget, tables included, and leave a plain number to each chart's own default. - docs/plugin-manifest-schema.md names the key. A host that predates `format` refuses a graph that names it, as one that predates sections refuses sections; both ship in the same release. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A per-day line chart dropped every day with no events and drew a straight line over it. The audit RPC returns no bucket for a day with no rows, and the kit's line chart spaces its points evenly by position, not by date. So ten closes on ten days across a 24-day span drew as a flat line at 1, which reads as one close a day, and the x axis compressed the empty days away. runMetric now puts a 0 at every day between the first and the last day the RPC returned, for an aggregation that is 0 over no events: count, count_distinct and sum. Week and month buckets step by a week and a month. With every bucket present, the even spacing is spacing by date. - avg, min, max and percentile have no value over no events, so a bucket with none stays out, as before. - A bucket the RPC did return, whose value was dropped as unknown (events that day, none carrying the field), stays out and keeps the series partial. It is not a day with no events. - A key that does not read as a date, or one off the bucket's step from the first, leaves the points as they came. - Totals do not change: the zeros add nothing. - Only the span between the first and the last returned bucket is filled, never the days before or after it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… beside it A section's grid lays tiles out in rows, and a row is as tall as its tallest tile. A number tile is about 120px and a line chart about 282px, so a number beside a chart left some 160px empty under it. A line, area, bar or table tile now spans two rows of its section's grid, and every card fills its cell. Two number tiles stack beside a chart, and the grid works out the row heights from what the tiles hold, so no height is fixed. The tiles keep the order the viewer gave them: the grid does not pack later tiles into earlier holes. So a section with an odd number of cells still ends on a half-empty row. The kit's SortableBoard gains an optional `itemClassName(id)`: classes for one tile's list item, which is its group's grid item. It knows nothing about charts; the host says which tiles are tall. Measured on a local host at 1440px wide: before, the page had a gap under every number tile beside a chart or bar list; after, no tile is shorter than the space it sits in, and the only empty cells are the last cell of a section with an odd cell count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts on The per-day zeros reached derived metrics. A ratio of two per-day counts with a day on which neither had events read 0/0 there and marked the whole series partial, so its tile said "Partial telemetry" where it said nothing before the zeros. A derived metric over a count and an avg read the avg as missing on the count's filled day and did the same. Before the zeros no input returned such a day, so it was no missing telemetry. runMetric now marks the zeros it puts in, and combineDerived treats a key where every operand is one of them, or missing, as a key with no events: a ratio there has no value and does not mark the series partial, and a sum or difference of the zeros is 0, marked the same way. A key where any input returned a bucket is combined as before, so a day with events in the denominator and none in the numerator reads 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
crisfiorini-oss
force-pushed
the
feat/dashboard-sections
branch
from
October 2, 2026 18:55
0fe584d to
ccf0978
Compare
antoinetoussaint-byte
previously approved these changes
Oct 2, 2026
…say where one crossed into another section The arrow keys on a tile's grip saved the layout on every press, even one that could not move the tile (up on the first tile, down on the last). A save pins the viewer to their own layout, so a press that changed nothing stopped the dashboard following its declared default. The grip now saves only when the tile's section or place changes, as the section grip and rename already do. A press that took a tile into the next or previous section said nothing to a screen reader, though a section move and a drag both do. It now says "<tile> moved to the start of <section>." going down, or "to the end of" going up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
crisfiorini-oss
force-pushed
the
feat/dashboard-sections
branch
from
October 2, 2026 20:09
81d2fb9 to
0e84d23
Compare
…udit log uses The kit carried two chart sets. The audit log draws with the metric charts (KPIRow and StatTile, MetricAreaChart): colour from the --chart palette, a hover readout, a legend, days lined up by label. A declared dashboard, in the host's solution page and in the kit's own Dashboard, still drew with the first set (StatChart, LineChart, AreaChart), so the two pages looked unrelated. WidgetChart draws one resolved widget with the metric charts, and both the kit Dashboard and the host's solution page draw a tile's values with it. MetricFigure is StatTile's value, delta and sparkline without the card, for a tile that already has its own header. BarList takes the audit log's look, and the audit log draws its top lists with it instead of a host copy. The old LineChart, AreaChart and StatChart stay exported, marked deprecated. A percent metric is a share from 0 to 1, and the figure's `percent` expects 45 for 45%, so WidgetChart scales the share once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts were version numbers only. This branch changes the content of the three published kit packages beyond main, so each takes main's version plus one patch: @codefly-dev/ui 0.12.1 -> 0.12.2, @codefly-dev/saas-ui 0.12.1 -> 0.12.2, @codefly-dev/saas-sdk 0.3.16 -> 0.3.17 (main is already at 0.3.16, the same number this branch had bumped to, so it moves too). The host pins follow, and the saas-plugin-contract pin takes main's 2.4.0 (this branch does not touch the contract). package-lock.json is main's, regenerated from the resolved package.json files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ard classes
The kit's stories and dashboard components on this branch use utility classes
(opacity-70, inset-x-0, -top-1.5, -bottom-1.5 and others) that main's committed
preview stylesheet does not define, so preview-stylesheet.test.tsx failed on the
merge ("is not stale" and "defines every class the kit's stories render"); it
passes on main alone. Ran generate-preview-stylesheet.mjs; --check is clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d zeros A per-day sum whose days with events all had an unknown value had no total. Filling the day between them with a 0 gave the series one point, so the total became 0 and the number tile read 0 over amounts it never observed. A total now counts only points something was observed on: a series of filled zeros alone has none, as it had none before the fill. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y kept is retired When a solution retired the last section a viewer had kept, the saved layout fell back to the declared default, and every widget the viewer had removed came back. The layout now gets the default's first section back instead, the viewer's tiles go there, and a widget they removed stays removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A dashboard's accent set only `--primary`. The bar list colours from it, but the metric charts a line or area widget now draws with colour their series from `--chart-1`, so with an accent the bars took it and the lines did not. The accent now sets both tokens on the dashboard's subtree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A solution dashboard's number tile was titled as a card heading: larger, darker and in the heading font. The audit log's number tiles (the kit's StatTile) label the figure in small, muted body text. A number tile's title now reads the same way; chart, bar and table tiles keep the card heading, as the audit log's chart cards do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts were version numbers and the UI inventory. This branch changes the content of the published kit packages beyond main, so each takes main's version plus one patch: @codefly-dev/ui 0.12.4 -> 0.12.5, @codefly-dev/saas-ui 0.12.4 -> 0.12.5, @codefly-dev/saas-sdk 0.3.16 -> 0.3.17 (main did not move it). The host pins follow, and the contract, manifest and plugin-react pins take main's 2.4.1, 0.1.1 and 0.4.2. package-lock.json is main's, regenerated from the resolved package.json files. ui-inventory.json keeps both sides: main's new accordion entry beside this branch's widget-chart.tsx entry; the kit dashboard.tsx entry already imports ./widget-chart.js. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A chart of one series drew it in `--chart-1`. The default skin's palette runs light to dark, so a solution dashboard's line was a faint grey (87% lightness) beside the audit log's chart, which reads dark because its topmost series, the total outline, takes the last colour. A lone series now takes that last colour too; two series or more keep the palette order. A dashboard's accent now sets `--chart-5`, the colour its one-series charts draw in, instead of `--chart-1`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A number tile's title took StatTile's muted grey with its label size, and beside the figure it read too faint to scan. It keeps the label size and weight and now takes the foreground colour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
github-merge-queue
Bot
removed this pull request from the merge queue due to a conflict with the base branch
Oct 6, 2026
This was referenced Oct 8, 2026
Contributor
|
lazybox has finished working on this. claim detail |
main now carries codefly-dev#1043, which rewrote the kit's <Dashboard> onto the metric tier and made SolutionDashboard compose it through slots. The owner chose that renderer over this branch's, so every conflicted file is resolved to main's side: the kit's dashboard.tsx, types.ts, metric-tiles.tsx, index.ts, charts.tsx, SolutionDashboard.tsx and its test, and the package manifests and lockfile. This commit is the conflict resolution only. The branch's sections, viewer arrangement, percent format, per-day zeros and two-row tiles are rebuilt on top of codefly-dev#1043's renderer in the commits that follow; until they land this commit does not type-check, because the branch's non-conflicting files (widget-chart.tsx, the v2 layout) still name what the resolution dropped. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…-dev#1043's codefly-dev#1043 made the kit's <Dashboard> draw every widget with the metric tier and the host compose it through slots, with a gate refusing a re-implementation. This branch had drawn the same widgets through its own path, which goes: - widget-chart.tsx (WidgetChart) dispatched a widget to a mark, which is <Dashboard>'s job; its inventory entry goes with it; - the lone-series colour (chartSeriesColor(i, count) taking --chart-5) contradicts codefly-dev#1043's accent, which is the first series' --chart-1; - formatShare and the formatValue props on the deprecated basic charts served WidgetChart's percent path; a widget's format now reaches every mark through the kit's formatMetricValue, and the share is scaled once in runDashboard (following commits); - the audit page's bar list keeps main's own drawing; restyling it was part of matching the solution dashboard to the audit log, which codefly-dev#1043 did by moving both onto the metric tier. The kit's dashboard and types tests are main's again here; this branch's cases are rebuilt on codefly-dev#1043's renderer in the following commits. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… format
The kit's view model gains sections: DashboardView.sections ({id, title,
description?}[], drawn in the order listed) and DashboardWidgetView.section.
<Dashboard>'s default layout heads each section with the new exported
DashboardSectionHeader (an <h3> under the dashboard's <h2>), lays each
section's widgets out exactly as before, draws a widget naming no section
first and untitled, and keeps a section no widget names, saying it is
empty. A view with sections is never "no widgets": it goes to the layout,
and a host's `layout` slot receives each tile's section. A view without
sections draws as it did. fromDashboardData carries both fields.
DashboardSectionHeader takes the host's controls without the host drawing
a heading of its own: a leading handle, actions, an `editing` node drawn in
place of the heading, a `label` naming the heading when its title is a
control, and children under the description.
A widget's format now reaches a chart's axis, readout and data table and a
table's values, through formatMetricValue, the function StatTile writes a
scalar with; before, only the scalar honoured it. The kit's `percent`
contract is unchanged: the value is already a percentage.
The kit's preview stylesheet is regenerated; a no-change control on main's
sources reproduced main's committed bytes first. The new classes are those
of SortableBoard, the menu hint and the section layout.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… sections A data graph's `format: "percent"` reads a metric's value as a share from 0 to 1, because that is what a ratio produces. The kit's view reads its `percent` as percentage points. This branch had put the graph's share semantics on ResolvedWidget.format, which fromDashboardData forwards structurally, so a remote drawing runDashboard's output would have written 0.4 as "0.4%". runDashboard is now the one place the two meet: a widget whose metric is declared percent carries its series multiplied by 100 and format "percent" (WidgetValueFormat, the kit's convention); byMetric keeps the shares, and nothing downstream scales again. A test pins 0.4 → 40 on the widget and 0.4 on byMetric, and fails with the scaling removed. DashboardData also carries the dashboard's sections and each widget's section, so a remote drawing its own dashboard with the kit gets them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rd slots SolutionDashboard rebuilds this branch's arrangement on codefly-dev#1043's file instead of beside it. The viewer's layout (dashboard-layout.ts, version 2, still the labelled browser-storage STOPGAP) is resolved into the view the kit's <Dashboard> takes: the viewer's sections in their order under their titles, each shown widget in the section the viewer put it in. The host's controls go into the renderer's slots: - `layout`: the kit's SortableBoard, one group per section, each headed by the kit's DashboardSectionHeader with the grip, the rename title, the section's + and its options in it; tiles move between sections by drag or arrow keys, and an arrow key that moves nothing saves nothing; - `renderWidget`: SolutionWidget, still codefly-dev#1043's per-widget query mounting the kit's DashboardWidget; its query is now the shared useWidgetSeries, so a + menu asks the same key (and a refusal it meets deletes the same snapshots) only while it is open, and says "No data yet." or "Telemetry incomplete." beside a removed widget whose tile would have nothing; - `actions`: the dashboard's + when it declares no sections. A widget's format is bound from the resolved answer with its series, because runDashboard is where a share became percentage points. A line, area, bar or table tile spans two grid rows and every tile fills its cell, so two number tiles stack beside a chart. "Add section" and the status region sit after the renderer. The branch's host tests are carried over onto codefly-dev#1043's file; those that read its own card now read the kit's markup (section headings by their slot, a tile's emptiness by the kit's "No data" / "Partial data" badges). A percent ratio is asserted as "40%" on the tile, the bar chart's data table and a table widget through the real SDK path. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AUDIT_METRICS.md says where a time-grouped count is zero-filled and what is not, that a widget's format reaches every mark, that the data graph's percent (a share) becomes the kit's (percentage points) once in runDashboard, and how a view's sections are drawn. The manifest schema doc names the same boundary and the zero fill beside the `format` key. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AggregateAuditLog keyed a time bucket with to_char(date_trunc(grain,
created_at), '...OF') in the database session's zone. Under a zone with
daylight saving the offset moves ("+01" then "+02"), so:
- the SDK's zero fill, which has to name the buckets the aggregate did not
return, could not know which offset a missing day takes (review m1: a
filled day copied the first key's offset and could parse as the day
before);
- the kit labels keys in UTC, so a "+02" midnight already read as 22:00
on the previous day.
Buckets are now UTC days, weeks and months, keyed "...T00:00:00+00" by
construction, as the entitlements usage buckets already are. A session in
UTC, the default, gets the same keys as before.
TestTimeBucketKeysAreUTCInAnySessionZone sets the session to Europe/Paris
around the 2026 change to summer time and asserts day, week and month
keys; with the old expression it fails on "+01"/"+02" keys. Run against
postgres:17-alpine, as CI's audit-postgres service is.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e range end Review M1: each input of a derived metric was zero-filled only between its own first and last bucket. A failure rate whose failures start after the requests, or never happen, dropped those days and read "partial" (the tile's "Partial data", the + menu's "Telemetry incomplete."), which is the case this PR said it fixed. A complete, time-grouped count, distinct count or sum is now marked fillable, and combineDerived reads a fillable input's missing time bucket as that input's no-events 0. Keys from every input are put in time order, the buckets between two inputs' ranges are combined too, and a sum or difference of fillable inputs is fillable in turn, so it can feed a rate. A partial input, an aggregation with no value over no events, and a non-date key keep a missing operand unknown. The prototype was the reviewer's; the union grid and the nested case are added here. Review m2: the fill stopped at the last returned bucket, so the days since the last event vanished and the newest point and the bucket delta read an old day as the latest. It now runs through the bucket holding the end of the range: just before an exclusive `to`, or now for an open query. Review m1: a fill copies the returned keys' suffix, which the RPC now keys in UTC (previous commit). Keys that disagree about their offset are left as they came rather than given a guessed one. Tests: the reviewer's two M1 cases, the trailing-days case, and a shared percent metric scaled once per widget, adopted; plus time order, union grid, nested sum, partial input, exclusive `to`, week buckets, and the offset refusal. Existing fill tests pin their clock before their own days. Each mechanism was removed in turn and reds its tests. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review m3: a section's title and its widgets' titles were all <h3>, so
heading navigation read a section's widgets as its peers. A widget inside
a section is now titled <h4>; a widget drawn above every section, and
every widget of a view without sections, keeps <h3>.
- Card takes titleAs ("h3" default, "h4"); StatTile's labelAs takes "h4".
- DashboardWidget takes heading (WidgetHeading, "h3" default).
- <Dashboard> decides the level and passes it to renderWidget as a second
argument, { heading }, so a host resolving its own widgets titles them
the same way. A slot that ignores it keeps <h3>.
Also: stories for a sectioned <Dashboard> and DashboardSectionHeader,
listed in ui-inventory.json, and a README note of what a remote will see
in 0.12.7 (review m6): a widget's format, compact and currency included,
now reaches chart axes, readouts, data tables and tables.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…as no activity - SolutionWidget passes renderWidget's heading to DashboardWidget, so a widget in a section is an <h4> on the host's page too. - The ⓘ's data range and freshness read a day counted 0 as a day with no events: with the fill running through today, the last filled day had become "the latest event". - The + menu's "Telemetry incomplete." test now misses an average, which has no value over no events; a count it missed before now reads 0, which is the review's M1 fix. - Adopted from the review: a refusal met by a + menu deletes the tile's retained values (200, removed, 403 in the menu, then 503; and a 403 landing after the menu closed), and stored layouts the manifest has moved past. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review m5: 0.1.1 is the version v0.0.92 to v0.0.94 carried, and this PR adds exported types (DashboardSection, MetricValueFormat) and changes what the validator accepts. A patch, as the kit's additive changes are. The root pin and the lockfile follow. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… inputs AUDIT_METRICS.md states that time buckets are UTC, that the fill runs through the end of the range (the current bucket being in progress), how a derived metric reads an input's day with no events, and the heading level of a section's widgets. The manifest schema doc says the same of format's neighbour, the zero fill. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The adopted stale-layout test dropped a widget's section by destructuring it into an unused name, which added a lint warning (40 against main's 39). It now sets the section to undefined. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
antoinetoussaint-byte
added a commit
that referenced
this pull request
Oct 13, 2026
… DashboardWidgetView gained
Merging main into this branch leaves `@codefly-dev/ui` unable to build:
src/dashboard/catalog.ts(195,3): error TS1360
Property 'sections' is missing in type … but required in type
'SchemaProperties<DashboardView>'
That is the catalog's own invariant firing, not a merge artefact.
`SchemaProperties<T>` maps over `keyof Required<T>`, so the published A2UI data
model must name EVERY field of the view types — which is the point: a field
added to the renderer's types without a catalog entry does not compile, so the
published vocabulary cannot silently fall behind what the renderer draws.
Main added sections (#958's declared sections a viewer can arrange) and the
scalar/placement fields alongside them, while this branch was writing the
catalog, so the two met here. Declared now:
- `DashboardView.sections`, an array of a new `DashboardSectionView` `$def`
(`id`, `title` required; `description` optional).
- `DashboardWidgetView.section`, naming the section a widget is drawn in.
- `DashboardWidgetView.failure`, as the `WidgetReadFailure` enum. Its
description keeps the distinction the type's own comment insists on: "failed"
says the read did not complete and says nothing about authority, so a series
already in hand is still drawn as stale, while "refused" says this viewer may
not see the number, which makes a previously fetched one wrong rather than
old. They are different questions, not a severity ordering.
- `DashboardWidgetView.format`, `unit` and `higherIsBetter`, the scalar
presentation fields.
`WIDGET_READ_FAILURES` and `METRIC_FORMATS` are declared through `closedSet`,
the way `DASHBOARD_LAYOUTS` already is, rather than as bare arrays. That is
load-bearing: `closedSet` refuses a list that has dropped a member, so a new
`WidgetReadFailure` or `MetricFormat` fails at the catalog instead of publishing
an enum that no longer spans its union.
`dashboard-data.schema.generated.json` is REGENERATED, not edited --
`npm run generate:dashboard-catalog --workspace @codefly-dev/ui` -- and the diff
is additive: +54 lines, the fields above and the new `$def`.
`check:dashboard-catalog` passes, which is what pins the published bytes to what
catalog.ts renders.
Also resolved in this merge: `src/dashboard/index.ts`, where main widened the
`dashboard.js` export with the section and widget components while this branch
added the `catalog.js` block. Neither side touched the other's, so the
resolution is the union.
Tests: frontend `--project pure` 4759/4759 across 336 files, `@codefly-dev/ui`
1478/1478 across 47 files, the dashboard and catalog suites 188/188,
`check:dashboard-catalog`, `tsc --noEmit` and `prepare:frontend` all exit 0,
eslint 0 errors. kit-version, module-verify, check-boundaries and naming-gate
pass. Not run: CI itself, codefly build phases, e2e.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
crisfiorini-oss
pushed a commit
to crisfiorini-oss/module-saas-starter
that referenced
this pull request
Oct 13, 2026
…mer names the same graphs (codefly-dev#1050) (codefly-dev#1051) * feat(frontend): publish the kit's chart vocabulary as an A2UI catalog (codefly-dev#1050) `@codefly-dev/ui/dashboard` draws five visualizations, and that list was a convention restated in three packages with nothing checking they agreed. The kit now publishes it: an A2UI catalog naming the dashboard components, and the data model those bindings resolve against, both generated from the kit's own types and type-checked against them. - `@codefly-dev/ui/dashboard-catalog.json` — `Dashboard`, `DashboardGrid` and the four standalone metric charts, pinned at `protocolVersion: "0.9"` because the published renderers expose no v1.0 client. - `@codefly-dev/ui/dashboard-data.schema.json` — `DashboardView` and the shapes beneath it, bound by reference, carrying no numbers. A second document because A2UI closes a catalog's root keys. - The component map is `Record<WidgetVisualization, string | null>` and the schema's properties `Record<keyof Required<DashboardView>, …>`, so a visualization or a view field the documents do not describe fails to build. - `table` maps to `null`: `<Dashboard>` draws it and the kit exports no standalone table chart, so the gap is declared rather than silent. No new chart components and no change to how anything draws. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(kit): the dashboard catalog declares the fields DashboardView and DashboardWidgetView gained Merging main into this branch leaves `@codefly-dev/ui` unable to build: src/dashboard/catalog.ts(195,3): error TS1360 Property 'sections' is missing in type … but required in type 'SchemaProperties<DashboardView>' That is the catalog's own invariant firing, not a merge artefact. `SchemaProperties<T>` maps over `keyof Required<T>`, so the published A2UI data model must name EVERY field of the view types — which is the point: a field added to the renderer's types without a catalog entry does not compile, so the published vocabulary cannot silently fall behind what the renderer draws. Main added sections (codefly-dev#958's declared sections a viewer can arrange) and the scalar/placement fields alongside them, while this branch was writing the catalog, so the two met here. Declared now: - `DashboardView.sections`, an array of a new `DashboardSectionView` `$def` (`id`, `title` required; `description` optional). - `DashboardWidgetView.section`, naming the section a widget is drawn in. - `DashboardWidgetView.failure`, as the `WidgetReadFailure` enum. Its description keeps the distinction the type's own comment insists on: "failed" says the read did not complete and says nothing about authority, so a series already in hand is still drawn as stale, while "refused" says this viewer may not see the number, which makes a previously fetched one wrong rather than old. They are different questions, not a severity ordering. - `DashboardWidgetView.format`, `unit` and `higherIsBetter`, the scalar presentation fields. `WIDGET_READ_FAILURES` and `METRIC_FORMATS` are declared through `closedSet`, the way `DASHBOARD_LAYOUTS` already is, rather than as bare arrays. That is load-bearing: `closedSet` refuses a list that has dropped a member, so a new `WidgetReadFailure` or `MetricFormat` fails at the catalog instead of publishing an enum that no longer spans its union. `dashboard-data.schema.generated.json` is REGENERATED, not edited -- `npm run generate:dashboard-catalog --workspace @codefly-dev/ui` -- and the diff is additive: +54 lines, the fields above and the new `$def`. `check:dashboard-catalog` passes, which is what pins the published bytes to what catalog.ts renders. Also resolved in this merge: `src/dashboard/index.ts`, where main widened the `dashboard.js` export with the section and widget components while this branch added the `catalog.js` block. Neither side touched the other's, so the resolution is the union. Tests: frontend `--project pure` 4759/4759 across 336 files, `@codefly-dev/ui` 1478/1478 across 47 files, the dashboard and catalog suites 188/188, `check:dashboard-catalog`, `tsc --noEmit` and `prepare:frontend` all exit 0, eslint 0 errors. kit-version, module-verify, check-boundaries and naming-gate pass. Not run: CI itself, codefly build phases, e2e. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Important
Maintainer update: rebuilt on #1043's renderer (2026-10-11)
Latest maintainer head:
d7b490183(review fixes, directly below). The first round starts at "What happened".Everything between this note and the horizontal rule below was added by a maintainer. The author's original description follows the rule, unchanged; it describes the branch before this update, so read its renderer sections (
WidgetChart,MetricFigure, the BarList restyle, the lone-series colour,formatShare) as history.Review fixes (second maintainer round, on top of
31cfe26d7)An independent deep review at
31cfe26d7returned LAND, 0 blockers. It found #1043 intact (kit.test.tsidentical, refusal deletion holding on the new + menu path too), the percent share scaled exactly once, and nothing of the author's work lost. It asked for the fixes below. Each fix has tests, and each mechanism was removed in turn to confirm a test fails without it.fe679cc43postgres_audit.go), keyed…T00:00:00+00in any session zone. A session-zone key's offset moves across daylight saving, so a day the SDK fills could not know its offset, and the kit, which labels in UTC, already read a+02midnight as the previous day. A UTC session (the default) gets identical keys.TestTimeBucketKeysAreUTCInAnySessionZoneruns against real Postgres underEurope/Parisacross the 2026 change and fails with the old expression.50322d093• M1: a complete, time-grouped count / distinct count / sum is fillable, and a derived metric reads its missing day as that input's
0. A rate whose numerator starts later, or never happens, now reads0and stays complete. Keys go in time order, the days between two inputs' ranges are combined, and a sum or difference of fillable inputs can feed a rate.• m2: the fill runs through the end of the range, the bucket holding
to, or today for an open query.• m1: keys that disagree about their offset are left unfilled rather than given a guessed one.
442896fc0<h4>, under the section's<h3>; outside sections it stays<h3>.CardgainedtitleAs,StatTilelabelAs="h4",DashboardWidgetheading, andrenderWidgeta second argument{ heading }. StoriesDashboardSectionsandSectionHeaderwere added, plus a kit README note of what a remote sees in 0.12.7.0afad20aa6c96c133f@codefly/saas-plugin-manifest0.1.2, with the root pin and the lockfile.41766e2a2AUDIT_METRICS.mdanddocs/plugin-manifest-schema.md.One deliberate divergence from the review. Its test "gives a filled day the offset the server would have given it" expects the SDK to infer
+02for 30 March from returned keys carrying+01and+02. The keys alone cannot settle that: 29 March is+01, 30 March is+02, and nothing in them says where the change falls. So the fix is at the owner: UTC keys, constant by construction. The SDK refuses to fill mixed offsets, and a test pins that refusal. Run unchanged against this tree, the review's own suite passes 65 of 67. The 2 failures are this test, which exists in two of its files.A behaviour change worth knowing. "Telemetry incomplete." no longer appears for a difference or rate whose count input simply had no events; that now reads
0. It still appears when an input is partial or has no value over no events, such as an average. The host test was moved to that case. A series now ends on the current, in-progress bucket, so its value there is the value so far.Release note (kit 0.12.7, for remotes):
format,compactandcurrencyincluded, now reaches chart axes, readouts, data tables and table cells; before, only scalars used it.<h4>.renderWidgetreceives{ heading }.CardhastitleAs.SortableBoardis exported.Still open, stated rather than fixed (m4, m7):
row-span-2without dense packing fills[chart, number, number]. It still leaves an empty cell under the number in[number, chart], and a chart-sized empty cell beside the chart in[number, number, chart]. Dense packing would break the viewer's order, so the remaining layout model (span,columns) is A solution's dashboard cannot express layout, and the grid renders it badly: giant scalar values, dead regions beside short widgets, and buggy reordering #1044 / fix(dashboard): additive layout controls, compact defaults and stable ordering #1045's. This is reasoned from CSS grid placement; happy-dom has no layout and it was not seen in a browser.sectionoptional on a sectioned dashboard (this validator requires it), adds per-sectioncolumns, and says a preference "cannot change membership" (this PR lets a viewer move tiles between sections). Whichever lands second reconciles, after the owner rules on cross-section moves.DashboardView.sections,DashboardWidgetView.sectionandWidgetHeading's use inrenderWidget, beside Draw every dashboard with the kit's metric tier, and refuse re-implementing it (#1041) #1043'sfailure,formatin percentage points,unit,higherIsBetterand nullableseries. Nothing in the repo enforces that today; a type-level exhaustiveness test in Publish the kit's chart vocabulary as an A2UI catalog, so every consumer names the same graphs (#1050) #1051 would.SortableBoard(ordered swap grid in groups; the caller supplies the keyboard path, as the host does with arrow keys) sits beside feat(ui): Markdown says which bytes it rendered, and a generic Board whose moves the consumer owns #1014'sBoard(unordered columns with a Move menu). An arrow-key move within a section is not announced; that was already true in Draw every dashboard with the kit's metric tier, and refuse re-implementing it (#1041) #1043.Verification at the new head (Node 24.21.0; frontend steps from
module/services/frontend/code):npm cipassed.tsc --noEmitexited 0.npm run lintreported 0 errors and 39 warnings, main's count.npm run test:purepassed 322 files, 4,467 tests.npm run buildexited 0.--checkpassed.test:published-uiandcheck:plugin-allowlistpassed.go test -race ./pkg/infra/auditmetricstestpassed againstpostgres:17-alpinewithAUDIT_METRICS_REQUIRE_DB=1, as CI runs it, andgo test -tags pure ./pkg/infrapassed..review-958suite, run unchanged against this tree, passed 65 of 67; the 2 failures are the divergence above.Not verified:
codefly ci runwas not run, nor was thepipelineproject.auditmetricstestand the purepkg/infratests were not run locally. Their sessions run in UTC, where the keys are unchanged.What happened
#1043 merged as
0f696ba3f, by owner decision, instead of this PR's renderer. It rewrote the same files (the kit'sdashboard.tsx,types.ts,metric-tiles.tsx,index.ts,charts.tsx,SolutionDashboard.tsxand its test, the package manifests). Its design is the base and stays: the kit's<Dashboard>draws the metric tier,SolutionDashboardcomposes it through slots, the checker-backed gate insrc/components/__tests__/kit.test.tsrefuses a second renderer,failure?: "failed" | "refused", refusals that delete retained snapshots at the read, availability apart from freshness, the accent on--chart-1, titled scalars as<h3>.origin/mainwas merged into the branch (43f97c027); nothing was rebased or force-pushed, and the author's commits are untouched. Every conflict was resolved to main's side, and this PR's features were then rebuilt on #1043's renderer in five commits on top:43f97c027main, conflicts resolved to #1043 (does not type-check alone; says so)c779cc96398564bf44DashboardSectionHeader, every mark written in the widget's format7e2b3faae95766c12531cfe26d7Dropped, because #1043 already does it or contradicts it
widget-chart.tsx(WidgetChart): a widget-to-mark dispatch, which is<Dashboard>'s job and what the gate refuses. Itsui-inventory.jsonentry goes too.MetricFigure, and the host'sWidgetCard/WidgetBody/WidgetValues: the kit'sStatTile/DashboardWidgetdraw these now.chartSeriesColor(i, count)→--chart-5, accent on--chart-5): contradicts Draw every dashboard with the kit's metric tier, and refuse re-implementing it (#1041) #1043's accent, which is the first series'--chart-1.formatShareand theformatValueprops on the deprecated basic charts, and the audit page'sBarListrestyle with itsBarsremoval: they served the old percent path and the "match the audit log" goal that Draw every dashboard with the kit's metric tier, and refuse re-implementing it (#1041) #1043 reached by moving both pages onto the metric tier.Kept as the author wrote it
The manifest schema, validator and conformance tests (
Dashboard.sections,MetricWidget.section, unique ids, a widget's section must be declared, nosectionon a dashboard without sections, metricformat); the per-day zero fill inrunMetricand the derived-metric handling of filled zeros;dashboard-layout.tsversion 2 and its tests; the kit'sDropdownMenuItemhint; theSortableGroupsandMenuHintsstories.SortableBoardis kept in the kit. It does not duplicate #1043's renderer: it is the kit's ownSortableGridgeneralised to several groups (SortableGridis now a one-group board with unchanged props), and it is reached through #1043'slayoutslot. One overlap a reviewer may want to rule on: the kit's./boardBoard(#1014) also moves cards between groups, but it is unordered columns keyed by a field with no swap and no group reordering, so neither can stand in for the other today.Rebuilt on #1043's renderer
types.ts):DashboardView.sections?: DashboardSectionView[]({ id, title, description? }, drawn in the order listed) andDashboardWidgetView.section?: string;fromDashboardDatacarries both.dashboard.tsx): the default layout heads each section with the new exportedDashboardSectionHeader(an<h3>under the dashboard's<h2>), lays each section's widgets out exactly as Draw every dashboard with the kit's metric tier, and refuse re-implementing it (#1041) #1043 does, draws a widget naming no section first and untitled, and keeps a declared section with no widgets ("No widgets in this section."). A view with sections never shows the "No widgets" empty state, and thelayoutslot is called with each tile'ssection. A view without sections renders as before (tested).SolutionDashboard.tsx): the viewer's layout is resolved into the view the kit draws (the viewer's sections, order and titles; each widget in the section the viewer put it in). The host's controls go into the slots:layoutis the kit'sSortableBoard, each group headed byDashboardSectionHeaderwith the grip, the rename title, the section's + and its options menu;renderWidgetis still Draw every dashboard with the kit's metric tier, and refuse re-implementing it (#1041) #1043'sSolutionWidgetmountingDashboardWidget;actionsis the dashboard's + when it declares no sections. Moving tiles across sections, renaming, reordering and removing sections, adding empty ones, per-section + menus offering only removed declared widgets, the muted "No data yet." / "Telemetry incomplete." hints, and the arrow-key announcements all work as the author described. The layout remains under the existing STOPGAP browser-storage label.useWidgetSeries, the same hook and key as the tile's (and so the same refusal handling that deletes snapshots), enabled only while the menu is open.The percent contract (decided here)
There is one conversion boundary, and both meanings are now explicit:
format: "percent"(manifest and SDK schema) means the metric's value is a share from 0 to 1, because that is what a ratio produces and the host cannot infer it.DashboardWidgetView.format: "percent"means the value is already in percentage points (unchanged from Draw every dashboard with the kit's metric tier, and refuse re-implementing it (#1041) #1043; its test still pins 45 → "45%").runDashboardis the only place they meet. A widget whose metric is declaredpercentcarries its points and total multiplied by 100 andformat: "percent"(ResolvedWidget.format: WidgetValueFormat);byMetrickeeps the shares.runDashboardis the only producer ofResolvedWidget, so the host and a remote usingfromDashboardDataboth receive scaled values, and nothing after it scales again.formaton a chart's axis, readout and data table and on a table's values throughformatMetricValue, the functionStatTilealready uses. Before, only scalars honoured it.The branch before this update put the share meaning on
ResolvedWidget.format, whichfromDashboardDataforwards structurally, so a remote would have drawn 0.4 as "0.4%". This change fixes that. The scaling does not live in the kit because the kit'spercentalready means percentage points for everyStatTilecaller. It does not live infromDashboardDatabecause the host never calls it.Regression tests: the SDK resolves a 2/5 ratio to 40 on the widget and 0.4 on
byMetric. The kit writes "40%" in the line, area and bar data tables and in a table, and leaves a plain number alone. The host renders a declared percent ratio as "40%" on the tile, the bar chart's data table and a table widget through the real SDK path, a plain ratio as 2.5, and never "0.4" or "0.4%". Removing the SDK scaling reds the SDK test and the host test, and removing the kit formatter reds four kit tests and the host test.Two-row tiles, and #1044 / #1045
The rule is a host-only
itemClassNameon theSortableBoardgrid. A line, area, bar or table tile getsrow-span-2and every tile fills its cell, so two number tiles stack beside a chart; a stack layout is untouched. The kit's default layout geometry is unchanged, and this PR adds nospanorcolumnsto the manifest, so it does not touch #1044's layout model in code. Removing the class reds the host test.Open PR #1045 (for #1044) proposes the same
sections/sectionvocabulary plusDashboard.columns,MetricWidget.spanand per-sectioncolumns. It also states that a preference "cannot change membership" of a section, which contradicts this PR's viewer arrangement as specified. Whichever lands second has to reconcile that rule, its lenient-validator change against this strict validator, and the{ id, title, description? }section shape, which both PRs already share.Obligation for #1051 (the A2UI catalog)
This PR grows the kit's view model again. #1051's
catalog.tsand data schema must describe:DashboardView.sections?: { id, title, description? }[]andDashboardWidgetView.section?: string(new here);failure?: "failed" | "refused",format(the kit'sMetricFormat, withpercentin percentage points and now applied to every mark, not just scalars),unit,higherIsBetter, andseries: WidgetSeries | null. Its schema tests must cover the nullability and the acceptedfailurevalues.The SDK side also gained
ResolvedWidget.section,DashboardData.sectionsandWidgetValueFormat.Classification
runDashboard: a fix at the owner. The SDK is the only producer that knows the data graph's share meaning.user_settingsfield still missing).ALLOWEDorDISPATCHERSentries, andkit.test.tsis byte-identical to main.Verification (Node 24.21.0,
module/services/frontend/code, at31cfe26d7)npm cipassed.npx tsc --noEmitexited 0.npm run lintreported 0 errors and 39 warnings, the same count as main and none in changed files.npm run test:purepassed 321 files, 4,444 tests. Among them,kit.test.tspassed 64 tests and the host'sSolutionDashboard.test.tsxpassed 67: Draw every dashboard with the kit's metric tier, and refuse re-implementing it (#1041) #1043's 36 plus the ported ones.npm run build(next build) exited 0.codefly-uipackage suite passed 1,441 tests,saas-sdkpassed 95, andsaas-plugin-manifestpassed 141.npm run test:published-uiandnpm run check:plugin-allowlistpassed.SortableBoard, the menu hint and the section layout.node module/tools/naming-gate.mjs check,scripts/check-boundaries.sh,node scripts/ci/kit-version.mjs check,node --test scripts/ci/kit-version.test.mjs(16) andscripts/ci/release-gates.test.mjs(111),module/tools/module-verify.mjs,interface-docs-gate.mjs check, andstory-trace-gate.mjs tests.mainalready carries the unreleased@codefly-dev/ui/@codefly-dev/saas-ui0.12.7 and@codefly-dev/saas-sdk0.3.25, above what v0.0.94 published, and the kit-version gate passes.@codefly/saas-plugin-manifeststays at main's 0.1.1.Host tests ported from the author's branch were adapted where they read the old card markup. Section headings are now found by the kit's
dashboard-section-titleslot. A tile's emptiness is read from the kit's "No data" / "Partial data" badges rather than the old card's sentences. The test that titled a number tile like the audit log was dropped, because #1043'sStatTileowns that title.Not verified
SortableGroupsstory are covered only by happy-dom tests.codefly ci run, the canonical service gate, was not run. Neither was thepipelinevitest project, which needs live services.Docs updated
module/AUDIT_METRICS.md: the zero fill and its limits, the percent boundary, and how sections are drawn.module/docs/plugin-manifest-schema.md: the percent boundary and the zero fill besideformat. Sections were already described there.module/services/frontend/AGENTS.md: the viewer's arrangement through the slots, and why a widget's format is bound from the resolved answer.packages/codefly-ui/README.md: sections,DashboardSectionHeader,SortableBoard, and the format reaching every mark.Summary
A solution can now group its dashboard's widgets into titled sections, declared in its manifest's data graph:
Dashboard.sections({ id, title, description? }[], drawn in declared order) andMetricWidget.section. Until now the validator refused the key (dashboard has unknown field 'sections'), so/api/solutions/registeranswered 422invalid_manifest.This fits ADR 0007. The solution declares the sections. A viewer only groups and arranges the tiles in their own view: they move tiles between sections, rename, reorder and remove sections, and add empty sections of their own that they fill by dragging tiles in. Each + menu (the dashboard's, and each section's) still offers only the declared widgets the viewer removed, and a section's + puts the widget back into that section. No preference adds a metric the dashboard has no widget for. Beside each removed widget, a + menu now says, muted, when its tile would have nothing to show ("No data yet.", or "Telemetry incomplete." for a derived metric missing an input), and it asks only once it is opened. The layout stays in browser storage under the existing STOPGAP label.
A metric may also name a
format.percentreads its value as a share from 0 to 1 and writes it as a percentage, so a rate of 0.4 reads "40%" instead of "0.4". The host cannot infer this, because a ratio is not always a share ("versions per memo" is one).number, the default, writes the value as it is.A per-day count now shows a 0 on each day with no events. Before, those days vanished and the line ran straight over them, so ten events on ten days of a 24-day span drew as a flat line at 1.
A chart, bar list or table tile is now two rows tall, so two number tiles stack beside it. Before, a number beside a chart left about 160px empty under it.
A solution dashboard now draws its tiles with the same charts as the audit log page. Before, a solution dashboard drew with the kit's first chart set: one colour, no readout when you hover, no legend. The audit log page draws with the metric charts. Both pages now draw with the same components.
A dashboard without sections, or a metric without a format, validates, renders and saves as before.
Changes
Manifest schema and validator (
feat(manifest))saas-plugin-manifest/src/data-graph.tsgainsDashboardSection,Dashboard.sectionsandMetricWidget.section. The validator checks each section's shape and that section ids are unique within a dashboard. A widget must name a declared section when its dashboard declares sections, and must name none when it declares none. An emptysectionslist is refused.plugin.codefly.schema.jsoncarries the same field rules, including the cross-field rule (anif/then/elseonsections). The conformance suite checks that the schema and the validator agree on acceptance and on five new rejections.saas-sdk/src/schema.tsmirrors the same shapes, and both packages export the section types.docs/plugin-manifest-schema.mddocuments the new keys and cross-field rules.dashboard.events(for declared audit types), and the dashboard-authoring spec (features/dashboard/model/validate.ts) is a separate shape. No Python code reads the data graph.Kit:
SortableBoard(feat(ui))codefly-ui/src/dashboard/sortable-grid.tsx:SortableBoardholds tiles in groups under one drag context. You can swap tiles across groups. WithonMove, a slot at the end of each group appears during a drag. WithonMoveGroup, a group is dragged by a handle in its header. The groups sit in one column and only the pointer's height counts, so a drop above the first group lands before it and a drop below the last lands after it. A group can befixed, and the board glides groups and tiles into place when they move.SortableGridis now a board with one group; its props and behaviour are unchanged. It keeps main'srenderOverlayand its item list keyed by content, and both now also apply to the board.SortableGroupsstory, listed inui-inventory.json.Host dashboard (
feat(frontend))src/solutions/dashboard-layout.ts, layout version 2: a list of sections, each with its tiles and, if the viewer renamed it, the viewer's title. It also stores the widgets and sections the layout was made against (seen,seenSections), so a section or widget the solution declares later still appears, while one the viewer removed stays removed.metric:tile saved before ADR 0007 applied.custom:plus 16 hex characters. Titles are trimmed and capped at 60 characters, and a new section is titled "New section".src/solutions/SolutionDashboard.tsx:section, because its throwaway dashboard declares no sections.useWidgetSerieshook and query key, and only while the menu is open. It shows the tile's no-values rule (withoutValues, now shared by the tile and the menu) as a muted hint: "No data yet.", "Telemetry incomplete." (the tile says "Telemetry unavailable or incomplete.", or "Incomplete telemetry; total unavailable."), or "Total unavailable." for a number with no total across groups. It shows nothing while loading, on a query error, or when there is data, and the item stays selectable. The hint is a new optionalhintprop on the kit'sDropdownMenuItem. The kit knows nothing about metrics: the item keeps its label as its accessible name and uses the hint as its description. There is a newMenuHintsstory, listed inui-inventory.json.docs/plugin-manifest-schema.mdgains one paragraph on what viewers can do with sections.Metric value format (
feat(manifest))data-graph.tsandplugin.codefly.schema.jsonallowformat(numberorpercent) on source and derived metrics and refuse any other value. The conformance suite checks that both accept it and both refuse an unsupported value on either kind.saas-sdk/src/schema.tsmirrors the type asMetricValueFormat.runDashboardputs each widget's metric format on the resolved widget, and the kit'sfromDashboardDatacarries it toDashboardWidgetView.StatChart,BarList,LineChartandAreaCharttake an optionalformatValuefor the tile's number, each bar's value and the y-axis ticks. The newformatSharewrites a share as a percentage, to one decimal (0.4 → "40%", 17/22 → "77.3%"). The kit's shared<Dashboard>and the host'sSolutionDashboarduse it for a percent widget, tables included, and pass nothing for a plain number, so each chart keeps its own default. They now draw throughWidgetChart(see Same charts as the audit log), which passes it on; theformatValueprops on the old charts remain.docs/plugin-manifest-schema.mdnames the key.Per-day zeros (
fix(sdk))saas-sdk/src/datagraph/run.ts:runMetricfills a time-grouped count, distinct count or sum with a 0 at every bucket between the first and the last the RPC returned. The RPC returns no bucket for a day with no rows, and the kit's line chart spaces points by position, so with every bucket present its spacing is by date. Week and month buckets step by a week and a month.fix(sdk)). Before this, a ratio of two per-day counts read 0/0 on such a day and marked the whole series "Partial telemetry", and a count combined with an avg did the same.combineDerivednow treats a key where every operand is a filled zero, or missing, as a day with no events: a ratio there has no value and the series stays complete, and a sum or difference of the zeros is 0. A day with events in the denominator and none in the numerator reads 0.Arrow keys on a tile's grip (
fix(frontend))Two-row tiles (
feat(frontend))SortableBoardgains an optionalitemClassName(id): classes for one tile's list item, which is its group's grid item. It knows nothing about charts.SolutionDashboard.tsxgives a line, area, bar or table tilerow-span-2, and every cardh-full. The grid works out the row heights from what the tiles hold, so no height is fixed. The grid does not pack later tiles into earlier holes, so the tiles keep the viewer's order, and a section with an odd cell count still ends on a half-empty row.Same charts as the audit log (
fix(frontend))codefly-ui/src/dashboard/widget-chart.tsx:WidgetChartdraws one resolved widget with the metric charts. A line widget draws withMetricLineChart, an area widget withMetricAreaChart, a bar widget withBarList, a number widget withMetricFigure, and a table as a table. The kit's<Dashboard>and the host'sSolutionDashboardboth draw a tile's values with it; each still says for itself what a tile with no values shows.MetricFigure(metric-tiles.tsx) isStatTile's value, delta and sparkline without the card, for a tile that already has its own header.StatTilenow draws its body with it.BarList(charts.tsx) takes the audit log's look: the label and the value above the bar, a thinner bar. The audit page's top lists (src/components/dashboard.tsx) use it instead of a copy in the host, and that copy (Bars) is removed.LineChart,AreaChartandStatChartstay exported, marked@deprecated, with a pointer to the chart that replaces each.percent: the figure'spercentexpects 45 for 45%, and a percent metric is a share from 0 to 1.WidgetChartscales the share once, so a ratio of 0.4 reads "40%".SolutionDashboard.tsx) takes the size and weight the audit log'sStatTilelabels a figure with, the small body text, not a card heading, in the foreground colour rather thanStatTile's muted grey, which read too faint beside the figure. Chart, bar and table tiles keep the card heading, as the audit log's chart cards do. A declaredlinestill draws as a line andareaas a filled area: the manifest makes that the solution's choice, so the host does not draw a line as an area to match the audit log's chart.ui-inventory.jsongains an entry forwidget-chart.tsx, and the entry for the kit'sdashboard.tsximports it in place ofcharts.js.Review fixes (
fix(sdk),fix(frontend),fix(ui))toSeries(saas-sdk/src/datagraph/run.ts) now gives a series whose points are all filled zeros no total, as it had none before the fill.parseLayout(src/solutions/dashboard-layout.ts) fell back to the declared default and brought back every widget the viewer had removed. It now gets the default's first section back, puts the viewer's tiles there, and keeps their removals.--primary, which the bar list colours from, while a line or area widget's metric chart colours its one series from a chart token. The kit's<Dashboard>now sets both from the accent (--primaryand--chart-5, below).--chart-1, the lightest grey in the default skin's light-to-dark palette, so a solution's line was faint beside the audit log's chart, whose dark outline is its topmost series in the last colour.chartSeriesColor(metric-chart.tsx) now gives a lone series the palette's last colour; two series or more keep the palette order. This applies to every metric chart with one series, the audit log's included when it is filtered to one category.Versions:
@codefly-dev/uiand@codefly-dev/saas-uigo to 0.12.5 (they share a version) and@codefly-dev/saas-sdkto 0.3.17, one abovemain's, with the root pins and lockfile updated to match.@codefly/saas-plugin-manifeststays at 0.1.0: the kit-version gate does not cover it, and earlier changes to it were not bumped either.Testing
All runs used Node 24.17.0, from
module/services/frontend/codeunless another directory is named. The branch is rebased ontomainat 7fecf40; the rebase had no conflicts.npx vitest run --project pure: 294 files, 3742 tests passed. Two new host tests check that an arrow key that moves nothing leaves storage empty, and that an arrow-key move into another section is announced; both failed before the grip fix.packages/codefly-ui:npx vitest run src/dashboard: 9 files, 104 tests passed. One checks thatitemClassNamelands on the tile's list item; the host checks that a line and a bar tile span two rows and a number does not. The board tests cover:renderOverlayand its default.packages/saas-plugin-manifest:npx vitest run: 141 tests passed. Cases added:formaton a source and a derived metric, and both refuse an unsupported format on either.packages/saas-sdk:npx vitest run: 92 tests passed. They include one that checksrunDashboardcarries each widget's metric format, and none for a metric without one. Ten check the per-day zeros:With the fill switched off, the five tests that expect zeros fail and the rest pass.
One more test checks that a per-day ratio and a difference over a day no input had events on stay complete, and that a day with events only in the denominator reads 0. It failed before the derived-metric fix ("partial");
maingives "complete" for the same graph.formatSharewrites 0.4 as "40%", 17/22 as "77.3%", 0 as "0%" and 1 as "100%";StatChartandBarListwrite values with the caller's formatter, andLineCharthands it to its y axis;fromDashboardDatacarries the format, and the kit's<Dashboard>writes a percent number and percent bars as percentages and a plain number as a number;metric:tiles;SolutionDashboard.test.tsx, "what a + menu says about a removed widget", 6 tests) check that:I broke the feature on purpose and ran the tests, then restored it:
packages/codefly-ui: one new test insrc/layout/__tests__/compound-controls.test.tsxchecks that a hinted item keeps its label as its name and gets the hint as its description. It fails if the hint is not hidden from the name, or if the description link is removed. The same file had 7 tests pass.npx eslintover its changed files: 0 errors. For the earlier commits,npm run lint: 0 errors. The 30 warnings are all patterns already onmain(anonymous story default exports,react-hooks/incompatible-libraryon tables); none is in new code.npm run typecheck: passed. After the format commit,npx tsc --noEmitwith the packages rebuilt: passed.npx biome checkover the files the format commit changes: its new lines are clean. Every file with findings has the same count on the commit before it: the files named below, pluscodefly-ui/src/dashboard/charts.tsx,dashboard.tsx,format.tsand__tests__/charts.test.tsxand__tests__/types.test.ts, which carry long lines and import order frommain. For the earlier commits, over their changed files: 17 of 24 files are clean. The other seven (codefly-ui/src/dashboard/index.ts,saas-sdk/src/index.ts,plugin.codefly.schema.json,ui-inventory.json,src/solutions/__tests__/registry.test.ts,codefly-ui/src/layout/__tests__/compound-controls.test.tsx,codefly-ui/stories/primitives.stories.tsx) fail the same way onorigin/main: whole-file import ordering or formatting, and in the last two, lint findings on lines this PR does not touch. The new lines follow each file's existing style.module/services/frontend/codeunder Node 24:npx vitest run --project pure: 294 files, 3747 tests passed. The host test that read a tile's title now reads the tile's own title, because a chart also names its hidden data table after it.npm run typecheck: passed.npm run test:published-ui: passed.npx eslintover the changed files: clean.npx biome check: no new findings beyond those already on the unchanged files. OnenoArrayIndexKeyfinding moved with the bar list from the host into the kit.npm run check:plugin-allowlist: passed.npm run prepare:frontendleaves the tree clean.npm run test:published-ui: passed ("Packed UI declarations and generic consumer rendering passed").node --test scripts/ci/kit-version.test.mjspassed (16 tests), andnode scripts/ci/kit-version.mjs checkpassed, reporting@codefly-dev/ui@0.11.5, @codefly-dev/saas-ui@0.11.5, @codefly-dev/saas-sdk@0.3.16.scripts/check-boundaries.sh: clean.node module/tools/naming-gate.mjs check: clean. Themessagemode passed on each commit message and on this body.percent, the solution registered and the rates read 40%, 77.3%, 71.4%, 77.3%, 81.1% and 81.8%, while "versions per memo" read 2.227;npx vitest run --project pure: 310 files, 4072 tests passed. Before the stylesheet commit, the preview-stylesheet test failed twice (stale, and a missingopacity-70); it passes onmainalone.npm run typecheck,npm run test:published-ui,npm run check:plugin-allowlist, the kit-version gate, the naming gate andscripts/check-boundaries.shpassed.main's copies of those files, none new.@codefly-dev/uiand@codefly-dev/saas-ui0.12.5,@codefly-dev/saas-sdk0.3.17, each one abovemain's; the lockfile ismain's, regenerated.mainagain (its shared navigation and lifecycle primitives; the only extra conflict wasui-inventory.json, where both entries are kept), under Node 24:npx vitest run --project pure: 312 files, 4146 tests passed; typecheck, published-ui, plugin allowlist, kit-version gate, naming gate andscripts/check-boundaries.shpassed; eslint and biome report nothing new.var(--chart-5)and two series keep--chart-1and--chart-2; it fails without the change.npx vitest run --project pure: 312 files, 4148 tests passed; typecheck andnpm run test:published-uipassed; eslint is clean and biome reports nothing new. Checked on the local stack beside the audit log.npx vitest run --project pure: 310 files, 4076 tests passed;npm run typecheckand the naming gate passed; eslint and biome are clean on the two changed files. Checked on the local stack at desktop width beside the audit log.--chart-1on the accented dashboard) and passes with it.npx vitest run --project pure: 310 files, 4075 tests passed.npm run typecheck,npm run test:published-ui, the kit-version gate and the naming gate passed; eslint is clean on the changed files and biome reports nothing new on them.Not verified
SortableGroupsstory in the explorer are unexercised outside happy-dom.Known behaviour
Open questions
user_settingsfield lands, the version-2 shape moves there as it is. There is no tracking issue for that field yet.🤖 Generated with Claude Code