diff --git a/.changeset/24126-system-overview-metric-height.md b/.changeset/24126-system-overview-metric-height.md new file mode 100644 index 00000000000..e5c4a845abc --- /dev/null +++ b/.changeset/24126-system-overview-metric-height.md @@ -0,0 +1,14 @@ +--- +'@objectstack/platform-objects': patch +--- + +Clause-②: no + +fix(platform-objects): the System Overview dashboard's six metric tiles declare `layout.h: 1`, one row tall, where they declared `2` + +The `system_overview` dashboard the Setup app ships (`SystemOverviewDashboard`) authored every one of its six `type: 'metric'` tiles (Total Users, Organizations, Active Sessions, Packages Installed, Login Events, Config Changes) at `layout.h: 2`. The console draws a widget over the grid rows its `h` names, each row floors at 5rem and grows to its content, and a metric card fills its box, so each tile drew a box about twice the height of its content: the title and the number in the top half, the lower half empty. The platform draws what the metadata declares, so the authored size is what is corrected. + +- **FROM:** the six metric tiles declare `layout: { …, h: 2 }`. +- **TO:** the six metric tiles declare `layout: { …, h: 1 }`; each now draws at content height. +- **Unchanged:** every `x`, `y` and `w`, the chart and table widgets (`h: 4`), every widget id, filter and binding, and every export. +- **An org that already published an overlay of `system_overview`** is served that overlay as stored (ADR-0126 Regime O), so it keeps the tile heights it was saved with. diff --git a/packages/platform-objects/src/apps/dashboards/system-overview-tile-layout.test.ts b/packages/platform-objects/src/apps/dashboards/system-overview-tile-layout.test.ts new file mode 100644 index 00000000000..370dfcd5ebf --- /dev/null +++ b/packages/platform-objects/src/apps/dashboards/system-overview-tile-layout.test.ts @@ -0,0 +1,72 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// System Overview metric tile height (the maintainer's ruling B on +// objectstack-ai/objectui#12704). +// +// The console draws a dashboard's positioned grid with `gridRow: span h` over +// rows that floor at 5rem and grow to their content, and a metric card fills +// its box. A KPI tile that declares `h: 2` therefore draws a box about twice the +// height of its content: the title and the number sit in the top half and the +// lower half is empty. The ruling is that the platform draws what the metadata +// declares (ADR-0078 forbids an `h` that is read and not honoured), so the +// AUTHORED size is what gets corrected, not the renderer: every metric tile on +// the shipped board is ONE row tall. +// +// What is pinned is the property "a KPI tile is one row", read off `type` +// rather than off a list of ids, so a metric tile added to the board later is +// held to it as well. Only `h` is pinned. `x`, `y` and `w` place the tile and +// are a separate decision this file has no opinion on. +// +// The pin carries its OPPOSITE DIRECTION, because "every metric tile is one row" +// has two ways to pass vacuously: +// +// - it also holds if the read finds no metric tile at all (a renamed key, a +// board that moved elsewhere) ⇒ the six tiles are named and the read must +// return exactly them; +// - it also holds if the fix were a blanket flattening of the whole board ⇒ +// the chart and table widgets must stay taller than a tile, because they +// are the widgets that genuinely need the rows. + +import { describe, it, expect } from 'vitest'; +import { SystemOverviewDashboard } from './system_overview.dashboard.js'; + +interface Widget { + id?: string; + type?: string; + layout?: { x: number; y: number; w: number; h: number }; +} +const widgets: Widget[] = (SystemOverviewDashboard as unknown as { widgets?: Widget[] }).widgets ?? []; + +const metricTiles = widgets.filter((w) => w.type === 'metric'); +const otherWidgets = widgets.filter((w) => w.type !== 'metric'); + +describe('System Overview metric tiles are one row tall', () => { + // Control for the assertion below: if this read ever returns nothing, the + // `h` check is a loop over an empty array and passes whatever the board says. + it('the read finds the six KPI tiles', () => { + expect(metricTiles.map((w) => w.id)).toEqual([ + 'widget_total_users', + 'widget_organizations', + 'widget_active_sessions', + 'widget_packages_installed', + 'widget_login_events', + 'widget_config_changes', + ]); + }); + + it('every metric tile declares layout.h of 1', () => { + const offenders = metricTiles + .filter((w) => w.layout?.h !== 1) + .map((w) => `${w.id} → layout.h=${String(w.layout?.h)}`); + expect(offenders, 'metric tiles taller than one row').toEqual([]); + }); + + // Opposite direction: the correction is for the KPI tiles, not the board. + it('opposite direction: the chart and table widgets stay taller than a tile', () => { + expect(otherWidgets.length, 'the board still carries non-metric widgets').toBeGreaterThan(0); + const flattened = otherWidgets + .filter((w) => !(typeof w.layout?.h === 'number' && w.layout.h > 1)) + .map((w) => `${w.id} → layout.h=${String(w.layout?.h)}`); + expect(flattened, 'non-metric widgets that lost their height').toEqual([]); + }); +}); diff --git a/packages/platform-objects/src/apps/dashboards/system_overview.dashboard.ts b/packages/platform-objects/src/apps/dashboards/system_overview.dashboard.ts index 3444605e8a6..53aa021f461 100644 --- a/packages/platform-objects/src/apps/dashboards/system_overview.dashboard.ts +++ b/packages/platform-objects/src/apps/dashboards/system_overview.dashboard.ts @@ -42,7 +42,7 @@ export const SystemOverviewDashboard = Dashboard.create({ dataset: 'sys_user_metrics', values: ['user_count'], title: 'Total Users', type: 'metric', - layout: { x: 0, y: 0, w: 3, h: 2 }, + layout: { x: 0, y: 0, w: 3, h: 1 }, // #7531 — a TOTAL must not move when the date bar moves. The dashboard's // `created_at` global filter is broadcast into every widget's analytics // query (#2501), so before this opt-out the tile reported "users created @@ -67,7 +67,7 @@ export const SystemOverviewDashboard = Dashboard.create({ // `requiresService: 'org-scoping'` — gate this KPI the same way so the // overview doesn't dangle a metric the admin can't act on. requiresService: 'org-scoping', - layout: { x: 3, y: 0, w: 3, h: 2 }, + layout: { x: 3, y: 0, w: 3, h: 1 }, // #7613 — the same #2501 fan-out as Total Users above, on the same row. // `sys_organization.created_at` exists, so the date bar landed on it and // the tile reported "organizations created in the last 7 days" under a @@ -83,7 +83,7 @@ export const SystemOverviewDashboard = Dashboard.create({ dataset: 'sys_session_metrics', values: ['session_count'], title: 'Active Sessions', type: 'metric', - layout: { x: 6, y: 0, w: 3, h: 2 }, + layout: { x: 6, y: 0, w: 3, h: 1 }, // #7531 — "Active Sessions" counted EVERY `sys_session` row: the dataset // is a bare count and the widget carried no predicate, so a signed-out or // long-expired session was still reported as active. `sys_session` can @@ -113,7 +113,7 @@ export const SystemOverviewDashboard = Dashboard.create({ // Cloud-only object — only registered when service-tenant is loaded. // Hide this widget gracefully in single-environment runtimes. requiresObject: 'sys_package_installation', - layout: { x: 9, y: 0, w: 3, h: 2 }, + layout: { x: 9, y: 0, w: 3, h: 1 }, // Which installations count: the ones that are actually installed. This // predicate is the tile's own, and it is ORTHOGONAL to the opt-out below // — the two answer different questions and both stand. @@ -162,7 +162,7 @@ export const SystemOverviewDashboard = Dashboard.create({ dataset: 'sys_audit_log_metrics', values: ['event_count'], title: 'Login Events', type: 'metric', - layout: { x: 0, y: 2, w: 6, h: 2 }, + layout: { x: 0, y: 2, w: 6, h: 1 }, filter: { action: 'login' }, colorVariant: 'blue', description: 'Authentication events recorded by the audit log', @@ -172,7 +172,7 @@ export const SystemOverviewDashboard = Dashboard.create({ dataset: 'sys_audit_log_metrics', values: ['event_count'], title: 'Config Changes', type: 'metric', - layout: { x: 6, y: 2, w: 6, h: 2 }, + layout: { x: 6, y: 2, w: 6, h: 1 }, filter: { action: 'config_change' }, colorVariant: 'blue', description: 'System configuration modifications',