Skip to content

Commit 783f714

Browse files
committed
fix(platform-objects): remove the permanently-empty permission_change dashboard tile
The System Overview board's "Permission Changes" tile filtered `sys_audit_log.action = 'permission_change'`, a value nothing in the repo has ever written — the only two audit writers are plugin-audit's generic hook (create/update/delete) and plugin-auth's admin user-import. The tile read `0` on every deployment that has ever existed, and #8147 then retired the value from the enum outright, leaving a filter no row can match. On a compliance surface an empty tile is worse than a missing one: "Permission Changes: 0" reads as a negative finding rather than an absent feature. - remove the tile; the two surviving Row 2 tiles split the 12-col row in half rather than leaving a hole where it sat - drop its title/description from all four locale bundles - stop naming `permission` among the example actions in the by-action tile's description (source + all four locales — the translations are the served text) - pin both directions: a tombstone for retired action values on the board, and the missing reverse direction in the app/dashboard translation parity test, which had no guard against a translation outliving its widget `import` is deliberately untouched: it was named in the same ruling but keeps a live writer and a shipped list view, so retiring it from the UI would produce the inverse defect. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XeQRiAa7vYRVX5Fog7Zby8
1 parent 6158146 commit 783f714

7 files changed

Lines changed: 115 additions & 26 deletions

File tree

