Phase 2: dashboard tabs + popups, chart fixes, shell relocation

Built via parallel Workflow (5 builders + adversarial verifiers), then
live-rendered and fixed in a headless browser against the running gateway.

Tabs (PrimeControls/Tabs/*):
- Overview: priority cards, KPI status row, alarm-rate timeline, insights, top-5
- Analysis: hour x day heatmap, priority donut vs ISA 80/15/5, MTTA/MTTR, floods
- BadActors: chattering/standing/fleeting tables, focus chip, AlarmDetail wiring
- Journal: paginated getJournalPage table, CSV export, EventDetail wiring

Popups (PrimeControls/Popups/*): HealthScore, ShiftReport, AlarmDetail, EventDetail

Chart fixes (verified rendering in-browser):
- Overview timeline: seeded dataSources.rate, switched to static fills, split
  flood bins into their own red stacked series (per-point deriveFieldsFromData
  fill was crashing render with React.cloneElement null).
- BadActors: removed the dual-axis Pareto XY chart (scaling was unusable);
  replaced with a ranked "Top Sources" table (share-of-total progress bar +
  cumulative %). Same insight, no axis scaling to break.

Structural / hygiene:
- Relocated Header + FilterBar from Dashboard/ to Shell/ and updated the two
  embed paths. A Perspective view cannot resolve sub-views nested inside another
  view's folder, so both were rendering as "view does not exist" placeholders.
- Set persistent:false on all view input params. persistent:true caused the
  gateway to bake the live bundle (with sim tag paths) into Tabs/Analysis on
  render; nulled that baked data and reset the session query range to 0.

lint_project.py: 0 failures / 0 warnings.

Verified in-browser: Overview, BadActors. NOT yet live-verified: Analysis,
Journal, and the four popups (schema-verified only). Header content is next
phase (renders now, but a benign cloneElement console warning remains).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 14:45:17 -05:00
parent ecb10b9b7b
commit 83b23431c6
30 changed files with 8678 additions and 312 deletions

View File

@@ -1,55 +1,463 @@
{
"custom": {},
"custom": {
"page": 0,
"pageData": null
},
"params": {
"bundle": null
},
"propConfig": {
"custom.pageData": {
"binding": {
"config": {
"expression": "{session.custom.PrimeControls.query.refreshToken} + '|' + {session.custom.PrimeControls.query.startMs} + '|' + {session.custom.PrimeControls.query.endMs} + '|' + {view.custom.page}"
},
"transforms": [
{
"code": "\tsafe = {'rows': [], 'total': 0, 'page': 0, 'page_size': 50, 'truncated': False, 'error': None}\n\ttry:\n\t\tq = self.session.custom.PrimeControls.query\n\t\tif not q.startMs or not q.endMs:\n\t\t\treturn safe\n\t\ttry:\n\t\t\tpage = max(0, int(self.view.custom.page or 0))\n\t\texcept:\n\t\t\tpage = 0\n\t\treturn PrimeControls.alarms.getJournalPage(q.startMs, q.endMs, {'priorities': list(q.priorities), 'areas': list(q.areas), 'states': list(q.states), 'search': q.search}, page, 50)\n\texcept:\n\t\timport sys\n\t\tsafe['error'] = '%s' % (sys.exc_info()[1],)\n\t\treturn safe",
"type": "script"
}
],
"type": "expr"
}
},
"params.bundle": {
"paramDirection": "input",
"persistent": true
"persistent": false
}
},
"props": {
"defaultSize": {
"height": 800,
"width": 1600
}
},
"props": {},
"root": {
"children": [
{
"meta": {
"name": "ph"
},
"props": {
"style": {
"classes": "PrimeControls/Empty"
"children": [
{
"meta": {
"name": "lblTotal"
},
"position": {
"shrink": 0
},
"propConfig": {
"props.text": {
"binding": {
"config": {
"path": "view.custom.pageData"
},
"transforms": [
{
"code": "\ttry:\n\t\ttotal = int(value['total'] or 0)\n\texcept:\n\t\ttotal = 0\n\ttry:\n\t\ttxt = '%s events' % PrimeControls.fmt.num(total)\n\texcept:\n\t\ttxt = '%d events' % total\n\ttry:\n\t\tif value['truncated']:\n\t\t\ttxt = txt + ' (truncated)'\n\texcept:\n\t\tpass\n\treturn txt",
"type": "script"
}
],
"type": "property"
}
}
},
"props": {
"textStyle": {
"fontWeight": "bold"
}
},
"type": "ia.display.label"
},
"text": "Journal - built in Phase 2"
},
"type": "ia.display.label"
},
{
"meta": {
"name": "evt"
},
"propConfig": {
"props.text": {
"binding": {
"config": {
"path": "view.params.bundle"
{
"meta": {
"name": "lblError"
},
"position": {
"shrink": 1
},
"propConfig": {
"position.display": {
"binding": {
"config": {
"path": "view.custom.pageData"
},
"transforms": [
{
"code": "\ttry:\n\t\treturn bool(value['error'])\n\texcept:\n\t\treturn False",
"type": "script"
}
],
"type": "property"
}
},
"transforms": [
{
"code": "\ttry:\n\t\treturn 'bundle: %s events in range' % value['meta']['event_count']\n\texcept:\n\t\treturn 'bundle: not loaded'",
"props.text": {
"binding": {
"config": {
"path": "view.custom.pageData"
},
"transforms": [
{
"code": "\ttry:\n\t\treturn value['error'] or ''\n\texcept:\n\t\treturn ''",
"type": "script"
}
],
"type": "property"
}
}
},
"props": {
"style": {
"classes": "PrimeControls/Text/Muted",
"color": "#D64550"
}
},
"type": "ia.display.label"
},
{
"children": [],
"meta": {
"name": "spacer"
},
"position": {
"grow": 1
},
"props": {},
"type": "ia.container.flex"
},
{
"events": {
"component": {
"onActionPerformed": {
"config": {
"script": "\ttry:\n\t\tp = int(self.view.custom.page or 0)\n\texcept:\n\t\tp = 0\n\tif p > 0:\n\t\tself.view.custom.page = p - 1"
},
"scope": "G",
"type": "script"
}
],
"type": "property"
}
}
},
"meta": {
"name": "btnPrev"
},
"position": {
"basis": "80px",
"shrink": 0
},
"propConfig": {
"props.enabled": {
"binding": {
"config": {
"path": "view.custom.pageData"
},
"transforms": [
{
"code": "\ttry:\n\t\treturn int(value['page'] or 0) > 0\n\texcept:\n\t\treturn False",
"type": "script"
}
],
"type": "property"
}
}
},
"props": {
"primary": false,
"text": "Prev"
},
"type": "ia.input.button"
},
{
"meta": {
"name": "lblPage"
},
"position": {
"shrink": 0
},
"propConfig": {
"props.text": {
"binding": {
"config": {
"path": "view.custom.pageData"
},
"transforms": [
{
"code": "\ttry:\n\t\ttotal = int(value['total'] or 0)\n\t\tsize = int(value['page_size'] or 50)\n\t\tpage = int(value['page'] or 0)\n\texcept:\n\t\ttotal = 0\n\t\tsize = 50\n\t\tpage = 0\n\tif size < 1:\n\t\tsize = 50\n\tpages = (total + size - 1) // size\n\tif pages < 1:\n\t\tpages = 1\n\ttry:\n\t\treturn 'Page %s of %s' % (PrimeControls.fmt.num(page + 1), PrimeControls.fmt.num(pages))\n\texcept:\n\t\treturn 'Page %d of %d' % (page + 1, pages)",
"type": "script"
}
],
"type": "property"
}
}
},
"props": {
"style": {
"classes": "PrimeControls/Text/Muted"
},
"textStyle": {
"textAlign": "center"
}
},
"type": "ia.display.label"
},
{
"events": {
"component": {
"onActionPerformed": {
"config": {
"script": "\ttry:\n\t\tp = int(self.view.custom.page or 0)\n\texcept:\n\t\tp = 0\n\ttry:\n\t\td = self.view.custom.pageData\n\t\ttotal = int(d['total'] or 0)\n\t\tsize = int(d['page_size'] or 50)\n\texcept:\n\t\ttotal = 0\n\t\tsize = 50\n\tif size < 1:\n\t\tsize = 50\n\tpages = (total + size - 1) // size\n\tif pages < 1:\n\t\tpages = 1\n\tif p < pages - 1:\n\t\tself.view.custom.page = p + 1"
},
"scope": "G",
"type": "script"
}
}
},
"meta": {
"name": "btnNext"
},
"position": {
"basis": "80px",
"shrink": 0
},
"propConfig": {
"props.enabled": {
"binding": {
"config": {
"path": "view.custom.pageData"
},
"transforms": [
{
"code": "\ttry:\n\t\ttotal = int(value['total'] or 0)\n\t\tsize = int(value['page_size'] or 50)\n\t\tpage = int(value['page'] or 0)\n\texcept:\n\t\treturn False\n\tif size < 1:\n\t\tsize = 50\n\treturn (page + 1) * size < total",
"type": "script"
}
],
"type": "property"
}
}
},
"props": {
"primary": false,
"text": "Next"
},
"type": "ia.input.button"
},
{
"events": {
"component": {
"onActionPerformed": {
"config": {
"script": "\ttry:\n\t\tq = self.session.custom.PrimeControls.query\n\t\tcsv = PrimeControls.alarms.journalCsv(q.startMs, q.endMs, {'priorities': list(q.priorities), 'areas': list(q.areas), 'states': list(q.states), 'search': q.search})\n\t\tsystem.perspective.download('alarm_journal.csv', csv)\n\texcept:\n\t\tpass"
},
"scope": "G",
"type": "script"
}
}
},
"meta": {
"name": "btnExport"
},
"position": {
"basis": "110px",
"shrink": 0
},
"props": {
"primary": true,
"text": "Export CSV"
},
"type": "ia.input.button"
}
],
"meta": {
"name": "toolbar"
},
"position": {
"shrink": 0
},
"props": {
"alignItems": "center",
"style": {
"classes": "PrimeControls/Text/Muted"
"classes": "PrimeControls/Toolbar",
"gap": "8px"
}
},
"type": "ia.display.label"
"type": "ia.container.flex"
},
{
"children": [
{
"events": {
"component": {
"onRowClick": {
"config": {
"script": "\trow = event.value\n\tevt = {}\n\ttry:\n\t\tfor k in row:\n\t\t\tv = row[k]\n\t\t\ttry:\n\t\t\t\tevt[k] = v['value']\n\t\t\texcept:\n\t\t\t\tevt[k] = v\n\texcept:\n\t\tevt = {}\n\ttry:\n\t\tif evt:\n\t\t\tsystem.perspective.openPopup('PC_EventDetail', 'PrimeControls/Popups/EventDetail', params = {'event': evt}, showCloseIcon = True, draggable = True)\n\texcept:\n\t\tpass"
},
"scope": "G",
"type": "script"
}
}
},
"meta": {
"name": "tblJournal"
},
"position": {
"grow": 1
},
"propConfig": {
"position.display": {
"binding": {
"config": {
"path": "view.custom.pageData"
},
"transforms": [
{
"code": "\ttry:\n\t\treturn len(value['rows'] or []) > 0\n\texcept:\n\t\treturn False",
"type": "script"
}
],
"type": "property"
}
},
"props.data": {
"binding": {
"config": {
"path": "view.custom.pageData"
},
"transforms": [
{
"code": "\tstate_cls = {'active': 'PrimeControls/State/Active', 'ack': 'PrimeControls/State/Acked', 'clear': 'PrimeControls/State/Cleared'}\n\tprio_cls = {'Diagnostic': 'PrimeControls/PrioritySoft/Diagnostic', 'Low': 'PrimeControls/PrioritySoft/Low', 'Medium': 'PrimeControls/PrioritySoft/Medium', 'High': 'PrimeControls/PrioritySoft/High', 'Critical': 'PrimeControls/PrioritySoft/Critical'}\n\ttry:\n\t\trows = value['rows'] or []\n\texcept:\n\t\trows = []\n\tout = []\n\tfor r in rows:\n\t\ttry:\n\t\t\tsc = state_cls.get(r['state'], '')\n\t\t\tpc = prio_cls.get(r['priority_name'], '')\n\t\t\tout.append({'time_label': r['time_label'] or '', 'state_label': {'value': r['state_label'] or '', 'style': {'classes': sc}}, 'priority_name': {'value': r['priority_name'] or '', 'style': {'classes': pc}}, 'label': r['label'] or '', 'area': r['area'] or '', 'ack_user': r['ack_user'] or '', 'id': r['id'] or '', 'time_ms': r['time_ms'] or 0, 'source': r['source'] or '', 'state': r['state'] or '', 'priority': r['priority']})\n\t\texcept:\n\t\t\tpass\n\treturn out",
"type": "script"
}
],
"type": "property"
}
}
},
"props": {
"columns": [
{
"field": "time_label",
"header": {
"title": "Time"
},
"sortable": false,
"strictWidth": true,
"width": 150
},
{
"field": "state_label",
"header": {
"title": "State"
},
"justify": "center",
"sortable": false,
"strictWidth": true,
"width": 110
},
{
"field": "priority_name",
"header": {
"title": "Priority"
},
"justify": "center",
"sortable": false,
"strictWidth": true,
"width": 110
},
{
"field": "label",
"header": {
"title": "Alarm"
},
"sortable": false
},
{
"field": "area",
"header": {
"title": "Area"
},
"sortable": false,
"strictWidth": true,
"width": 160
},
{
"field": "ack_user",
"header": {
"title": "Ack By"
},
"sortable": false,
"strictWidth": true,
"width": 120
},
{
"field": "id",
"visible": false
},
{
"field": "time_ms",
"visible": false
},
{
"field": "source",
"visible": false
},
{
"field": "state",
"visible": false
},
{
"field": "priority",
"visible": false
}
],
"pager": {
"bottom": false,
"top": false
}
},
"type": "ia.display.table"
},
{
"meta": {
"name": "empty"
},
"position": {
"grow": 1
},
"propConfig": {
"position.display": {
"binding": {
"config": {
"path": "view.custom.pageData"
},
"transforms": [
{
"code": "\ttry:\n\t\treturn len(value['rows'] or []) == 0\n\texcept:\n\t\treturn True",
"type": "script"
}
],
"type": "property"
}
}
},
"props": {
"params": {
"icon": "search",
"message": "No events match the current filters"
},
"path": "PrimeControls/Components/EmptyState"
},
"type": "ia.display.view"
}
],
"meta": {
"name": "cardTable"
},
"position": {
"grow": 1
},
"props": {
"direction": "column",
"style": {
"classes": "PrimeControls/Card"
}
},
"type": "ia.container.flex"
}
],
"meta": {
@@ -58,11 +466,9 @@
"props": {
"direction": "column",
"style": {
"alignItems": "center",
"classes": "PrimeControls/Card",
"justifyContent": "center"
"gap": "12px"
}
},
"type": "ia.container.flex"
}
}
}