diff --git a/.changeset/strict-validate-cleanup.md b/.changeset/strict-validate-cleanup.md new file mode 100644 index 00000000..211aa55b --- /dev/null +++ b/.changeset/strict-validate-cleanup.md @@ -0,0 +1,48 @@ +--- +'hotcrm': patch +--- + +Clear the `os validate --strict` findings that were dead metadata + +The 17.6.0 upgrade left `os validate --strict` reporting 111 warnings. This +change clears 97 of them. The 14 that remain are deliberate, and each is listed +below with its reason. + +- **List rows are now tinted as the docs said they were.** Nine `rowColor` maps + wrote hex colours (`#dc2626`, …). The console resolves only colour names (or a + full `bg-*` class), so no row was ever tinted. The maps now name the same hue + (`red`, `orange`, `yellow`, `slate`, `green`, `blue`, `sky`, `teal`, `amber`, + `purple`, `emerald`, `gray`). **What readers will see:** soft row tints by + priority on *All Cases*, *Unassigned Triage* and the task list; by stage on + *Open Deals*; by status on events and knowledge articles; by response on + event attendees; and by rating on *High Priority* leads. The *Cases* and + *Opportunities* pages already described these tints. Accounts declare an + active/inactive tint too, but it belongs to a list the *All Accounts* landing + tab does not use, so that tab is unchanged. +- **Dashboard widgets drop 86 `options` keys that no renderer reads**: tile + icons and formats, table `columns` / `striped` / `density`, a `suffix`, the + pivot's field keys and its `drillDown`, and the SLA gauge's + green/amber/red `thresholds`. A dataset-bound widget takes its labels and + formats from the dataset, and every removed `format` matched its measure's + own, so no dashboard changes. The pivot never opened a drill-down drawer and + the gauge never drew those bands. Two tests pinned these keys, and both + changes go with the keys. `test/ownership-model.test.ts` loses its check of + the pivot's drill-down columns, which would have run over nothing. In + `test/sla-compliance-gauge.test.ts`, the band assertions go, and the gauge + still pins its compliance measure, value and target line. +- **Four permission-set row-security policies drop `label` / `description`.** + 17.6.0 marks those keys as having no runtime effect. Their wording moves into + a comment beside each policy. + +Still reported, on purpose: + +- `hierarchy-security` in `requires` (the enterprise capability). It stays + declared under the #1378 ruling. +- Six approval nodes routed to positions. The warning is that an unstaffed + position leaves a request waiting. That is a staffing fact about each + deployment, not a metadata defect. +- Six `{…}` template expressions in *Quote Generation* and *Forecast + Snapshot*. The lint says the template form keeps working. Moving the arithmetic + to CEL changes how it divides, so that is its own change with its own tests. +- The *Ask the AI Assistant* card's `description` on Sales Home. A + ruling-backed guard pins it (#1216). diff --git a/src/sales/dashboards/activity.dashboard.ts b/src/sales/dashboards/activity.dashboard.ts index 3ebcda9c..5836b390 100644 --- a/src/sales/dashboards/activity.dashboard.ts +++ b/src/sales/dashboards/activity.dashboard.ts @@ -89,7 +89,6 @@ export const ActivityDashboard: Dashboard = { colorVariant: 'success', dataset: 'event_metrics', values: ['event_count'], layout: { x: 0, y: 0, w: 3, h: 2 }, - options: { icon: 'PhoneCall', format: '0,0' }, }, { id: 'meetings_booked', @@ -100,7 +99,6 @@ export const ActivityDashboard: Dashboard = { colorVariant: 'blue', dataset: 'event_metrics', values: ['event_count'], layout: { x: 3, y: 0, w: 3, h: 2 }, - options: { icon: 'CalendarPlus', format: '0,0' }, }, { id: 'customer_minutes', @@ -111,7 +109,6 @@ export const ActivityDashboard: Dashboard = { colorVariant: 'purple', dataset: 'event_metrics', values: ['total_minutes'], layout: { x: 6, y: 0, w: 3, h: 2 }, - options: { icon: 'Clock', format: '0,0' }, }, { // First widget in the app to use `task_metrics`. The dataset shipped with @@ -125,7 +122,6 @@ export const ActivityDashboard: Dashboard = { colorVariant: 'orange', dataset: 'task_metrics', values: ['task_count'], layout: { x: 9, y: 0, w: 3, h: 2 }, - options: { icon: 'CheckCheck', format: '0,0' }, }, // ─── Row 2: who, and when ───────────────────────────────────────── @@ -211,7 +207,6 @@ export const ActivityDashboard: Dashboard = { colorVariant: 'success', dataset: 'event_metrics', values: ['event_count'], layout: { x: 8, y: 6, w: 2, h: 4 }, - options: { icon: 'Target', format: '0,0' }, }, { id: 'open_deals_for_activity', @@ -222,7 +217,6 @@ export const ActivityDashboard: Dashboard = { colorVariant: 'blue', dataset: 'opportunity_metrics', values: ['opp_count'], layout: { x: 10, y: 6, w: 2, h: 4 }, - options: { icon: 'Briefcase', format: '0,0' }, }, // ─── Row 4: the churn story, now backed by a real signal ────────── @@ -241,7 +235,6 @@ export const ActivityDashboard: Dashboard = { colorVariant: 'warning', dataset: 'account_metrics', values: ['account_count'], layout: { x: 0, y: 10, w: 4, h: 2 }, - options: { icon: 'BellOff', format: '0,0' }, }, { id: 'quiet_accounts_60', @@ -252,7 +245,6 @@ export const ActivityDashboard: Dashboard = { colorVariant: 'orange', dataset: 'account_metrics', values: ['account_count'], layout: { x: 4, y: 10, w: 4, h: 2 }, - options: { icon: 'AlertTriangle', format: '0,0' }, }, { id: 'quiet_accounts_90', @@ -263,7 +255,6 @@ export const ActivityDashboard: Dashboard = { colorVariant: 'danger', dataset: 'account_metrics', values: ['account_count'], layout: { x: 8, y: 10, w: 4, h: 2 }, - options: { icon: 'AlertOctagon', format: '0,0' }, }, ], }; diff --git a/src/sales/dashboards/crm.dashboard.ts b/src/sales/dashboards/crm.dashboard.ts index 685cb01b..b79f89d3 100644 --- a/src/sales/dashboards/crm.dashboard.ts +++ b/src/sales/dashboards/crm.dashboard.ts @@ -9,7 +9,7 @@ import { avgDealSizeMetricWidget, pipelineByStageFunnelWidget } from './shared-w * Single-page snapshot of revenue, pipeline, and customer activity. Designed * to mirror the polished CRM dashboard reference at * https://github.com/objectstack-ai/objectui/tree/main/examples/crm/src/dashboards - * — KPI tiles with icons, an area-chart revenue trend, a lead-source donut, a + * — KPI tiles, an area-chart revenue trend, a lead-source donut, a * pipeline funnel, and a recent-deals table. * * This dashboard intentionally uses the framework's first-class metadata fields @@ -67,7 +67,6 @@ export const CrmOverviewDashboard: Dashboard = { colorVariant: 'success', dataset: 'opportunity_metrics', values: ['total_amount'], layout: { x: 0, y: 0, w: 3, h: 2 }, - options: { icon: 'DollarSign' }, }, { id: 'active_deals', @@ -78,10 +77,6 @@ export const CrmOverviewDashboard: Dashboard = { colorVariant: 'blue', dataset: 'opportunity_metrics', values: ['opp_count'], layout: { x: 3, y: 0, w: 3, h: 2 }, - options: { - icon: 'Briefcase', - format: '0,0', - }, }, { id: 'won_deals', @@ -92,13 +87,8 @@ export const CrmOverviewDashboard: Dashboard = { colorVariant: 'purple', dataset: 'opportunity_metrics', values: ['opp_count'], layout: { x: 6, y: 0, w: 3, h: 2 }, - options: { - icon: 'Trophy', - format: '0,0', - }, }, - // No overrides left once the fabricated trend is gone: the factory already - // declares `options: { icon: 'bar-chart' }`, so re-passing it said nothing. + // No overrides left once the fabricated trend is gone. avgDealSizeMetricWidget({ x: 9, y: 0, w: 3, h: 2 }), // ─── Charts Row 1 ───────────────────────────────────────────────── @@ -178,17 +168,9 @@ export const CrmOverviewDashboard: Dashboard = { dataset: 'opportunity_metrics', dimensions: ['owner'], values: ['total_amount', 'opp_count', 'avg_amount'], layout: { x: 0, y: 10, w: 12, h: 4 }, options: { - columns: [ - { header: 'Owner', accessorKey: 'owner' }, - { header: 'Pipeline', accessorKey: 'total_amount', format: '0,0' }, - { header: 'Opportunities', accessorKey: 'opp_count' }, - { header: 'Avg Deal Size', accessorKey: 'avg_amount', format: '0,0' }, - ], sortBy: 'total_amount', sortOrder: 'desc', limit: 10, - striped: true, - density: 'comfortable', }, }, ], diff --git a/src/sales/dashboards/sales.dashboard.ts b/src/sales/dashboards/sales.dashboard.ts index cfb4d969..c02b96c9 100644 --- a/src/sales/dashboards/sales.dashboard.ts +++ b/src/sales/dashboards/sales.dashboard.ts @@ -75,10 +75,6 @@ export const SalesDashboard: Dashboard = { colorVariant: 'blue', dataset: 'opportunity_metrics', values: ['total_amount'], layout: { x: 0, y: 0, w: 3, h: 2 }, - options: { - icon: 'DollarSign', - format: '0,0', - }, }, { id: 'closed_won_qtd', @@ -90,10 +86,6 @@ export const SalesDashboard: Dashboard = { colorVariant: 'success', dataset: 'opportunity_metrics', values: ['total_amount'], layout: { x: 3, y: 0, w: 3, h: 2 }, - options: { - icon: 'Trophy', - format: '0,0', - }, }, { id: 'open_opportunities', @@ -104,20 +96,12 @@ export const SalesDashboard: Dashboard = { colorVariant: 'orange', dataset: 'opportunity_metrics', values: ['opp_count'], layout: { x: 6, y: 0, w: 3, h: 2 }, - options: { - icon: 'Briefcase', - format: '0,0', - }, }, avgDealSizeMetricWidget({ x: 9, y: 0, w: 3, h: 2 }, { description: 'Average value of closed-won deals this quarter', filter: { stage: 'closed_won', close_date: { $gte: '{current_quarter_start}' } }, filterBindings: { dateRange: false }, // self-scoped to QTD — the date picker must not re-window it colorVariant: 'purple', - options: { - icon: 'bar-chart', - format: '0,0', - }, }), // ─── Row 2: Win / Loss KPIs ─────────────────────────────────────── @@ -148,7 +132,6 @@ export const SalesDashboard: Dashboard = { colorVariant: 'success', dataset: 'opportunity_metrics', values: ['win_rate'], layout: { x: 0, y: 2, w: 4, h: 2 }, - options: { icon: 'Percent', format: '0%' }, }, { id: 'won_deals_12m', @@ -160,7 +143,6 @@ export const SalesDashboard: Dashboard = { colorVariant: 'blue', dataset: 'opportunity_metrics', values: ['won_count'], layout: { x: 4, y: 2, w: 4, h: 2 }, - options: { icon: 'Trophy', format: '0,0' }, }, { id: 'lost_deals_12m', @@ -172,7 +154,6 @@ export const SalesDashboard: Dashboard = { colorVariant: 'orange', dataset: 'opportunity_metrics', values: ['lost_count'], layout: { x: 8, y: 2, w: 4, h: 2 }, - options: { icon: 'TrendingDown', format: '0,0' }, }, // ─── Row 3: Pipeline & Trends ───────────────────────────────────── @@ -250,17 +231,9 @@ export const SalesDashboard: Dashboard = { dataset: 'opportunity_metrics', dimensions: ['owner'], values: ['total_amount', 'opp_count', 'avg_probability'], layout: { x: 0, y: 12, w: 12, h: 4 }, options: { - columns: [ - { header: 'Owner', accessorKey: 'owner' }, - { header: 'Open Pipeline', accessorKey: 'total_amount', format: '0,0' }, - { header: 'Open Deals', accessorKey: 'opp_count' }, - { header: 'Avg Win Prob.', accessorKey: 'avg_probability', format: '0%' }, - ], sortBy: 'total_amount', sortOrder: 'desc', limit: 10, - striped: true, - density: 'comfortable', }, }, @@ -330,17 +303,9 @@ export const SalesDashboard: Dashboard = { dataset: 'forecast_metrics', dimensions: ['owner'], values: ['quota_sum', 'closed_sum', 'attainment'], layout: { x: 0, y: 16, w: 12, h: 4 }, options: { - columns: [ - { header: 'Owner', accessorKey: 'owner' }, - { header: 'Quota', accessorKey: 'quota_sum', format: '0,0' }, - { header: 'Closed', accessorKey: 'closed_sum', format: '0,0' }, - { header: 'Attainment', accessorKey: 'attainment', format: '0%' }, - ], sortBy: 'attainment', sortOrder: 'desc', limit: 10, - striped: true, - density: 'comfortable', }, }, @@ -373,19 +338,9 @@ export const SalesDashboard: Dashboard = { values: ['won_count', 'lost_count', 'decided_count', 'win_rate', 'won_amount'], layout: { x: 0, y: 20, w: 6, h: 4 }, options: { - columns: [ - { header: 'Owner', accessorKey: 'owner' }, - { header: 'Won', accessorKey: 'won_count' }, - { header: 'Lost', accessorKey: 'lost_count' }, - { header: 'Settled', accessorKey: 'decided_count' }, - { header: 'Win Rate', accessorKey: 'win_rate', format: '0%' }, - { header: 'Won Revenue', accessorKey: 'won_amount', format: '0,0' }, - ], sortBy: 'decided_count', sortOrder: 'desc', limit: 10, - striped: true, - density: 'comfortable', }, }, { @@ -401,19 +356,9 @@ export const SalesDashboard: Dashboard = { values: ['won_count', 'lost_count', 'decided_count', 'win_rate', 'won_amount'], layout: { x: 6, y: 20, w: 6, h: 4 }, options: { - columns: [ - { header: 'Lead Source', accessorKey: 'lead_source' }, - { header: 'Won', accessorKey: 'won_count' }, - { header: 'Lost', accessorKey: 'lost_count' }, - { header: 'Settled', accessorKey: 'decided_count' }, - { header: 'Win Rate', accessorKey: 'win_rate', format: '0%' }, - { header: 'Won Revenue', accessorKey: 'won_amount', format: '0,0' }, - ], sortBy: 'decided_count', sortOrder: 'desc', limit: 12, - striped: true, - density: 'comfortable', }, }, { @@ -454,24 +399,6 @@ export const SalesDashboard: Dashboard = { colorVariant: 'default', dataset: 'opportunity_metrics', dimensions: ['stage', 'lead_source'], values: ['total_amount'], layout: { x: 0, y: 28, w: 12, h: 4 }, - options: { - rowField: 'stage', - columnField: 'lead_source', - valueField: 'amount', - aggregation: 'sum', - showRowTotals: true, - showColumnTotals: true, - format: '0,0', - drillDown: { - enabled: true, - // Clicking a pivot cell opens a drawer listing the underlying - // opportunity records for that stage + lead-source slice — the - // same drill-through pattern used by the other dashboard widgets. - target: 'drawer', - columns: ['name', 'crm_account', 'amount', 'forecast_category', 'close_date', 'owner_id'], - maxRows: 100, - }, - }, }, ], }; diff --git a/src/sales/dashboards/shared-widgets.ts b/src/sales/dashboards/shared-widgets.ts index c4d1d1e8..3c1ccb92 100644 --- a/src/sales/dashboards/shared-widgets.ts +++ b/src/sales/dashboards/shared-widgets.ts @@ -62,6 +62,5 @@ export const avgDealSizeMetricWidget = ( colorVariant: 'orange', dataset: 'opportunity_metrics', values: ['avg_amount'], layout, - options: { icon: 'bar-chart' }, ...overrides, }); diff --git a/src/sales/profiles/marketing-user.profile.ts b/src/sales/profiles/marketing-user.profile.ts index e01983fa..5dd6b1e0 100644 --- a/src/sales/profiles/marketing-user.profile.ts +++ b/src/sales/profiles/marketing-user.profile.ts @@ -53,9 +53,10 @@ export const MarketingUserProfile = { rowLevelSecurity: [ { name: 'opportunity_private_owner_only_marketing', - label: 'Private opportunities stay with their owner', - description: - 'A deal flagged Private is visible only to its owner, even to holders of org-wide opportunity read.', + // A deal flagged Private is visible only to its owner, even to holders of + // org-wide opportunity read. (`rowLevelSecurity[].label` / `description` + // have no runtime effect — liveness: dead in 17.6.0 — so the intent lives + // in this comment.) object: 'crm_opportunity', operation: 'select' as const, using: 'is_private == false || owner_id == current_user.id', @@ -77,9 +78,8 @@ export const MarketingUserProfile = { // `{id: {$null: false}}`). { name: 'marketing_campaign_updates', - label: 'Marketing works any campaign', - description: - 'Marketing users edit any campaign (and thereby enrol members into it), not only campaigns they created.', + // Marketing users edit any campaign (and thereby enrol members into it), + // not only campaigns they created. object: 'crm_campaign', operation: 'update' as const, using: 'id != null', @@ -89,9 +89,8 @@ export const MarketingUserProfile = { // default owner-only-writes policy would otherwise deny. { name: 'marketing_campaign_member_updates', - label: 'Marketing updates any campaign member', - description: - 'Marketing users update member response state on rows they did not personally create.', + // Marketing users update member response state on rows they did not + // personally create. object: 'crm_campaign_member', operation: 'update' as const, using: 'id != null', diff --git a/src/sales/profiles/sales-manager.profile.ts b/src/sales/profiles/sales-manager.profile.ts index e8c4ab41..f060325d 100644 --- a/src/sales/profiles/sales-manager.profile.ts +++ b/src/sales/profiles/sales-manager.profile.ts @@ -108,9 +108,10 @@ export const SalesManagerProfile = { rowLevelSecurity: [ { name: 'opportunity_private_owner_only', - label: 'Private opportunities stay with their owner', - description: - 'A deal flagged Private is visible only to its owner, even to holders of org-wide opportunity read.', + // A deal flagged Private is visible only to its owner, even to holders of + // org-wide opportunity read. (`rowLevelSecurity[].label` / `description` + // have no runtime effect — liveness: dead in 17.6.0 — so the intent lives + // in this comment.) object: 'crm_opportunity', operation: 'select' as const, using: 'is_private == false || owner_id == current_user.id', diff --git a/src/sales/views/account.view.ts b/src/sales/views/account.view.ts index 93cc6690..7cfefdec 100644 --- a/src/sales/views/account.view.ts +++ b/src/sales/views/account.view.ts @@ -24,7 +24,7 @@ export const AccountViews = defineView({ { field: 'health_score', width: 140 }, ], sort: [{ field: 'annual_revenue', order: 'desc' }], - rowColor: { field: 'is_active', colors: { true: '#16a34a', false: '#94a3b8' } }, + rowColor: { field: 'is_active', colors: { true: 'green', false: 'slate' } }, selection: { type: 'multiple' }, pagination: { pageSize: 50, pageSizeOptions: [25, 50, 100] }, exportOptions: { formats: ['csv', 'xlsx'] }, diff --git a/src/sales/views/event.view.ts b/src/sales/views/event.view.ts index 98deb4a0..fdbe74aa 100644 --- a/src/sales/views/event.view.ts +++ b/src/sales/views/event.view.ts @@ -41,7 +41,7 @@ export const EventViews = defineView({ rowColor: { // Mirrors the option colors on crm_event.status. field: 'status', - colors: { planned: '#4169E1', held: '#16a34a', cancelled: '#94a3b8', no_show: '#f97316' }, + colors: { planned: 'blue', held: 'green', cancelled: 'slate', no_show: 'orange' }, }, selection: { type: 'multiple' }, pagination: { pageSize: 50 }, diff --git a/src/sales/views/event_attendee.view.ts b/src/sales/views/event_attendee.view.ts index a24569b5..b9bb2ca8 100644 --- a/src/sales/views/event_attendee.view.ts +++ b/src/sales/views/event_attendee.view.ts @@ -69,7 +69,7 @@ export const EventAttendeeViews = defineView({ rowColor: { // Mirrors the option colors on crm_event_attendee.response. field: 'response', - colors: { accepted: '#16a34a', declined: '#dc2626', tentative: '#f97316', no_response: '#94a3b8' }, + colors: { accepted: 'green', declined: 'red', tentative: 'orange', no_response: 'slate' }, }, pagination: { pageSize: 25 }, }, diff --git a/src/sales/views/lead.view.ts b/src/sales/views/lead.view.ts index d38c4bb0..00a86bc5 100644 --- a/src/sales/views/lead.view.ts +++ b/src/sales/views/lead.view.ts @@ -483,8 +483,8 @@ export const LeadViews = defineView({ rowColor: { field: 'rating', colors: { - '5': '#00AA00', - '4': '#FFA500', + '5': 'green', + '4': 'orange', }, }, }, diff --git a/src/sales/views/opportunity.view.ts b/src/sales/views/opportunity.view.ts index 734f3997..3dd3a252 100644 --- a/src/sales/views/opportunity.view.ts +++ b/src/sales/views/opportunity.view.ts @@ -41,18 +41,23 @@ export const OpportunityViews = defineView({ rowColor: { field: 'stage', colors: { - prospecting: '#94a3b8', - qualification: '#60a5fa', + prospecting: 'slate', + qualification: 'sky', // Teal sits between the cool qualification blue and the warm proposal - // amber, keeping the cool→warm funnel ramp readable. NOT the - // `#FFD700` this option carries in `_picklists.ts`: this map is a - // Tailwind palette, and gold is one hue step from proposal's - // `#f59e0b` — the two adjacent stages would tint rows the same. - needs_analysis: '#14b8a6', - proposal: '#f59e0b', - negotiation: '#a855f7', - closed_won: '#16a34a', - closed_lost: '#dc2626', + // amber, keeping the cool→warm funnel ramp readable. NOT the gold + // `#FFD700` this option carries in `_picklists.ts`: gold is one hue + // step from proposal's amber, so the two adjacent stages would tint + // rows the same. + // + // Values are colour NAMES, not hex: objectui's `useRowColor` resolves + // only a name from its own vocabulary (or a full `bg-*` class) and + // turns a hex into no colour at all — so until 17.6.0's lint said so, + // this map tinted nothing. + needs_analysis: 'teal', + proposal: 'amber', + negotiation: 'purple', + closed_won: 'green', + closed_lost: 'red', }, }, pagination: { pageSize: 25, pageSizeOptions: [25, 50, 100] }, diff --git a/src/sales/views/task.view.ts b/src/sales/views/task.view.ts index 62a8cc01..ce6af208 100644 --- a/src/sales/views/task.view.ts +++ b/src/sales/views/task.view.ts @@ -70,7 +70,7 @@ export const TaskViews = defineView({ // keys were Case values, so urgent and normal rows got no color at all. // Colors mirror the option colors on crm_task.priority. field: 'priority', - colors: { urgent: '#dc2626', high: '#f97316', normal: '#16a34a', low: '#94a3b8' }, + colors: { urgent: 'red', high: 'orange', normal: 'green', low: 'slate' }, }, selection: { type: 'multiple' }, pagination: { pageSize: 50 }, diff --git a/src/service/dashboards/service.dashboard.ts b/src/service/dashboards/service.dashboard.ts index fcef7256..5bd84fc4 100644 --- a/src/service/dashboards/service.dashboard.ts +++ b/src/service/dashboards/service.dashboard.ts @@ -84,10 +84,6 @@ export const ServiceDashboard: Dashboard = { colorVariant: 'orange', dataset: 'case_metrics', values: ['case_count'], layout: { x: 0, y: 0, w: 3, h: 2 }, - options: { - icon: 'Inbox', - format: '0,0', - }, }, { id: 'critical_cases', @@ -98,10 +94,6 @@ export const ServiceDashboard: Dashboard = { colorVariant: 'danger', dataset: 'case_metrics', values: ['case_count'], layout: { x: 3, y: 0, w: 3, h: 2 }, - options: { - icon: 'AlertTriangle', - format: '0,0', - }, }, { id: 'avg_resolution_time', @@ -112,11 +104,6 @@ export const ServiceDashboard: Dashboard = { colorVariant: 'blue', dataset: 'case_metrics', values: ['avg_resolution'], layout: { x: 6, y: 0, w: 3, h: 2 }, - options: { - icon: 'Clock', - format: '0.0', - suffix: 'h', - }, }, { id: 'sla_violations', @@ -127,10 +114,6 @@ export const ServiceDashboard: Dashboard = { colorVariant: 'warning', dataset: 'case_metrics', values: ['case_count'], layout: { x: 9, y: 0, w: 3, h: 2 }, - options: { - icon: 'ShieldAlert', - format: '0,0', - }, }, // ─── Row 2: Distribution ────────────────────────────────────────── @@ -239,17 +222,11 @@ export const ServiceDashboard: Dashboard = { { type: 'line', axis: 'y', value: 0.95, label: 'Target', style: 'dashed', color: '#10B981' }, ], }, - // The ladder and the target line were written for COMPLIANCE and are - // unchanged: 95%+ is green, 85–95% amber, below that red. They were - // always right — it was the plotted value that disagreed with them. - options: { - format: '0%', - thresholds: [ - { value: 0.95, color: 'success' }, - { value: 0.85, color: 'warning' }, - { value: 0, color: 'danger' }, - ], - }, + // The target line was written for COMPLIANCE and is unchanged: it was + // always right — it was the plotted value that disagreed with it. (The + // green/amber/red `options.thresholds` ladder beside it was deleted in + // 17.6.0's strict cleanup: no dashboard renderer reads widget + // `thresholds`, so it never coloured anything.) }, // ─── Row 4: Knowledge deflection (#601) ─────────────────────────── @@ -270,7 +247,6 @@ export const ServiceDashboard: Dashboard = { colorVariant: 'success', dataset: 'case_metrics', values: ['kb_deflection_rate'], layout: { x: 0, y: 10, w: 4, h: 2 }, - options: { icon: 'BookOpenCheck', format: '0%' }, }, { id: 'kb_resolved_cases', @@ -280,7 +256,6 @@ export const ServiceDashboard: Dashboard = { colorVariant: 'blue', dataset: 'case_metrics', values: ['kb_resolved_count'], layout: { x: 4, y: 10, w: 4, h: 2 }, - options: { icon: 'BookOpen', format: '0,0' }, }, { id: 'closed_cases_total', @@ -290,7 +265,6 @@ export const ServiceDashboard: Dashboard = { colorVariant: 'default', dataset: 'case_metrics', values: ['closed_count'], layout: { x: 8, y: 10, w: 4, h: 2 }, - options: { icon: 'CheckCheck', format: '0,0' }, }, { id: 'top_resolving_articles', @@ -302,15 +276,9 @@ export const ServiceDashboard: Dashboard = { dataset: 'case_metrics', dimensions: ['resolved_article'], values: ['kb_resolved_count'], layout: { x: 0, y: 12, w: 12, h: 4 }, options: { - columns: [ - { header: 'Article', accessorKey: 'resolved_article' }, - { header: 'Cases Resolved', accessorKey: 'kb_resolved_count' }, - ], sortBy: 'kb_resolved_count', sortOrder: 'desc', limit: 10, - striped: true, - density: 'comfortable', }, }, @@ -333,16 +301,9 @@ export const ServiceDashboard: Dashboard = { dataset: 'case_metrics', dimensions: ['priority'], values: ['case_count', 'avg_sla_violated'], layout: { x: 0, y: 16, w: 12, h: 4 }, options: { - columns: [ - { header: 'Priority', accessorKey: 'priority' }, - { header: 'Open Cases', accessorKey: 'case_count' }, - { header: 'SLA Violation Rate', accessorKey: 'avg_sla_violated', format: '0.0%' }, - ], sortBy: 'case_count', sortOrder: 'desc', limit: 10, - striped: true, - density: 'comfortable', }, }, ], diff --git a/src/service/views/case.view.ts b/src/service/views/case.view.ts index 2b8da4a9..28c87c22 100644 --- a/src/service/views/case.view.ts +++ b/src/service/views/case.view.ts @@ -38,7 +38,7 @@ export const CaseViews = defineView({ ], rowColor: { field: 'priority', - colors: { critical: '#dc2626', high: '#f97316', medium: '#eab308', low: '#94a3b8' }, + colors: { critical: 'red', high: 'orange', medium: 'yellow', low: 'slate' }, }, selection: { type: 'multiple' }, pagination: { pageSize: 50 }, @@ -226,7 +226,7 @@ export const CaseViews = defineView({ ], rowColor: { field: 'priority', - colors: { critical: '#dc2626', high: '#f97316', medium: '#eab308', low: '#94a3b8' }, + colors: { critical: 'red', high: 'orange', medium: 'yellow', low: 'slate' }, }, // The empty state carries the operational instruction, because "no rows" // here is ambiguous on its own: it means either "the round-robin placed diff --git a/src/service/views/knowledge_article.view.ts b/src/service/views/knowledge_article.view.ts index 0edb85ff..627d9bf9 100644 --- a/src/service/views/knowledge_article.view.ts +++ b/src/service/views/knowledge_article.view.ts @@ -43,7 +43,7 @@ export const KnowledgeArticleViews = defineView({ pagination: { pageSize: 50 }, rowColor: { field: 'status', - colors: { draft: '#94A3B8', in_review: '#F59E0B', published: '#10B981', archived: '#475569' }, + colors: { draft: 'slate', in_review: 'amber', published: 'emerald', archived: 'gray' }, }, appearance: { showDescription: true, diff --git a/test/ownership-model.test.ts b/test/ownership-model.test.ts index d1e0d72d..96858cb5 100644 --- a/test/ownership-model.test.ts +++ b/test/ownership-model.test.ts @@ -386,36 +386,6 @@ describe('every owner-facing surface points at the one column', () => { expect(bad, `flow references to the retired column:\n ${bad.join('\n ')}`).toEqual([]); }); - it('dashboard drill-down column lists name real record columns, not the dataset alias', () => { - // A drill-down drawer lists RECORDS, so its `columns` are object fields — - // unlike the widget's `dimensions`, which are dataset dimension NAMES and - // legitimately still read `owner` (the semantic layer resolves that alias - // onto the `owner_id` column). The two live inches apart in the same widget - // and a blanket rename would have broken the analytics binding while a - // blanket skip left a dead column in the drawer. - const dashboards: AnyRec[] = (stack as any).dashboards ?? []; - const datasets: AnyRec[] = (stack as any).datasets ?? []; - const objectOfDataset = new Map(datasets.map((d) => [d.name as string, d.object as string])); - - const bad: string[] = []; - let checked = 0; - for (const d of dashboards) { - for (const w of (d.widgets ?? []) as AnyRec[]) { - const columns = w.options?.drillDown?.columns as string[] | undefined; - if (!Array.isArray(columns)) continue; - const objectName = objectOfDataset.get(String(w.dataset)); - const fields = objectByName.get(String(objectName))?.fields ?? {}; - for (const col of columns) { - checked++; - if (col.includes('.')) continue; // traversal — resolved by the query engine - if (!(col in fields)) bad.push(`${d.name}/${w.id}: drill-down column "${col}" is not a field on ${objectName}`); - } - } - } - expect(checked, 'no drill-down columns found — this guard stopped guarding').toBeGreaterThan(0); - expect(bad, `drill-down columns that resolve to nothing:\n ${bad.join('\n ')}`).toEqual([]); - }); - it('the RLS predicates that mean “its owner” key on owner_id', () => { // #547's `is_private` filter was deliberately written against the VISIBLE // field for form consistency, which is precisely how it came to disagree diff --git a/test/sla-compliance-gauge.test.ts b/test/sla-compliance-gauge.test.ts index 2c415dd0..6ba0cf7b 100644 --- a/test/sla-compliance-gauge.test.ts +++ b/test/sla-compliance-gauge.test.ts @@ -64,18 +64,15 @@ describe('the SLA gauge is bound to a compliance measure (#1213)', () => { }); /** - * PM assumption 3, checked rather than assumed: the ladder and the target - * line were authored for compliance. They needed no inversion — it was the - * plotted value that disagreed with them. + * PM assumption 3, checked rather than assumed: the target line was + * authored for compliance. It needed no inversion — it was the plotted value + * that disagreed with it. (The green/amber/red `options.thresholds` ladder + * this test also pinned was deleted in 17.6.0's strict cleanup: no dashboard + * renderer reads widget `thresholds`, so it never coloured anything.) */ - it('keeps the compliance-shaped ladder and target line unchanged', () => { + it('keeps the compliance-shaped target line unchanged', () => { const w = gauge(); expect(w.colorVariant).toBe('success'); - expect(w.options.thresholds).toEqual([ - { value: 0.95, color: 'success' }, - { value: 0.85, color: 'warning' }, - { value: 0, color: 'danger' }, - ]); const target = (w.chartConfig?.annotations ?? []).find((a: AnyRec) => a.label === 'Target'); expect(target, 'the 0.95 target line is gone').toBeTruthy(); expect(target.value).toBe(0.95); @@ -349,19 +346,4 @@ describe('the gauge reads 100% on the seeded demo org', () => { await (ql as AnyRec)?.close(); } }); - - /** - * What the reader sees, at that value. 1.0 clears the 0.95 target and lands - * in the success band — while the number the widget used to plot (a 0.0 - * violation rate) would have landed in `danger` under the very same ladder. - * That mismatch, not the ladder, was the defect. - */ - it('lands in the success band at 100%, where the old value landed in danger', () => { - const bandFor = (v: number): string => - (gauge().options.thresholds as AnyRec[]) - .filter((t) => v >= (t.value as number)) - .sort((a, b) => (b.value as number) - (a.value as number))[0].color as string; - expect(bandFor(1)).toBe('success'); - expect(bandFor(0)).toBe('danger'); - }); });