packages/platform-objects/src/apps/dashboards/system-overview-tile-semantics.test.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,6 @@ describe('the dashboard filter the Row 1 inventory tiles opt out of', () => {
238238
it('still reaches every audit widget', () => {
239239
const auditWidgets = [
240240
'widget_login_events',
241-
'widget_permission_changes',
242241
'widget_config_changes',
243242
'widget_events_by_type',
244243
'widget_events_by_user',
@@ -250,6 +249,64 @@ describe('the dashboard filter the Row 1 inventory tiles opt out of', () => {
250249
});
251250
});
252251

252+
// ── Tombstone: no tile filters on a retired action value ────────────────────
253+
//
254+
// The board carried a "Permission Changes" tile filtering
255+
// `action: 'permission_change'` for its whole life. Nothing ever wrote that
256+
// value — the only two `sys_audit_log` writers are plugin-audit's generic hook
257+
// (`actionFor` maps afterInsert/Update/Delete to create/update/delete and
258+
// nothing else) and plugin-auth's admin user-import — so the tile reported `0`
259+
// on every deployment that has ever existed, and the value was then retired
260+
// from the enum outright. `export` retired alongside it.
261+
//
262+
// This is the same defect class as the rest of this file, one level up: not "the
263+
// query answers a different question from the label" but "the query can answer
264+
// nothing at all, under a label that implies it did". On a COMPLIANCE board the
265+
// empty tile is the more dangerous of the two — "Permission Changes: 0" reads as
266+
// a negative finding, not as an absent feature.
267+
//
268+
// ⚠️ `import` is NOT on this list and must not be added. It was named in the
269+
// same ruling but survives with a live writer (plugin-auth's admin user-import
270+
// writes a run-level row) and a shipped list view that filters it. Retiring it
271+
// from the UI while the platform still emits it would produce the inverse defect
272+
// — an action that can be written but not found.
273+
//
274+
// Why hard-coded rather than diffed against the enum: `sys_audit_log` lives in
275+
// `@objectstack/plugin-audit`, which this package does not depend on (the audit
276+
// objects moved OUT of here under ADR-0029 K2/D8) — and it must not start
277+
// depending on it for a test. Hard-coded ids checked one by one is the same
278+
// disposition `setup-nav-dead-key-tombstone.test.ts` records for the same
279+
// reason.
280+
describe('retired `sys_audit_log.action` values are gone from the board', () => {
281+
const RETIRED_ACTIONS = ['permission_change', 'export'];
282+
283+
const actionFilterOf = (w: { filter?: FilterCondition }): unknown =>
284+
(w.filter as Record<string, unknown> | undefined)?.action;
285+
286+
it('no widget filters on one', () => {
287+
const offenders = (board.widgets ?? [])
288+
.filter((w) => RETIRED_ACTIONS.includes(String(actionFilterOf(w))))
289+
.map((w) => `${w.id} → action=${String(actionFilterOf(w))}`);
290+
expect(offenders, 'widgets filtering a retired audit action').toEqual([]);
291+
});
292+
293+
it('and the removed tile itself is not back', () => {
294+
expect((board.widgets ?? []).map((w) => w.id)).not.toContain('widget_permission_changes');
295+
});
296+
297+
// Opposite direction. Both assertions above also pass on a board with no
298+
// widgets, or if `filter.action` stopped being where a tile's action
299+
// predicate lives — in which case they would be pinning nothing at all. A
300+
// LIVE action filter must still be visible through exactly the same read.
301+
it('opposite direction — a live action filter is still found by the same read', () => {
302+
const live = (board.widgets ?? [])
303+
.map((w) => actionFilterOf(w))
304+
.filter((a): a is string => typeof a === 'string');
305+
expect(live, 'the board still filters on live actions').toContain('login');
306+
expect(live).toContain('config_change');
307+
});
308+
});
309+
253310
// ── Tile 1: "Total Users" ───────────────────────────────────────────────────
254311

255312
describe('widget_total_users — "Total" means total', () => {

packages/platform-objects/src/apps/dashboards/system_overview.dashboard.ts

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { Dashboard } from '@objectstack/spec/ui';
1313
*
1414
* Layout (4 rows on a 12-col grid):
1515
* 1. Platform KPIs — users / orgs / sessions / packages
16-
* 2. Security KPIs — login / permission / config audit counts
16+
* 2. Security KPIs — login / config audit counts
1717
* 3. Distribution charts — audit events by action + by user
1818
* 4. Recent audit events table
1919
*
@@ -134,32 +134,45 @@ export const SystemOverviewDashboard = Dashboard.create({
134134
// successful logins (both fold into `action='login'`). Surfacing a
135135
// total Login Events count is honest; a "Failed Logins" widget will
136136
// need a richer enum or a separate detail field first.
137+
//
138+
// This row carried a THIRD tile, "Permission Changes", filtering
139+
// `action: 'permission_change'`. It is gone, and no replacement tile takes
140+
// its place. The value had no writer anywhere in the repo — the only two
141+
// `sys_audit_log` writers are plugin-audit's generic hook (whose `actionFor`
142+
// maps afterInsert/Update/Delete to create/update/delete and nothing else)
143+
// and plugin-auth's admin user-import — so the tile read `0` on every
144+
// deployment that has ever existed, and then its action value was retired
145+
// from the enum outright, leaving a filter no row can ever match. An empty
146+
// widget on a COMPLIANCE surface is worse than a missing one: an auditor
147+
// reading "Permission Changes: 0" concludes the platform watched for them
148+
// and found none, which is false. 审计面宁窄勿谎 — a narrow audit surface
149+
// beats a lying one.
150+
//
151+
// Not replaced by a refiltered tile, deliberately: permission and role
152+
// edits ARE captured, as ordinary `create`/`update` rows on the permission
153+
// objects written by the generic hook, so the honest lens on them is
154+
// `object_name` on the audit list view — a row-level question, not a
155+
// single-number KPI. Inventing a tile that approximates it here would put
156+
// a second not-quite-true number on the same board.
157+
//
158+
// The two survivors split the 12-col row in half (the Row 3 shape) rather
159+
// than leaving a 4-col hole where the removed tile sat.
137160
{
138161
id: 'widget_login_events',
139162
dataset: 'sys_audit_log_metrics', values: ['event_count'],
140163
title: 'Login Events',
141164
type: 'metric',
142-
layout: { x: 0, y: 2, w: 4, h: 2 },
165+
layout: { x: 0, y: 2, w: 6, h: 2 },
143166
filter: { action: 'login' },
144167
colorVariant: 'blue',
145168
description: 'Authentication events recorded by the audit log',
146169
},
147-
{
148-
id: 'widget_permission_changes',
149-
dataset: 'sys_audit_log_metrics', values: ['event_count'],
150-
title: 'Permission Changes',
151-
type: 'metric',
152-
layout: { x: 4, y: 2, w: 4, h: 2 },
153-
filter: { action: 'permission_change' },
154-
colorVariant: 'warning',
155-
description: 'Recent permission and role modifications',
156-
},
157170
{
158171
id: 'widget_config_changes',
159172
dataset: 'sys_audit_log_metrics', values: ['event_count'],
160173
title: 'Config Changes',
161174
type: 'metric',
162-
layout: { x: 8, y: 2, w: 4, h: 2 },
175+
layout: { x: 6, y: 2, w: 6, h: 2 },
163176
filter: { action: 'config_change' },
164177
colorVariant: 'blue',
165178
description: 'System configuration modifications',
@@ -195,7 +208,10 @@ export const SystemOverviewDashboard = Dashboard.create({
195208
{
196209
id: 'widget_recent_events',
197210
title: 'Audit Events by Action',
198-
description: 'Event volume grouped by action (login, permission, config, …)',
211+
// The example actions named here have to be actions the platform can
212+
// actually emit — this string used to lead with `permission`, which
213+
// advertised the retired value from a second place on the same board.
214+
description: 'Event volume grouped by action (login, logout, config, …)',
199215
type: 'table',
200216
dataset: 'sys_audit_log_metrics',
201217
dimensions: ['action'],

packages/platform-objects/src/apps/translations/app-nav-translation-parity.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,27 @@ describe('dashboard widgets are translated in every locale', () => {
111111
expect(missing, `untranslated widget titles in dashboards.${dashboard.name}`).toEqual([]);
112112
});
113113
}
114+
115+
// The reverse direction, for the same reason it exists for Studio's nav above:
116+
// a translation for a widget the board no longer declares is dead weight that
117+
// reads as coverage. This half was missing, and a removal proved why — when
118+
// `widget_permission_changes` was deleted from the board, its title and
119+
// description stayed behind in all four locales and every gate in this package
120+
// was green. A dashboard CAN be walked statically (unlike Setup, which is
121+
// composed at runtime — see `setup-nav-dead-key-tombstone.test.ts`), so there
122+
// is nothing here to stop the general claim being made.
123+
for (const [locale, data] of Object.entries(LOCALES)) {
124+
it(`${dashboard.name}${locale} carries no translation for a removed widget`, () => {
125+
const declared = new Set(
126+
(dashboard.widgets ?? []).map((w) => w.id).filter((id): id is string => !!id),
127+
);
128+
const translated = Object.keys(
129+
(data.dashboards?.[dashboard.name]?.widgets ?? {}) as Record<string, unknown>,
130+
);
131+
expect(
132+
translated.filter((id) => !declared.has(id)),
133+
`dashboards.${dashboard.name}.widgets keys with no declaring widget`,
134+
).toEqual([]);
135+
});
136+
}
114137
});

packages/platform-objects/src/apps/translations/en.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -211,10 +211,6 @@ export const en: TranslationData = {
211211
title: 'Login Events',
212212
description: 'Authentication events recorded by the audit log',
213213
},
214-
widget_permission_changes: {
215-
title: 'Permission Changes',
216-
description: 'Recent permission and role modifications',
217-
},
218214
widget_config_changes: {
219215
title: 'Config Changes',
220216
description: 'System configuration modifications',
@@ -229,7 +225,7 @@ export const en: TranslationData = {
229225
},
230226
widget_recent_events: {
231227
title: 'Recent Audit Events',
232-
description: 'Latest platform events (login, permission, config, …)',
228+
description: 'Latest platform events (login, logout, config, …)',
233229
},
234230
},
235231
},

packages/platform-objects/src/apps/translations/es-ES.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,11 +148,10 @@ export const esES: TranslationData = {
148148
widget_active_sessions: { title: 'Sesiones Activas', description: 'Número de sesiones de usuario activas en este momento' },
149149
widget_packages_installed: { title: 'Paquetes Instalados', description: 'Instalaciones de paquetes activas en los proyectos' },
150150
widget_login_events: { title: 'Eventos de Inicio de Sesión', description: 'Eventos de autenticación registrados por el log de auditoría' },
151-
widget_permission_changes: { title: 'Cambios de Permisos', description: 'Modificaciones recientes de permisos y roles' },
152151
widget_config_changes: { title: 'Cambios de Configuración', description: 'Modificaciones de configuración del sistema' },
153152
widget_events_by_type: { title: 'Eventos de Auditoría por Acción', description: 'Distribución de eventos de auditoría por tipo de acción' },
154153
widget_events_by_user: { title: 'Eventos por Usuario', description: 'Distribución de actividad entre usuarios' },
155-
widget_recent_events: { title: 'Eventos de Auditoría Recientes', description: 'Últimos eventos de la plataforma (inicio de sesión, permisos, configuración, …)' },
154+
widget_recent_events: { title: 'Eventos de Auditoría Recientes', description: 'Últimos eventos de la plataforma (inicio de sesión, cierre de sesión, configuración, …)' },
156155
},
157156
},
158157
},

packages/platform-objects/src/apps/translations/ja-JP.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,11 +148,10 @@ export const jaJP: TranslationData = {
148148
widget_active_sessions: { title: 'アクティブセッション', description: '現在アクティブなユーザーセッション数' },
149149
widget_packages_installed: { title: 'インストール済みパッケージ', description: 'プロジェクトでアクティブなパッケージインストール数' },
150150
widget_login_events: { title: 'ログインイベント', description: '監査ログに記録された認証イベント' },
151-
widget_permission_changes: { title: '権限変更', description: '最近の権限とロールの変更' },
152151
widget_config_changes: { title: '構成変更', description: 'システム構成の変更' },
153152
widget_events_by_type: { title: 'アクション別監査イベント', description: 'アクションタイプ別の監査イベント分布' },
154153
widget_events_by_user: { title: 'ユーザー別イベント', description: 'ユーザー別アクティビティ分布' },
155-
widget_recent_events: { title: '最近の監査イベント', description: '最新のプラットフォームイベント(ログイン、権限、構成など)' },
154+
widget_recent_events: { title: '最近の監査イベント', description: '最新のプラットフォームイベント(ログイン、ログアウト、構成など)' },
156155
},
157156
},
158157
},

packages/platform-objects/src/apps/translations/zh-CN.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,11 +158,10 @@ export const zhCN: TranslationData = {
158158
widget_active_sessions: { title: '活跃会话', description: '当前活跃用户会话数量' },
159159
widget_packages_installed: { title: '已安装包', description: '项目中已激活的安装包数' },
160160
widget_login_events: { title: '登录事件', description: '审计日志中记录的认证事件' },
161-
widget_permission_changes: { title: '权限变更', description: '最近的权限和角色修改' },
162161
widget_config_changes: { title: '配置变更', description: '系统配置修改' },
163162
widget_events_by_type: { title: '按操作分布的审计事件', description: '审计事件按操作类型分布' },
164163
widget_events_by_user: { title: '按用户分布的事件', description: '用户活动分布' },
165-
widget_recent_events: { title: '最近审计事件', description: '最新的平台事件(登录、权限、配置等)' },
164+
widget_recent_events: { title: '最近审计事件', description: '最新的平台事件(登录、登出、配置等)' },
166165
},
167166
},
168167
},

0 commit comments

Comments
 (0)