From d353c5001eda8a33fb6a9f5d9ff8502e9ecf777e Mon Sep 17 00:00:00 2001 From: "a.williamson" Date: Thu, 16 Jul 2026 16:27:48 -0500 Subject: [PATCH] Data seeding, filter overhaul, UI scaling fixes, sanitized AlarmAnalysis copy Data & simulation: - simulation_tags.json: re-prioritize 10 tags to Medium so all four priorities appear in live data (backfilled journal history to match) - SimHarness probe P2: ack-user/includeData ground truth + filtered-bundle verification against the live journal Data layer (PrimeControls.calc/alarms): - filters now drive every metric: queryStatus rows (Active Now/Unacked/ Standing) filtered via calc.apply_status_filters; event_count counts only filtered instances (per-instance n_events); dropdown options enumerate unfiltered data so choices never collapse - build_bundle refactored into _assemble(); when filters are active an unfiltered mirror is attached as bundle.overview - the Overview tab stays a plant-wide summary while all other consumers bind filtered sections - _fetch_journal now honors includeData (required for ackUserName; without it ack attribution is always None); enabled for journal rows + source detail so Top Ack Users and the Ack By column populate FilterBar/Dashboard: - root-cause fix: bidirectional bindings never wrote dropdown selections back to session props, so filters silently did nothing; Apply now pulls values directly from the sibling controls, Clear resets both - multi-select dropdowns cap at 280px and wrap chips; filter bar row is auto-height so wrapped chips push tabs down instead of overlapping - filter bar hidden on the Overview tab (not applicable there) UI scaling: - Header: Prime logo added; KpiCard compacted to fit the 71px KPI row (basis-95% overflow fix, chip hugs text, embeds clipped) - no scrollbars - Overview timeline: thinned x labels (minDistance 140), labels anchored below the axis, smaller legend markers - Bad Actors focus chip shows the full source string with the clear X - popup repeaters (Daily Activity, ScoreBars) no longer overflow (useDefaultViewWidth/Height off, taller strip); 5px table cell padding Tooling/tests: - lint_project.py: accept propConfig params.* as param declarations (Designer saves strip null params entries) - tests: 31 -> 34 (status-filter semantics, overview mirror, filtered event counts) - .gitignore: runtime churn (valueStore.idb, local-system-properties, migration logs, thumbnails, .schemas) New project: AlarmAnalysis - submission copy of PrimeBAT with client-visible Prime Controls branding removed (logo, subtitle, project title, shift report heading); PrimeControls organizational namespaces retained per spec. Co-Authored-By: Claude Fable 5 --- .gitignore | 8 + .../config/ignition/tags/valueStore.idb | Bin 20480 -> 0 bytes .../ignition/api-token/Buildathon/config.json | 4 +- .../api-token/Buildathon/resource.json | 2 +- .../PrimeControls/image (4).png/image (4).png | Bin 0 -> 22586 bytes .../PrimeControls/image (4).png/resource.json | 21 + .../BuildathonSim/BoilerHouse/tags.json | 160 +- .../default/BuildathonSim/Intake/tags.json | 4 +- .../default/BuildathonSim/Packaging/tags.json | 90 +- .../default/BuildathonSim/TankFarm/tags.json | 190 +- .../default/BuildathonSim/Utilities/tags.json | 152 +- .../local-system-properties/config.json | 4 - .../local-system-properties/resource.json | 16 - .../page-config/config.json | 9 + .../page-config/resource.json | 10 + .../session-props/props.json | 95 + .../session-props/resource.json | 16 + .../PrimeControls/Card/resource.json | 10 + .../PrimeControls/Card/style.json | 10 + .../PrimeControls/CardTitle/resource.json | 10 + .../PrimeControls/CardTitle/style.json | 12 + .../PrimeControls/Chip/Bad/resource.json | 10 + .../PrimeControls/Chip/Bad/style.json | 13 + .../PrimeControls/Chip/Flat/resource.json | 10 + .../PrimeControls/Chip/Flat/style.json | 13 + .../PrimeControls/Chip/Good/resource.json | 10 + .../PrimeControls/Chip/Good/style.json | 13 + .../PrimeControls/Empty/resource.json | 10 + .../PrimeControls/Empty/style.json | 9 + .../PrimeControls/Grade/A/resource.json | 10 + .../PrimeControls/Grade/A/style.json | 10 + .../PrimeControls/Grade/B/resource.json | 10 + .../PrimeControls/Grade/B/style.json | 10 + .../PrimeControls/Grade/C/resource.json | 10 + .../PrimeControls/Grade/C/style.json | 10 + .../PrimeControls/Grade/D/resource.json | 10 + .../PrimeControls/Grade/D/style.json | 10 + .../PrimeControls/Grade/F/resource.json | 10 + .../PrimeControls/Grade/F/style.json | 10 + .../PrimeControls/Grade/NA/resource.json | 10 + .../PrimeControls/Grade/NA/style.json | 10 + .../PrimeControls/PageBg/resource.json | 10 + .../PrimeControls/PageBg/style.json | 8 + .../Priority/Critical/resource.json | 10 + .../Priority/Critical/style.json | 15 + .../Priority/Diagnostic/resource.json | 10 + .../Priority/Diagnostic/style.json | 15 + .../PrimeControls/Priority/High/resource.json | 10 + .../PrimeControls/Priority/High/style.json | 15 + .../PrimeControls/Priority/Low/resource.json | 10 + .../PrimeControls/Priority/Low/style.json | 15 + .../Priority/Medium/resource.json | 10 + .../PrimeControls/Priority/Medium/style.json | 15 + .../PrioritySoft/Critical/resource.json | 10 + .../PrioritySoft/Critical/style.json | 10 + .../PrioritySoft/Diagnostic/resource.json | 10 + .../PrioritySoft/Diagnostic/style.json | 10 + .../PrioritySoft/High/resource.json | 10 + .../PrioritySoft/High/style.json | 10 + .../PrioritySoft/Low/resource.json | 10 + .../PrimeControls/PrioritySoft/Low/style.json | 10 + .../PrioritySoft/Medium/resource.json | 10 + .../PrioritySoft/Medium/style.json | 10 + .../PrimeControls/State/Acked/resource.json | 10 + .../PrimeControls/State/Acked/style.json | 8 + .../PrimeControls/State/Active/resource.json | 10 + .../PrimeControls/State/Active/style.json | 9 + .../PrimeControls/State/Cleared/resource.json | 10 + .../PrimeControls/State/Cleared/style.json | 8 + .../PrimeControls/Tab/Active/resource.json | 10 + .../PrimeControls/Tab/Active/style.json | 16 + .../PrimeControls/Tab/Inactive/resource.json | 10 + .../PrimeControls/Tab/Inactive/style.json | 23 + .../PrimeControls/Text/Big/resource.json | 10 + .../PrimeControls/Text/Big/style.json | 9 + .../PrimeControls/Text/Kpi/resource.json | 10 + .../PrimeControls/Text/Kpi/style.json | 9 + .../PrimeControls/Text/Muted/resource.json | 10 + .../PrimeControls/Text/Muted/style.json | 8 + .../PrimeControls/Toolbar/resource.json | 10 + .../PrimeControls/Toolbar/style.json | 10 + .../Components/EmptyState/resource.json | 10 + .../Components/EmptyState/view.json | 93 + .../Components/HealthGauge/resource.json | 16 + .../Components/HealthGauge/view.json | 134 + .../Components/HeatmapCell/resource.json | 10 + .../Components/HeatmapCell/view.json | 94 + .../Components/InsightRow/resource.json | 10 + .../Components/InsightRow/view.json | 121 + .../Components/KpiCard/resource.json | 16 + .../Components/KpiCard/view.json | 199 ++ .../Components/PriorityCard/resource.json | 10 + .../Components/PriorityCard/view.json | 137 + .../Components/ScoreBar/resource.json | 10 + .../Components/ScoreBar/view.json | 207 ++ .../Components/SourceRow/resource.json | 10 + .../Components/SourceRow/view.json | 192 ++ .../Components/TabButton/resource.json | 10 + .../Components/TabButton/view.json | 69 + .../PrimeControls/Dashboard/resource.json | 16 + .../views/PrimeControls/Dashboard/view.json | 330 +++ .../Popups/AlarmDetail/resource.json | 16 + .../Popups/AlarmDetail/view.json | 790 ++++++ .../Popups/EventDetail/resource.json | 10 + .../Popups/EventDetail/view.json | 665 +++++ .../Popups/HealthScore/resource.json | 10 + .../Popups/HealthScore/view.json | 318 +++ .../Popups/ShiftReport/resource.json | 10 + .../Popups/ShiftReport/view.json | 204 ++ .../Shell/FilterBar/resource.json | 10 + .../PrimeControls/Shell/FilterBar/view.json | 281 ++ .../PrimeControls/Shell/Header/resource.json | 16 + .../PrimeControls/Shell/Header/view.json | 697 +++++ .../PrimeControls/Tabs/Analysis/resource.json | 10 + .../PrimeControls/Tabs/Analysis/view.json | 2463 +++++++++++++++++ .../Tabs/BadActors/resource.json | 16 + .../PrimeControls/Tabs/BadActors/view.json | 1095 ++++++++ .../PrimeControls/Tabs/Journal/resource.json | 10 + .../PrimeControls/Tabs/Journal/view.json | 479 ++++ .../PrimeControls/Tabs/Overview/resource.json | 10 + .../PrimeControls/Tabs/Overview/view.json | 1807 ++++++++++++ .../client-tags/data.bin | Bin 0 -> 342 bytes .../client-tags/resource.json | 16 + .../ignition/global-props/data.bin | Bin 0 -> 457 bytes .../ignition/global-props/resource.json | 16 + .../PrimeControls/alarms/code.py | 402 +++ .../PrimeControls/alarms/resource.json | 17 + .../script-python/PrimeControls/calc/code.py | 807 ++++++ .../PrimeControls/calc/resource.json | 17 + .../script-python/PrimeControls/fmt/code.py | 324 +++ .../PrimeControls/fmt/resource.json | 17 + .../projects/AlarmAnalysis/project.json | 7 + .../session-props/props.json | 6 +- .../session-props/resource.json | 10 +- .../Components/HealthGauge/resource.json | 13 +- .../Components/HealthGauge/view.json | 13 +- .../Components/KpiCard/resource.json | 13 +- .../Components/KpiCard/view.json | 45 +- .../PrimeControls/Dashboard/resource.json | 13 +- .../views/PrimeControls/Dashboard/view.json | 17 +- .../Popups/AlarmDetail/resource.json | 13 +- .../Popups/AlarmDetail/view.json | 76 +- .../Popups/EventDetail/view.json | 7 +- .../Popups/HealthScore/view.json | 6 +- .../PrimeControls/Shell/FilterBar/view.json | 18 +- .../PrimeControls/Shell/Header/resource.json | 13 +- .../PrimeControls/Shell/Header/view.json | 81 +- .../PrimeControls/Tabs/Analysis/view.json | 7 +- .../Tabs/BadActors/resource.json | 13 +- .../PrimeControls/Tabs/BadActors/view.json | 67 +- .../PrimeControls/Tabs/Journal/view.json | 7 +- .../PrimeControls/Tabs/Overview/view.json | 56 +- .../client-tags/data.bin | Bin 0 -> 342 bytes .../client-tags/resource.json | 16 + .../PrimeControls/alarms/code.py | 21 +- .../PrimeControls/alarms/resource.json | 9 +- .../script-python/PrimeControls/calc/code.py | 87 +- .../PrimeControls/calc/resource.json | 9 +- .../script-python/PrimeControls/fmt/code.py | 2 +- .../PrimeControls/fmt/resource.json | 9 +- .../ignition/script-python/probe/code.py | 88 +- .../ignition/timer/ProbeTick/resource.json | 2 +- test-data/simulation_tags.json | 20 +- tests/test_calc.py | 52 +- tools/lint_project.py | 6 + 165 files changed, 13988 insertions(+), 525 deletions(-) delete mode 100644 ignition/gateway/config/ignition/tags/valueStore.idb create mode 100644 ignition/gateway/config/resources/core/ignition/images/PrimeControls/image (4).png/image (4).png create mode 100644 ignition/gateway/config/resources/core/ignition/images/PrimeControls/image (4).png/resource.json delete mode 100644 ignition/gateway/config/resources/local/ignition/local-system-properties/config.json delete mode 100644 ignition/gateway/config/resources/local/ignition/local-system-properties/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/page-config/config.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/page-config/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/session-props/props.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/session-props/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Card/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Card/style.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/CardTitle/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/CardTitle/style.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Chip/Bad/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Chip/Bad/style.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Chip/Flat/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Chip/Flat/style.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Chip/Good/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Chip/Good/style.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Empty/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Empty/style.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Grade/A/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Grade/A/style.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Grade/B/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Grade/B/style.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Grade/C/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Grade/C/style.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Grade/D/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Grade/D/style.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Grade/F/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Grade/F/style.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Grade/NA/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Grade/NA/style.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/PageBg/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/PageBg/style.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Priority/Critical/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Priority/Critical/style.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Priority/Diagnostic/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Priority/Diagnostic/style.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Priority/High/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Priority/High/style.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Priority/Low/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Priority/Low/style.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Priority/Medium/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Priority/Medium/style.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/PrioritySoft/Critical/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/PrioritySoft/Critical/style.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/PrioritySoft/Diagnostic/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/PrioritySoft/Diagnostic/style.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/PrioritySoft/High/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/PrioritySoft/High/style.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/PrioritySoft/Low/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/PrioritySoft/Low/style.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/PrioritySoft/Medium/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/PrioritySoft/Medium/style.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/State/Acked/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/State/Acked/style.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/State/Active/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/State/Active/style.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/State/Cleared/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/State/Cleared/style.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Tab/Active/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Tab/Active/style.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Tab/Inactive/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Tab/Inactive/style.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Text/Big/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Text/Big/style.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Text/Kpi/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Text/Kpi/style.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Text/Muted/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Text/Muted/style.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Toolbar/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/style-classes/PrimeControls/Toolbar/style.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/EmptyState/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/EmptyState/view.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/HealthGauge/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/HealthGauge/view.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/HeatmapCell/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/HeatmapCell/view.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/InsightRow/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/InsightRow/view.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/KpiCard/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/KpiCard/view.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/PriorityCard/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/PriorityCard/view.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/ScoreBar/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/ScoreBar/view.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/SourceRow/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/SourceRow/view.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/TabButton/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/TabButton/view.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Dashboard/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Dashboard/view.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Popups/AlarmDetail/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Popups/AlarmDetail/view.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Popups/EventDetail/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Popups/EventDetail/view.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Popups/HealthScore/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Popups/HealthScore/view.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Popups/ShiftReport/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Popups/ShiftReport/view.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Shell/FilterBar/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Shell/FilterBar/view.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Shell/Header/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Shell/Header/view.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Analysis/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Analysis/view.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Tabs/BadActors/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Tabs/BadActors/view.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Journal/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Journal/view.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Overview/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Overview/view.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.vision/client-tags/data.bin create mode 100644 ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.vision/client-tags/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/ignition/global-props/data.bin create mode 100644 ignition/gateway/projects/AlarmAnalysis/ignition/global-props/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/ignition/script-python/PrimeControls/alarms/code.py create mode 100644 ignition/gateway/projects/AlarmAnalysis/ignition/script-python/PrimeControls/alarms/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/ignition/script-python/PrimeControls/calc/code.py create mode 100644 ignition/gateway/projects/AlarmAnalysis/ignition/script-python/PrimeControls/calc/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/ignition/script-python/PrimeControls/fmt/code.py create mode 100644 ignition/gateway/projects/AlarmAnalysis/ignition/script-python/PrimeControls/fmt/resource.json create mode 100644 ignition/gateway/projects/AlarmAnalysis/project.json create mode 100644 ignition/gateway/projects/PrimeBAT/com.inductiveautomation.vision/client-tags/data.bin create mode 100644 ignition/gateway/projects/PrimeBAT/com.inductiveautomation.vision/client-tags/resource.json diff --git a/.gitignore b/.gitignore index 382b09f..e9f7a83 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,11 @@ ignition/gateway/**/.resources/ ignition/gateway/projects/.probe/ __pycache__/ .pytest_cache/ +# Gateway/Designer runtime churn (ONBOARDING §6) — never commit +ignition/gateway/config/ignition/tags/valueStore.idb +ignition/gateway/config/resources/local/ignition/local-system-properties/ +ignition/gateway/config/resources/migration-log-*.md +# Designer save artifacts +ignition/gateway/projects/**/thumbnail.png +# Harvested component schemas (regenerate with tools/schema_harvest.sh) +.schemas/ diff --git a/ignition/gateway/config/ignition/tags/valueStore.idb b/ignition/gateway/config/ignition/tags/valueStore.idb deleted file mode 100644 index 366ba4a6a303d6676bf8eb85db0afb0992b6b920..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20480 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 rf*>@3IMjk52!bF8f*=Tj&; 0.55:\n\t\t\treturn '#FFFFFF'\n\texcept:\n\t\tpass\n\treturn 'var(--neutral-90)'", + "type": "script" + } + ], + "type": "property" + } + }, + "props.text": { + "binding": { + "config": { + "path": "view.params.count" + }, + "transforms": [ + { + "code": "\treturn '' if not value else value", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "borderRadius": "2px", + "fontSize": "10px", + "marginBottom": "1px", + "marginRight": "1px", + "textAlign": "center" + } + }, + "type": "ia.display.label" + } + ], + "meta": { + "name": "root" + }, + "props": { + "direction": "row", + "style": { + "alignItems": "stretch" + } + }, + "type": "ia.container.flex" + } +} \ No newline at end of file diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/InsightRow/resource.json b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/InsightRow/resource.json new file mode 100644 index 0000000..276dced --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/InsightRow/resource.json @@ -0,0 +1,10 @@ +{ + "scope": "G", + "version": 1, + "restricted": false, + "overridable": true, + "files": [ + "view.json" + ], + "attributes": {} +} diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/InsightRow/view.json b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/InsightRow/view.json new file mode 100644 index 0000000..6072c17 --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/InsightRow/view.json @@ -0,0 +1,121 @@ +{ + "custom": {}, + "params": { + "icon": null, + "severity": null, + "tab": null, + "text": null + }, + "propConfig": { + "params.icon": { + "paramDirection": "input", + "persistent": false + }, + "params.severity": { + "paramDirection": "input", + "persistent": false + }, + "params.tab": { + "paramDirection": "input", + "persistent": false + }, + "params.text": { + "paramDirection": "input", + "persistent": false + } + }, + "props": {}, + "root": { + "children": [ + { + "meta": { + "name": "icon" + }, + "position": { + "basis": "24px", + "shrink": 0 + }, + "propConfig": { + "props.color": { + "binding": { + "config": { + "path": "view.params.severity" + }, + "transforms": [ + { + "code": "\tcolors = {0: '#D64550', 1: '#E8883A', 2: '#3B7DD8', 3: 'var(--neutral-60)'}\n\treturn colors.get(value, 'var(--neutral-60)')", + "type": "script" + } + ], + "type": "property" + } + }, + "props.path": { + "binding": { + "config": { + "path": "view.params.icon" + }, + "transforms": [ + { + "code": "\treturn 'material/' + (value or 'insights')", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "path": "material/insights", + "style": { + "fontSize": "18px" + } + }, + "type": "ia.display.icon" + }, + { + "meta": { + "name": "text" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.params.text" + }, + "transforms": [ + { + "code": "\treturn value or ''", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "fontSize": "13px" + } + }, + "type": "ia.display.label" + } + ], + "meta": { + "name": "root" + }, + "props": { + "direction": "row", + "style": { + "alignItems": "center", + "gap": "8px", + "paddingBottom": "4px", + "paddingTop": "4px" + } + }, + "type": "ia.container.flex" + } +} \ No newline at end of file diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/KpiCard/resource.json b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/KpiCard/resource.json new file mode 100644 index 0000000..f597cc7 --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/KpiCard/resource.json @@ -0,0 +1,16 @@ +{ + "scope": "G", + "version": 1, + "restricted": false, + "overridable": true, + "files": [ + "view.json" + ], + "attributes": { + "lastModificationSignature": "75b1aa709e8638ac045f15b7a6d3543d023b8f3e886e2f9f1c263c0390c4fe9d", + "lastModification": { + "actor": "admin", + "timestamp": "2026-07-16T20:45:13Z" + } + } +} diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/KpiCard/view.json b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/KpiCard/view.json new file mode 100644 index 0000000..4140c30 --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/KpiCard/view.json @@ -0,0 +1,199 @@ +{ + "custom": {}, + "params": {}, + "propConfig": { + "params.delta": { + "paramDirection": "input", + "persistent": false + }, + "params.label": { + "paramDirection": "input", + "persistent": false + }, + "params.unit": { + "paramDirection": "input", + "persistent": false + }, + "params.value": { + "paramDirection": "input", + "persistent": false + } + }, + "props": {}, + "root": { + "children": [ + { + "meta": { + "name": "lbl" + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.params.label" + }, + "type": "property" + } + } + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted", + "lineHeight": "1.1" + } + }, + "type": "ia.display.label", + "position": { + "shrink": 0 + } + }, + { + "children": [ + { + "meta": { + "name": "value" + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.params.value" + }, + "transforms": [ + { + "code": "\treturn value if value is not None else '--'", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Kpi", + "lineHeight": "1.1" + } + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "unit" + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.params.unit" + }, + "transforms": [ + { + "code": "\treturn value or ''", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted", + "lineHeight": "1.1" + } + }, + "type": "ia.display.label" + } + ], + "meta": { + "name": "valueRow" + }, + "position": { + "grow": 1, + "shrink": 1, + "basis": "auto" + }, + "props": { + "style": { + "alignItems": "baseline", + "gap": "4px" + } + }, + "type": "ia.container.flex" + }, + { + "meta": { + "name": "chip" + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.params.delta" + }, + "transforms": [ + { + "code": "\treturn value is not None", + "type": "script" + } + ], + "type": "property" + } + }, + "props.style.classes": { + "binding": { + "config": { + "path": "view.params.delta" + }, + "transforms": [ + { + "code": "\tbase = 'PrimeControls/Chip/Flat'\n\ttry:\n\t\tif value and value['good'] == True:\n\t\t\tbase = 'PrimeControls/Chip/Good'\n\t\telif value and value['good'] == False:\n\t\t\tbase = 'PrimeControls/Chip/Bad'\n\texcept:\n\t\tpass\n\treturn base", + "type": "script" + } + ], + "type": "property" + } + }, + "props.text": { + "binding": { + "config": { + "path": "view.params.delta" + }, + "transforms": [ + { + "code": "\tif not value:\n\t\treturn ''\n\ttry:\n\t\tchip = PrimeControls.fmt.delta_chip(value)\n\t\treturn '%s %s' % (chip['arrow'], chip['text'])\n\texcept:\n\t\treturn ''", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "alignSelf": "flex-start", + "padding": "0 6px" + } + }, + "type": "ia.display.label", + "position": { + "shrink": 0 + } + } + ], + "meta": { + "name": "root" + }, + "props": { + "direction": "column", + "style": { + "classes": "PrimeControls/Card", + "justifyContent": "space-between", + "overflow": "hidden", + "padding": "6px 12px" + } + }, + "type": "ia.container.flex" + } +} diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/PriorityCard/resource.json b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/PriorityCard/resource.json new file mode 100644 index 0000000..276dced --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/PriorityCard/resource.json @@ -0,0 +1,10 @@ +{ + "scope": "G", + "version": 1, + "restricted": false, + "overridable": true, + "files": [ + "view.json" + ], + "attributes": {} +} diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/PriorityCard/view.json b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/PriorityCard/view.json new file mode 100644 index 0000000..5096d0e --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/PriorityCard/view.json @@ -0,0 +1,137 @@ +{ + "custom": {}, + "params": { + "count": null, + "pct": null, + "priority_name": null + }, + "propConfig": { + "params.count": { + "paramDirection": "input", + "persistent": false + }, + "params.pct": { + "paramDirection": "input", + "persistent": false + }, + "params.priority_name": { + "paramDirection": "input", + "persistent": false + } + }, + "props": {}, + "root": { + "children": [ + { + "meta": { + "name": "name" + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.params.priority_name" + }, + "transforms": [ + { + "code": "\treturn value or 'Unknown'", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "fontSize": "13px", + "fontWeight": "bold" + } + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "count" + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.params.count" + }, + "transforms": [ + { + "code": "\treturn PrimeControls.fmt.num(value)", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Big" + } + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "pct" + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.params.pct" + }, + "transforms": [ + { + "code": "\treturn PrimeControls.fmt.pct(value) + ' of events' if value is not None else ''", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted" + } + }, + "type": "ia.display.label" + } + ], + "meta": { + "name": "root" + }, + "propConfig": { + "props.style.classes": { + "binding": { + "config": { + "path": "view.params.priority_name" + }, + "transforms": [ + { + "code": "\tnames = ['Diagnostic', 'Low', 'Medium', 'High', 'Critical']\n\tn = value if value in names else 'Diagnostic'\n\treturn 'PrimeControls/Card PrimeControls/PrioritySoft/' + n", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "direction": "column", + "style": { + "alignItems": "center", + "classes": "PrimeControls/Card", + "gap": "2px" + } + }, + "type": "ia.container.flex" + } +} \ No newline at end of file diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/ScoreBar/resource.json b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/ScoreBar/resource.json new file mode 100644 index 0000000..276dced --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/ScoreBar/resource.json @@ -0,0 +1,10 @@ +{ + "scope": "G", + "version": 1, + "restricted": false, + "overridable": true, + "files": [ + "view.json" + ], + "attributes": {} +} diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/ScoreBar/view.json b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/ScoreBar/view.json new file mode 100644 index 0000000..7a9f40a --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/ScoreBar/view.json @@ -0,0 +1,207 @@ +{ + "custom": {}, + "params": { + "detail": null, + "key": null, + "label": null, + "score": null, + "weight": null + }, + "propConfig": { + "params.detail": { + "paramDirection": "input", + "persistent": false + }, + "params.key": { + "paramDirection": "input", + "persistent": false + }, + "params.label": { + "paramDirection": "input", + "persistent": false + }, + "params.score": { + "paramDirection": "input", + "persistent": false + }, + "params.weight": { + "paramDirection": "input", + "persistent": false + } + }, + "props": {}, + "root": { + "children": [ + { + "children": [ + { + "meta": { + "name": "lbl" + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.params.label" + }, + "transforms": [ + { + "code": "\treturn value or ''", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "fontSize": "13px", + "fontWeight": "bold" + } + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "score" + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.params.score" + }, + "transforms": [ + { + "code": "\treturn '%d' % round(value) if value is not None else 'N/A'", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "fontSize": "13px", + "fontWeight": "bold" + } + }, + "type": "ia.display.label" + } + ], + "meta": { + "name": "topRow" + }, + "props": { + "direction": "row", + "style": { + "justifyContent": "space-between" + } + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "children": [], + "meta": { + "name": "fill" + }, + "propConfig": { + "props.style.backgroundColor": { + "binding": { + "config": { + "path": "view.params.score" + }, + "transforms": [ + { + "code": "\ttry:\n\t\ts = float(value)\n\texcept:\n\t\treturn 'var(--neutral-40)'\n\tif s >= 80:\n\t\treturn '#2E9E5B'\n\tif s >= 60:\n\t\treturn '#E5C453'\n\treturn '#D64550'", + "type": "script" + } + ], + "type": "property" + } + }, + "props.style.width": { + "binding": { + "config": { + "path": "view.params.score" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn '%d%%' % max(0, min(100, round(value)))\n\texcept:\n\t\treturn '0%'", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "direction": "row", + "style": { + "borderRadius": "4px" + } + }, + "type": "ia.container.flex" + } + ], + "meta": { + "name": "track" + }, + "position": { + "basis": "8px", + "shrink": 0 + }, + "props": { + "direction": "row", + "style": { + "backgroundColor": "var(--neutral-30)", + "borderRadius": "4px" + } + }, + "type": "ia.container.flex" + }, + { + "meta": { + "name": "detail" + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.params.detail" + }, + "transforms": [ + { + "code": "\treturn value or ''", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted" + } + }, + "type": "ia.display.label" + } + ], + "meta": { + "name": "root" + }, + "props": { + "direction": "column", + "style": { + "gap": "2px", + "paddingBottom": "8px" + } + }, + "type": "ia.container.flex" + } +} \ No newline at end of file diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/SourceRow/resource.json b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/SourceRow/resource.json new file mode 100644 index 0000000..276dced --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/SourceRow/resource.json @@ -0,0 +1,10 @@ +{ + "scope": "G", + "version": 1, + "restricted": false, + "overridable": true, + "files": [ + "view.json" + ], + "attributes": {} +} diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/SourceRow/view.json b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/SourceRow/view.json new file mode 100644 index 0000000..86db373 --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/SourceRow/view.json @@ -0,0 +1,192 @@ +{ + "custom": {}, + "params": { + "count": null, + "label": null, + "pct": null, + "rank": null, + "source": null + }, + "propConfig": { + "params.count": { + "paramDirection": "input", + "persistent": false + }, + "params.label": { + "paramDirection": "input", + "persistent": false + }, + "params.pct": { + "paramDirection": "input", + "persistent": false + }, + "params.rank": { + "paramDirection": "input", + "persistent": false + }, + "params.source": { + "paramDirection": "input", + "persistent": false + } + }, + "props": {}, + "root": { + "children": [ + { + "meta": { + "name": "rank" + }, + "position": { + "basis": "24px", + "shrink": 0 + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.params.rank" + }, + "transforms": [ + { + "code": "\treturn '#%s' % value if value is not None else ''", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted" + } + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "lbl" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.params.label" + }, + "transforms": [ + { + "code": "\treturn value or ''", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "fontSize": "13px", + "fontWeight": "bold" + } + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "count" + }, + "position": { + "basis": "56px", + "shrink": 0 + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.params.count" + }, + "transforms": [ + { + "code": "\treturn PrimeControls.fmt.num(value)", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "fontSize": "13px", + "fontWeight": "bold", + "textAlign": "right" + } + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "pct" + }, + "position": { + "basis": "48px", + "shrink": 0 + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.params.pct" + }, + "transforms": [ + { + "code": "\treturn PrimeControls.fmt.pct(value) if value is not None else ''", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted", + "textAlign": "right" + } + }, + "type": "ia.display.label" + } + ], + "events": { + "dom": { + "onClick": { + "config": { + "script": "\tself.session.custom.PrimeControls.ui.badActorFocus = self.view.params.source\n\tself.session.custom.PrimeControls.ui.selectedTab = 2" + }, + "scope": "G", + "type": "script" + } + } + }, + "meta": { + "name": "root" + }, + "props": { + "direction": "row", + "style": { + "alignItems": "center", + "borderBottomColor": "var(--neutral-30)", + "borderBottomStyle": "solid", + "borderBottomWidth": "1px", + "cursor": "pointer", + "gap": "8px", + "paddingBottom": "4px", + "paddingTop": "4px" + } + }, + "type": "ia.container.flex" + } +} \ No newline at end of file diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/TabButton/resource.json b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/TabButton/resource.json new file mode 100644 index 0000000..276dced --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/TabButton/resource.json @@ -0,0 +1,10 @@ +{ + "scope": "G", + "version": 1, + "restricted": false, + "overridable": true, + "files": [ + "view.json" + ], + "attributes": {} +} diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/TabButton/view.json b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/TabButton/view.json new file mode 100644 index 0000000..bab323f --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Components/TabButton/view.json @@ -0,0 +1,69 @@ +{ + "custom": {}, + "params": { + "index": null, + "title": null + }, + "propConfig": { + "params.index": { + "paramDirection": "input", + "persistent": false + }, + "params.title": { + "paramDirection": "input", + "persistent": false + } + }, + "props": {}, + "root": { + "children": [ + { + "events": { + "dom": { + "onClick": { + "config": { + "script": "\tself.session.custom.PrimeControls.ui.selectedTab = self.view.params.index" + }, + "scope": "G", + "type": "script" + } + } + }, + "meta": { + "name": "btn" + }, + "propConfig": { + "props.style.classes": { + "binding": { + "config": { + "expression": "if({view.params.index} = {session.custom.PrimeControls.ui.selectedTab}, 'PrimeControls/Tab/Active', 'PrimeControls/Tab/Inactive')" + }, + "type": "expr" + } + }, + "props.text": { + "binding": { + "config": { + "path": "view.params.title" + }, + "type": "property" + } + } + }, + "props": {}, + "type": "ia.display.label" + } + ], + "meta": { + "name": "root" + }, + "props": { + "direction": "row", + "style": { + "alignItems": "center", + "justifyContent": "center" + } + }, + "type": "ia.container.flex" + } +} \ No newline at end of file diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Dashboard/resource.json b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Dashboard/resource.json new file mode 100644 index 0000000..d0fbe8c --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Dashboard/resource.json @@ -0,0 +1,16 @@ +{ + "scope": "G", + "version": 1, + "restricted": false, + "overridable": true, + "files": [ + "view.json" + ], + "attributes": { + "lastModificationSignature": "04bdfb9c4eecc4f8cf35a85994e6790ef8d62a1d1aa00553e4dc4193e171a110", + "lastModification": { + "actor": "admin", + "timestamp": "2026-07-16T20:31:35Z" + } + } +} diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Dashboard/view.json b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Dashboard/view.json new file mode 100644 index 0000000..dc8f982 --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Dashboard/view.json @@ -0,0 +1,330 @@ +{ + "custom": {}, + "events": { + "system": { + "onStartup": { + "config": { + "script": "\tq = self.session.custom.PrimeControls.query\n\tif not q.startMs or q.startMs == 0:\n\t\tnow = system.date.toMillis(system.date.now())\n\t\tq.endMs = now\n\t\tq.startMs = now - 8 * 3600 * 1000\n\t\tq.rangePreset = '8h'" + }, + "scope": "G", + "type": "script" + } + } + }, + "params": {}, + "propConfig": { + "custom.bundle": { + "binding": { + "config": { + "expression": "{session.custom.PrimeControls.query.refreshToken} + '|' + {session.custom.PrimeControls.query.startMs} + '|' + {session.custom.PrimeControls.query.endMs}" + }, + "transforms": [ + { + "code": "\tq = self.session.custom.PrimeControls.query\n\tif not q.startMs or not q.endMs:\n\t\treturn None\n\treturn PrimeControls.alarms.getDashboardBundle(q.startMs, q.endMs, {'priorities': list(q.priorities), 'areas': list(q.areas), 'states': list(q.states), 'search': q.search})", + "type": "script" + } + ], + "type": "expr" + } + } + }, + "props": { + "defaultSize": { + "height": 900, + "width": 1600 + } + }, + "root": { + "children": [ + { + "meta": { + "name": "header" + }, + "position": { + "basis": "150px", + "shrink": 0 + }, + "propConfig": { + "props.params.bundle": { + "binding": { + "config": { + "path": "view.custom.bundle" + }, + "type": "property" + } + } + }, + "props": { + "path": "PrimeControls/Shell/Header" + }, + "type": "ia.display.view" + }, + { + "meta": { + "name": "filterBar" + }, + "position": { + "basis": "auto", + "shrink": 0 + }, + "propConfig": { + "props.params.bundle": { + "binding": { + "config": { + "path": "view.custom.bundle" + }, + "type": "property" + } + }, + "position.display": { + "binding": { + "config": { + "expression": "{session.custom.PrimeControls.ui.selectedTab} != 0" + }, + "type": "expr" + } + } + }, + "props": { + "path": "PrimeControls/Shell/FilterBar", + "style": { + "minHeight": "56px" + } + }, + "type": "ia.display.view" + }, + { + "children": [ + { + "meta": { + "name": "tabBtn0" + }, + "position": { + "basis": "140px", + "shrink": 0 + }, + "props": { + "params": { + "index": 0, + "title": "Overview" + }, + "path": "PrimeControls/Components/TabButton" + }, + "type": "ia.display.view" + }, + { + "meta": { + "name": "tabBtn1" + }, + "position": { + "basis": "140px", + "shrink": 0 + }, + "props": { + "params": { + "index": 1, + "title": "Analysis" + }, + "path": "PrimeControls/Components/TabButton" + }, + "type": "ia.display.view" + }, + { + "meta": { + "name": "tabBtn2" + }, + "position": { + "basis": "140px", + "shrink": 0 + }, + "props": { + "params": { + "index": 2, + "title": "Bad Actors" + }, + "path": "PrimeControls/Components/TabButton" + }, + "type": "ia.display.view" + }, + { + "meta": { + "name": "tabBtn3" + }, + "position": { + "basis": "140px", + "shrink": 0 + }, + "props": { + "params": { + "index": 3, + "title": "Journal" + }, + "path": "PrimeControls/Components/TabButton" + }, + "type": "ia.display.view" + } + ], + "meta": { + "name": "tabBar" + }, + "position": { + "basis": "42px", + "shrink": 0 + }, + "props": { + "style": { + "borderBottomColor": "var(--neutral-40)", + "borderBottomStyle": "solid", + "borderBottomWidth": "1px" + } + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "meta": { + "name": "tabOverview" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "expression": "{session.custom.PrimeControls.ui.selectedTab} = 0" + }, + "type": "expr" + } + }, + "props.params.bundle": { + "binding": { + "config": { + "path": "view.custom.bundle" + }, + "type": "property" + } + } + }, + "props": { + "path": "PrimeControls/Tabs/Overview" + }, + "type": "ia.display.view" + }, + { + "meta": { + "name": "tabAnalysis" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "expression": "{session.custom.PrimeControls.ui.selectedTab} = 1" + }, + "type": "expr" + } + }, + "props.params.bundle": { + "binding": { + "config": { + "path": "view.custom.bundle" + }, + "type": "property" + } + } + }, + "props": { + "path": "PrimeControls/Tabs/Analysis" + }, + "type": "ia.display.view" + }, + { + "meta": { + "name": "tabBadActors" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "expression": "{session.custom.PrimeControls.ui.selectedTab} = 2" + }, + "type": "expr" + } + }, + "props.params.bundle": { + "binding": { + "config": { + "path": "view.custom.bundle" + }, + "type": "property" + } + } + }, + "props": { + "path": "PrimeControls/Tabs/BadActors" + }, + "type": "ia.display.view" + }, + { + "meta": { + "name": "tabJournal" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "expression": "{session.custom.PrimeControls.ui.selectedTab} = 3" + }, + "type": "expr" + } + }, + "props.params.bundle": { + "binding": { + "config": { + "path": "view.custom.bundle" + }, + "type": "property" + } + } + }, + "props": { + "path": "PrimeControls/Tabs/Journal" + }, + "type": "ia.display.view" + } + ], + "meta": { + "name": "tabContent" + }, + "position": { + "grow": 1 + }, + "props": { + "direction": "column" + }, + "type": "ia.container.flex" + } + ], + "meta": { + "name": "root" + }, + "props": { + "direction": "column", + "style": { + "classes": "PrimeControls/PageBg", + "gap": "10px" + } + }, + "type": "ia.container.flex" + } +} diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Popups/AlarmDetail/resource.json b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Popups/AlarmDetail/resource.json new file mode 100644 index 0000000..fc7d184 --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Popups/AlarmDetail/resource.json @@ -0,0 +1,16 @@ +{ + "scope": "G", + "version": 1, + "restricted": false, + "overridable": true, + "files": [ + "view.json" + ], + "attributes": { + "lastModificationSignature": "1d6feda3a22e57c52a032bea5ed828b0b7836e249e46a68faa58af39207b9119", + "lastModification": { + "actor": "admin", + "timestamp": "2026-07-16T20:41:00Z" + } + } +} diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Popups/AlarmDetail/view.json b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Popups/AlarmDetail/view.json new file mode 100644 index 0000000..9addc44 --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Popups/AlarmDetail/view.json @@ -0,0 +1,790 @@ +{ + "custom": {}, + "params": {}, + "propConfig": { + "custom.detail": { + "binding": { + "config": { + "expression": "{view.params.source} + \u0027|\u0027 + {session.custom.PrimeControls.query.startMs} + \u0027|\u0027 + {session.custom.PrimeControls.query.endMs}" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tsrc \u003d self.view.params.source\n\texcept:\n\t\tsrc \u003d None\n\tif not src:\n\t\treturn None\n\ttry:\n\t\tq \u003d self.session.custom.PrimeControls.query\n\t\treturn PrimeControls.alarms.getSourceDetail(src, q.startMs, q.endMs)\n\texcept:\n\t\treturn None", + "type": "script" + } + ], + "type": "expr" + } + }, + "params.label": { + "paramDirection": "input", + "persistent": false + }, + "params.source": { + "paramDirection": "input", + "persistent": false + } + }, + "props": { + "defaultSize": { + "height": 660, + "width": 720 + } + }, + "root": { + "children": [ + { + "children": [ + { + "meta": { + "name": "lblTitle" + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.params.label" + }, + "transforms": [ + { + "code": "\tif value:\n\t\treturn value\n\ttry:\n\t\treturn self.view.params.source or \u0027Alarm Detail\u0027\n\texcept:\n\t\treturn \u0027Alarm Detail\u0027", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Big" + } + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "lblArea" + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.custom.detail" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn value[\u0027area\u0027] or \u0027\u0027\n\texcept:\n\t\treturn \u0027\u0027", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted" + } + }, + "type": "ia.display.label" + } + ], + "meta": { + "name": "headerCol" + }, + "position": { + "shrink": 0 + }, + "props": { + "direction": "column" + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "children": [ + { + "meta": { + "name": "kpiCount" + }, + "position": { + "basis": "100px", + "grow": 1 + }, + "propConfig": { + "props.params.value": { + "binding": { + "config": { + "path": "view.custom.detail" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn PrimeControls.fmt.num(value[\u0027stats\u0027][\u0027count\u0027])\n\texcept:\n\t\treturn \u0027--\u0027", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "params": { + "label": "Activations" + }, + "path": "PrimeControls/Components/KpiCard" + }, + "type": "ia.display.view" + }, + { + "meta": { + "name": "kpiTta" + }, + "position": { + "basis": "100px", + "grow": 1 + }, + "propConfig": { + "props.params.value": { + "binding": { + "config": { + "path": "view.custom.detail" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn PrimeControls.fmt.dur(value[\u0027stats\u0027][\u0027avg_tta_ms\u0027])\n\texcept:\n\t\treturn \u0027--\u0027", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "params": { + "label": "Avg Time to Ack" + }, + "path": "PrimeControls/Components/KpiCard" + }, + "type": "ia.display.view" + }, + { + "meta": { + "name": "kpiActive" + }, + "position": { + "basis": "100px", + "grow": 1 + }, + "propConfig": { + "props.params.value": { + "binding": { + "config": { + "path": "view.custom.detail" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn PrimeControls.fmt.dur(value[\u0027stats\u0027][\u0027avg_active_ms\u0027])\n\texcept:\n\t\treturn \u0027--\u0027", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "params": { + "label": "Avg Active Time" + }, + "path": "PrimeControls/Components/KpiCard" + }, + "type": "ia.display.view" + }, + { + "meta": { + "name": "kpiFleeting" + }, + "position": { + "basis": "100px", + "grow": 1 + }, + "propConfig": { + "props.params.value": { + "binding": { + "config": { + "path": "view.custom.detail" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn PrimeControls.fmt.num(value[\u0027stats\u0027][\u0027fleeting_count\u0027])\n\texcept:\n\t\treturn \u0027--\u0027", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "params": { + "label": "Fleeting" + }, + "path": "PrimeControls/Components/KpiCard" + }, + "type": "ia.display.view" + } + ], + "meta": { + "name": "statsRow" + }, + "position": { + "basis": "88px", + "shrink": 0 + }, + "props": { + "style": { + "gap": "10px" + } + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "meta": { + "name": "lblDailyTitle" + }, + "position": { + "shrink": 0 + }, + "props": { + "style": { + "classes": "PrimeControls/CardTitle" + }, + "text": "Daily activity" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "repDaily" + }, + "position": { + "basis": "72px", + "shrink": 0 + }, + "propConfig": { + "props.instances": { + "binding": { + "config": { + "path": "view.custom.detail" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tdaily \u003d list(value[\u0027daily\u0027] or [])\n\texcept:\n\t\tdaily \u003d []\n\tcounts \u003d []\n\tfor d in daily:\n\t\ttry:\n\t\t\tcounts.append(int(d[\u0027count\u0027]))\n\t\texcept:\n\t\t\tcounts.append(0)\n\tif not counts:\n\t\treturn []\n\tm \u003d max(counts)\n\treturn [{\u0027count\u0027: c, \u0027max\u0027: m} for c in counts]", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "elementPosition": { + "basis": "8px" + }, + "path": "PrimeControls/Components/HeatmapCell", + "style": { + "gap": "2px" + }, + "useDefaultViewHeight": false, + "useDefaultViewWidth": false + }, + "type": "ia.display.flex-repeater" + }, + { + "meta": { + "name": "lblDailyRange" + }, + "position": { + "shrink": 0 + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.custom.detail" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tdaily \u003d list(value[\u0027daily\u0027] or [])\n\texcept:\n\t\tdaily \u003d []\n\tif not daily:\n\t\treturn \u0027No activity in range\u0027\n\ttry:\n\t\treturn \u0027Activations per day, %s - %s\u0027 % (PrimeControls.fmt.day_clock(daily[0][\u0027t0\u0027]), PrimeControls.fmt.day_clock(daily[-1][\u0027t0\u0027]))\n\texcept:\n\t\treturn \u0027Activations per day\u0027", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted" + } + }, + "type": "ia.display.label" + } + ], + "meta": { + "name": "cardDaily" + }, + "position": { + "shrink": 0 + }, + "props": { + "direction": "column", + "style": { + "classes": "PrimeControls/Card", + "gap": "6px" + } + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "meta": { + "name": "lblAckTitle" + }, + "position": { + "shrink": 0 + }, + "props": { + "style": { + "classes": "PrimeControls/CardTitle" + }, + "text": "Top ack users" + }, + "type": "ia.display.label" + }, + { + "children": [ + { + "meta": { + "name": "ack0" + }, + "position": { + "shrink": 0 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.custom.detail" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn len(value[\u0027stats\u0027][\u0027top_ack_users\u0027]) \u003e 0\n\texcept:\n\t\treturn False", + "type": "script" + } + ], + "type": "property" + } + }, + "props.text": { + "binding": { + "config": { + "path": "view.custom.detail" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tu \u003d value[\u0027stats\u0027][\u0027top_ack_users\u0027][0]\n\t\treturn \u0027%s (%d)\u0027 % (u[\u0027user\u0027], u[\u0027count\u0027])\n\texcept:\n\t\treturn \u0027\u0027", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "classes": "PrimeControls/Chip/Flat", + "paddingBottom": "2px", + "paddingTop": "2px" + } + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "ack1" + }, + "position": { + "shrink": 0 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.custom.detail" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn len(value[\u0027stats\u0027][\u0027top_ack_users\u0027]) \u003e 1\n\texcept:\n\t\treturn False", + "type": "script" + } + ], + "type": "property" + } + }, + "props.text": { + "binding": { + "config": { + "path": "view.custom.detail" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tu \u003d value[\u0027stats\u0027][\u0027top_ack_users\u0027][1]\n\t\treturn \u0027%s (%d)\u0027 % (u[\u0027user\u0027], u[\u0027count\u0027])\n\texcept:\n\t\treturn \u0027\u0027", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "classes": "PrimeControls/Chip/Flat", + "paddingBottom": "2px", + "paddingTop": "2px" + } + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "ack2" + }, + "position": { + "shrink": 0 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.custom.detail" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn len(value[\u0027stats\u0027][\u0027top_ack_users\u0027]) \u003e 2\n\texcept:\n\t\treturn False", + "type": "script" + } + ], + "type": "property" + } + }, + "props.text": { + "binding": { + "config": { + "path": "view.custom.detail" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tu \u003d value[\u0027stats\u0027][\u0027top_ack_users\u0027][2]\n\t\treturn \u0027%s (%d)\u0027 % (u[\u0027user\u0027], u[\u0027count\u0027])\n\texcept:\n\t\treturn \u0027\u0027", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "classes": "PrimeControls/Chip/Flat", + "paddingBottom": "2px", + "paddingTop": "2px" + } + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "lblNoAcks" + }, + "position": { + "shrink": 0 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.custom.detail" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn len(value[\u0027stats\u0027][\u0027top_ack_users\u0027]) \u003d\u003d 0\n\texcept:\n\t\treturn True", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted" + }, + "text": "No acknowledgements in range" + }, + "type": "ia.display.label" + } + ], + "meta": { + "name": "ackRow" + }, + "position": { + "shrink": 0 + }, + "props": { + "alignItems": "center", + "style": { + "gap": "6px" + }, + "wrap": "wrap" + }, + "type": "ia.container.flex" + } + ], + "meta": { + "name": "cardAcks" + }, + "position": { + "shrink": 0 + }, + "props": { + "direction": "column", + "style": { + "classes": "PrimeControls/Card", + "gap": "6px" + } + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "meta": { + "name": "lblEvTitle" + }, + "position": { + "shrink": 0 + }, + "props": { + "style": { + "classes": "PrimeControls/CardTitle" + }, + "text": "Recent events" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "tblEvents" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "props.data": { + "binding": { + "config": { + "path": "view.custom.detail" + }, + "transforms": [ + { + "code": "\tout \u003d []\n\ttry:\n\t\trows \u003d list(value[\u0027events\u0027] or [])\n\texcept:\n\t\trows \u003d []\n\tfor r in rows[:25]:\n\t\ttry:\n\t\t\tout.append({\u0027time_label\u0027: r[\u0027time_label\u0027], \u0027state_label\u0027: r[\u0027state_label\u0027], \u0027ack_user\u0027: r[\u0027ack_user\u0027] or \u0027\u0027})\n\t\texcept:\n\t\t\tpass\n\treturn out", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "cells": { + "style": { + "padding": "5px" + } + }, + "columns": [ + { + "field": "time_label", + "header": { + "title": "Time" + }, + "render": "string", + "sortable": false, + "strictWidth": true, + "width": 160 + }, + { + "field": "state_label", + "header": { + "title": "State" + }, + "render": "string", + "sortable": false, + "strictWidth": true, + "width": 110 + }, + { + "field": "ack_user", + "header": { + "title": "Ack by" + }, + "render": "string", + "sortable": false + } + ], + "emptyMessage": { + "noData": { + "text": "No events in range" + } + }, + "pager": { + "bottom": false + } + }, + "type": "ia.display.table" + } + ], + "meta": { + "name": "cardEvents" + }, + "position": { + "grow": 1 + }, + "props": { + "direction": "column", + "style": { + "classes": "PrimeControls/Card", + "gap": "6px" + } + }, + "type": "ia.container.flex" + } + ], + "meta": { + "name": "bodyCol" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.custom.detail" + }, + "transforms": [ + { + "code": "\tif value is None:\n\t\treturn False\n\ttry:\n\t\treturn not value[\u0027error\u0027]\n\texcept:\n\t\treturn True", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "direction": "column", + "style": { + "gap": "10px" + } + }, + "type": "ia.container.flex" + }, + { + "meta": { + "name": "emptyWrap" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.custom.detail" + }, + "transforms": [ + { + "code": "\tif value is None:\n\t\treturn True\n\ttry:\n\t\treturn bool(value[\u0027error\u0027])\n\texcept:\n\t\treturn False", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "params": { + "icon": "notifications_off", + "message": "No journal data for this alarm in the selected range" + }, + "path": "PrimeControls/Components/EmptyState" + }, + "type": "ia.display.view" + }, + { + "children": [ + { + "events": { + "component": { + "onActionPerformed": { + "config": { + "script": "\tsystem.perspective.closePopup(\u0027PC_AlarmDetail\u0027)" + }, + "scope": "G", + "type": "script" + } + } + }, + "meta": { + "name": "btnClose" + }, + "position": { + "basis": "90px", + "shrink": 0 + }, + "props": { + "text": "Close" + }, + "type": "ia.input.button" + } + ], + "meta": { + "name": "footerRow" + }, + "position": { + "shrink": 0 + }, + "props": { + "alignItems": "center", + "justify": "flex-end" + }, + "type": "ia.container.flex" + } + ], + "meta": { + "name": "root" + }, + "props": { + "direction": "column", + "style": { + "classes": "PrimeControls/PageBg", + "gap": "10px", + "padding": "16px" + } + }, + "type": "ia.container.flex" + } +} \ No newline at end of file diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Popups/EventDetail/resource.json b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Popups/EventDetail/resource.json new file mode 100644 index 0000000..276dced --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Popups/EventDetail/resource.json @@ -0,0 +1,10 @@ +{ + "scope": "G", + "version": 1, + "restricted": false, + "overridable": true, + "files": [ + "view.json" + ], + "attributes": {} +} diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Popups/EventDetail/view.json b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Popups/EventDetail/view.json new file mode 100644 index 0000000..7fd848c --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Popups/EventDetail/view.json @@ -0,0 +1,665 @@ +{ + "custom": { + "props": [] + }, + "params": { + "event": null + }, + "propConfig": { + "custom.props": { + "binding": { + "config": { + "path": "view.params.event" + }, + "transforms": [ + { + "code": "\tev = value\n\tif not ev:\n\t\treturn []\n\ttry:\n\t\tres = PrimeControls.alarms.getEventData(ev['id'], ev['time_ms'], ev['source'])\n\t\treturn res['props'] or []\n\texcept:\n\t\treturn []", + "type": "script" + } + ], + "type": "property" + } + }, + "params.event": { + "paramDirection": "input", + "persistent": false + } + }, + "props": { + "defaultSize": { + "height": 580, + "width": 580 + } + }, + "root": { + "children": [ + { + "children": [ + { + "meta": { + "name": "lblTitle" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.params.event" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn value['label'] or value['source'] or 'Event Detail'\n\texcept:\n\t\treturn 'Event Detail'", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Big" + } + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "chipState" + }, + "position": { + "shrink": 0 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.params.event" + }, + "transforms": [ + { + "code": "\treturn value is not None", + "type": "script" + } + ], + "type": "property" + } + }, + "props.style.classes": { + "binding": { + "config": { + "path": "view.params.event" + }, + "transforms": [ + { + "code": "\ttry:\n\t\ts = value['state']\n\texcept:\n\t\ts = None\n\tif s == 'ack':\n\t\treturn 'PrimeControls/State/Acked'\n\tif s == 'clear':\n\t\treturn 'PrimeControls/State/Cleared'\n\treturn 'PrimeControls/State/Active'", + "type": "script" + } + ], + "type": "property" + } + }, + "props.text": { + "binding": { + "config": { + "path": "view.params.event" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn value['state_label'] or ''\n\texcept:\n\t\treturn ''", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "borderRadius": "10px", + "fontSize": "11px", + "paddingBottom": "2px", + "paddingLeft": "8px", + "paddingRight": "8px", + "paddingTop": "2px" + } + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "chipPriority" + }, + "position": { + "shrink": 0 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.params.event" + }, + "transforms": [ + { + "code": "\treturn value is not None", + "type": "script" + } + ], + "type": "property" + } + }, + "props.style.classes": { + "binding": { + "config": { + "path": "view.params.event" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tp = value['priority_name']\n\texcept:\n\t\tp = None\n\tif p not in ['Diagnostic', 'Low', 'Medium', 'High', 'Critical']:\n\t\tp = 'Diagnostic'\n\treturn 'PrimeControls/Priority/' + p", + "type": "script" + } + ], + "type": "property" + } + }, + "props.text": { + "binding": { + "config": { + "path": "view.params.event" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn value['priority_name'] or ''\n\texcept:\n\t\treturn ''", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "borderRadius": "10px", + "fontSize": "11px", + "paddingBottom": "2px", + "paddingLeft": "8px", + "paddingRight": "8px", + "paddingTop": "2px" + } + }, + "type": "ia.display.label" + } + ], + "meta": { + "name": "headerRow" + }, + "position": { + "shrink": 0 + }, + "props": { + "alignItems": "center", + "style": { + "gap": "8px" + } + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "children": [ + { + "children": [ + { + "meta": { + "name": "k" + }, + "position": { + "basis": "90px", + "shrink": 0 + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted" + }, + "text": "Time" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "v" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.params.event" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn value['time_label'] or PrimeControls.fmt.day_clock(value['time_ms'])\n\texcept:\n\t\treturn '--'", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": {}, + "type": "ia.display.label" + } + ], + "meta": { + "name": "rowTime" + }, + "props": { + "style": { + "gap": "8px" + } + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "meta": { + "name": "k" + }, + "position": { + "basis": "90px", + "shrink": 0 + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted" + }, + "text": "Source" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "v" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.params.event" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn value['source'] or '--'\n\texcept:\n\t\treturn '--'", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "overflowWrap": "anywhere" + } + }, + "type": "ia.display.label" + } + ], + "meta": { + "name": "rowSource" + }, + "props": { + "style": { + "gap": "8px" + } + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "meta": { + "name": "k" + }, + "position": { + "basis": "90px", + "shrink": 0 + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted" + }, + "text": "Area" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "v" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.params.event" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn value['area'] or '--'\n\texcept:\n\t\treturn '--'", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": {}, + "type": "ia.display.label" + } + ], + "meta": { + "name": "rowArea" + }, + "props": { + "style": { + "gap": "8px" + } + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "meta": { + "name": "k" + }, + "position": { + "basis": "90px", + "shrink": 0 + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted" + }, + "text": "Ack by" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "v" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.params.event" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn value['ack_user'] or '--'\n\texcept:\n\t\treturn '--'", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": {}, + "type": "ia.display.label" + } + ], + "meta": { + "name": "rowAck" + }, + "props": { + "style": { + "gap": "8px" + } + }, + "type": "ia.container.flex" + } + ], + "meta": { + "name": "cardCore" + }, + "position": { + "shrink": 0 + }, + "props": { + "direction": "column", + "style": { + "classes": "PrimeControls/Card", + "gap": "4px" + } + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "meta": { + "name": "lblPropsTitle" + }, + "position": { + "shrink": 0 + }, + "props": { + "style": { + "classes": "PrimeControls/CardTitle" + }, + "text": "Event properties" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "tblProps" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "props.data": { + "binding": { + "config": { + "path": "view.custom.props" + }, + "transforms": [ + { + "code": "\tout = []\n\tfor p in (value or []):\n\t\ttry:\n\t\t\tv = p['value']\n\t\t\tout.append({'name': '%s' % p['name'], 'value': '' if v is None else '%s' % v})\n\t\texcept:\n\t\t\tpass\n\treturn out", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "columns": [ + { + "field": "name", + "header": { + "title": "Property" + }, + "render": "string", + "sortable": false, + "strictWidth": true, + "width": 180 + }, + { + "field": "value", + "header": { + "title": "Value" + }, + "render": "string", + "sortable": false + } + ], + "emptyMessage": { + "noData": { + "text": "No extended properties for this event" + } + }, + "pager": { + "bottom": false, + "top": false + }, + "cells": { + "style": { + "padding": "5px" + } + } + }, + "type": "ia.display.table" + } + ], + "meta": { + "name": "cardProps" + }, + "position": { + "grow": 1 + }, + "props": { + "direction": "column", + "style": { + "classes": "PrimeControls/Card", + "gap": "6px" + } + }, + "type": "ia.container.flex" + } + ], + "meta": { + "name": "bodyCol" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.params.event" + }, + "transforms": [ + { + "code": "\treturn value is not None", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "direction": "column", + "style": { + "gap": "10px" + } + }, + "type": "ia.container.flex" + }, + { + "meta": { + "name": "emptyWrap" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.params.event" + }, + "transforms": [ + { + "code": "\treturn value is None", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "params": { + "icon": "info", + "message": "No event selected" + }, + "path": "PrimeControls/Components/EmptyState" + }, + "type": "ia.display.view" + }, + { + "children": [ + { + "events": { + "component": { + "onActionPerformed": { + "config": { + "script": "\tsystem.perspective.closePopup('PC_EventDetail')" + }, + "scope": "G", + "type": "script" + } + } + }, + "meta": { + "name": "btnClose" + }, + "position": { + "basis": "90px", + "shrink": 0 + }, + "props": { + "primary": true, + "text": "Close" + }, + "type": "ia.input.button" + } + ], + "meta": { + "name": "footerRow" + }, + "position": { + "shrink": 0 + }, + "props": { + "alignItems": "center", + "justify": "flex-end" + }, + "type": "ia.container.flex" + } + ], + "custom": {}, + "meta": { + "name": "root" + }, + "props": { + "direction": "column", + "style": { + "classes": "PrimeControls/PageBg", + "gap": "10px", + "padding": "16px" + } + }, + "type": "ia.container.flex" + } +} diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Popups/HealthScore/resource.json b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Popups/HealthScore/resource.json new file mode 100644 index 0000000..276dced --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Popups/HealthScore/resource.json @@ -0,0 +1,10 @@ +{ + "scope": "G", + "version": 1, + "restricted": false, + "overridable": true, + "files": [ + "view.json" + ], + "attributes": {} +} diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Popups/HealthScore/view.json b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Popups/HealthScore/view.json new file mode 100644 index 0000000..9f16e99 --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Popups/HealthScore/view.json @@ -0,0 +1,318 @@ +{ + "custom": {}, + "params": { + "health": null + }, + "propConfig": { + "params.health": { + "paramDirection": "input", + "persistent": false + } + }, + "props": { + "defaultSize": { + "height": 540, + "width": 540 + } + }, + "root": { + "children": [ + { + "children": [ + { + "meta": { + "name": "gradeChip" + }, + "position": { + "shrink": 0 + }, + "propConfig": { + "props.style.classes": { + "binding": { + "config": { + "path": "view.params.health" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tg = value['grade']\n\texcept:\n\t\tg = None\n\treturn 'PrimeControls/Grade/' + (g if g in ['A', 'B', 'C', 'D', 'F'] else 'NA')", + "type": "script" + } + ], + "type": "property" + } + }, + "props.text": { + "binding": { + "config": { + "path": "view.params.health" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn value['grade'] or 'N/A'\n\texcept:\n\t\treturn 'N/A'", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "fontSize": "38px", + "paddingBottom": "4px", + "paddingLeft": "18px", + "paddingRight": "18px", + "paddingTop": "4px", + "textAlign": "center" + } + }, + "type": "ia.display.label" + }, + { + "children": [ + { + "meta": { + "name": "lblTitle" + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Big" + }, + "text": "Alarm Health Score" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "lblScore" + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.params.health" + }, + "transforms": [ + { + "code": "\ttry:\n\t\ts = value['score']\n\texcept:\n\t\ts = None\n\tif s is None:\n\t\treturn 'No data in selected range'\n\treturn 'Overall score %s / 100' % PrimeControls.fmt.num(s, 1)", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted" + } + }, + "type": "ia.display.label" + } + ], + "meta": { + "name": "titleCol" + }, + "position": { + "grow": 1 + }, + "props": { + "direction": "column", + "justify": "center" + }, + "type": "ia.container.flex" + } + ], + "meta": { + "name": "headerRow" + }, + "position": { + "shrink": 0 + }, + "props": { + "alignItems": "center", + "style": { + "gap": "14px" + } + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "meta": { + "name": "lblSubsTitle" + }, + "position": { + "shrink": 0 + }, + "props": { + "style": { + "classes": "PrimeControls/CardTitle" + }, + "text": "Component scores" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "repSubs" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "props.instances": { + "binding": { + "config": { + "path": "view.params.health" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tsubs = value['subs'] or []\n\texcept:\n\t\tsubs = []\n\tout = []\n\tfor s in subs:\n\t\ttry:\n\t\t\tout.append({'key': s['key'], 'label': s['label'], 'score': s['score'], 'weight': s['weight'], 'detail': s['detail']})\n\t\texcept:\n\t\t\tpass\n\treturn out", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "direction": "column", + "elementPosition": { + "basis": "64px", + "grow": 0, + "shrink": 0 + }, + "path": "PrimeControls/Components/ScoreBar", + "style": { + "overflowY": "auto" + }, + "useDefaultViewWidth": false, + "useDefaultViewHeight": false + }, + "type": "ia.display.flex-repeater" + }, + { + "meta": { + "name": "emptyWrap" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.params.health" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn not (value and value['subs'])\n\texcept:\n\t\treturn True", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "params": { + "icon": "assessment", + "message": "No health data in the selected range" + }, + "path": "PrimeControls/Components/EmptyState" + }, + "type": "ia.display.view" + } + ], + "meta": { + "name": "cardSubs" + }, + "position": { + "grow": 1 + }, + "props": { + "direction": "column", + "style": { + "classes": "PrimeControls/Card", + "gap": "6px" + } + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "meta": { + "name": "lblCaption" + }, + "position": { + "grow": 1, + "shrink": 1 + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted" + }, + "text": "Weighted ISA-18.2 composite: rate, flood, chattering, standing, priority mix" + }, + "type": "ia.display.label" + }, + { + "events": { + "component": { + "onActionPerformed": { + "config": { + "script": "\tsystem.perspective.closePopup('PC_HealthScore')" + }, + "scope": "G", + "type": "script" + } + } + }, + "meta": { + "name": "btnClose" + }, + "position": { + "basis": "90px", + "shrink": 0 + }, + "props": { + "primary": true, + "text": "Close" + }, + "type": "ia.input.button" + } + ], + "meta": { + "name": "footerRow" + }, + "position": { + "shrink": 0 + }, + "props": { + "alignItems": "center", + "justify": "space-between", + "style": { + "gap": "12px" + } + }, + "type": "ia.container.flex" + } + ], + "custom": {}, + "meta": { + "name": "root" + }, + "props": { + "direction": "column", + "style": { + "classes": "PrimeControls/PageBg", + "gap": "12px", + "padding": "16px" + } + }, + "type": "ia.container.flex" + } +} diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Popups/ShiftReport/resource.json b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Popups/ShiftReport/resource.json new file mode 100644 index 0000000..276dced --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Popups/ShiftReport/resource.json @@ -0,0 +1,10 @@ +{ + "scope": "G", + "version": 1, + "restricted": false, + "overridable": true, + "files": [ + "view.json" + ], + "attributes": {} +} diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Popups/ShiftReport/view.json b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Popups/ShiftReport/view.json new file mode 100644 index 0000000..8592942 --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Popups/ShiftReport/view.json @@ -0,0 +1,204 @@ +{ + "custom": { + "report": null + }, + "params": {}, + "propConfig": { + "custom.report": { + "binding": { + "config": { + "expression": "{session.custom.PrimeControls.query.refreshToken}" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tr = PrimeControls.alarms.getShiftReportText()\n\t\tif r and r['text']:\n\t\t\treturn r\n\texcept:\n\t\tpass\n\treturn {'text': 'Report unavailable', 'start_ms': 0, 'end_ms': 0, 'label': ''}", + "type": "script" + } + ], + "type": "expr" + } + } + }, + "props": { + "defaultSize": { + "height": 560, + "width": 620 + } + }, + "root": { + "children": [ + { + "children": [ + { + "meta": { + "name": "lblTitle" + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.custom.report" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn value['label'] or 'Shift Report'\n\texcept:\n\t\treturn 'Shift Report'", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Big" + } + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "lblWindow" + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.custom.report" + }, + "transforms": [ + { + "code": "\ttry:\n\t\ts = value['start_ms']\n\t\te = value['end_ms']\n\texcept:\n\t\treturn ''\n\tif not s or not e:\n\t\treturn ''\n\treturn '%s - %s' % (PrimeControls.fmt.day_clock(s), PrimeControls.fmt.day_clock(e))", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted" + } + }, + "type": "ia.display.label" + } + ], + "meta": { + "name": "headerCol" + }, + "position": { + "shrink": 0 + }, + "props": { + "direction": "column" + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "meta": { + "name": "lblText" + }, + "position": { + "shrink": 0 + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.custom.report" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn value['text'] or 'Report unavailable'\n\texcept:\n\t\treturn 'Report unavailable'", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "fontFamily": "monospace", + "fontSize": "12px", + "lineHeight": "1.5", + "whiteSpace": "pre-wrap" + } + }, + "type": "ia.display.label" + } + ], + "meta": { + "name": "cardText" + }, + "position": { + "grow": 1 + }, + "props": { + "direction": "column", + "style": { + "classes": "PrimeControls/Card", + "overflowY": "auto" + } + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "events": { + "component": { + "onActionPerformed": { + "config": { + "script": "\tsystem.perspective.closePopup('PC_ShiftReport')" + }, + "scope": "G", + "type": "script" + } + } + }, + "meta": { + "name": "btnClose" + }, + "position": { + "basis": "90px", + "shrink": 0 + }, + "props": { + "primary": true, + "text": "Close" + }, + "type": "ia.input.button" + } + ], + "meta": { + "name": "footerRow" + }, + "position": { + "shrink": 0 + }, + "props": { + "alignItems": "center", + "justify": "flex-end" + }, + "type": "ia.container.flex" + } + ], + "custom": {}, + "meta": { + "name": "root" + }, + "props": { + "direction": "column", + "style": { + "classes": "PrimeControls/PageBg", + "gap": "12px", + "padding": "16px" + } + }, + "type": "ia.container.flex" + } +} diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Shell/FilterBar/resource.json b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Shell/FilterBar/resource.json new file mode 100644 index 0000000..276dced --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Shell/FilterBar/resource.json @@ -0,0 +1,10 @@ +{ + "scope": "G", + "version": 1, + "restricted": false, + "overridable": true, + "files": [ + "view.json" + ], + "attributes": {} +} diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Shell/FilterBar/view.json b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Shell/FilterBar/view.json new file mode 100644 index 0000000..18da2e5 --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Shell/FilterBar/view.json @@ -0,0 +1,281 @@ +{ + "custom": {}, + "params": { + "bundle": null + }, + "propConfig": { + "params.bundle": { + "paramDirection": "input", + "persistent": false + } + }, + "props": { + "defaultSize": { + "height": 56, + "width": 1600 + } + }, + "root": { + "children": [ + { + "meta": { + "name": "ddPriority" + }, + "position": { + "basis": "170px", + "shrink": 0 + }, + "propConfig": { + "props.options": { + "binding": { + "config": { + "path": "view.params.bundle.meta.priorities_seen" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn [{'value': p['value'], 'label': p['label']} for p in (value or [])]\n\texcept:\n\t\treturn []", + "type": "script" + } + ], + "type": "property" + } + }, + "props.value": { + "binding": { + "bidirectional": true, + "config": { + "path": "session.custom.PrimeControls.query.priorities" + }, + "type": "property" + } + } + }, + "props": { + "multiSelect": true, + "placeholder": { + "text": "Priority" + }, + "wrapMultiSelectValues": true, + "style": { + "maxWidth": "280px" + } + }, + "type": "ia.input.dropdown" + }, + { + "meta": { + "name": "ddArea" + }, + "position": { + "basis": "170px", + "shrink": 0 + }, + "propConfig": { + "props.options": { + "binding": { + "config": { + "path": "view.params.bundle.meta.areas" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn [{'value': a, 'label': a} for a in (value or [])]\n\texcept:\n\t\treturn []", + "type": "script" + } + ], + "type": "property" + } + }, + "props.value": { + "binding": { + "bidirectional": true, + "config": { + "path": "session.custom.PrimeControls.query.areas" + }, + "type": "property" + } + } + }, + "props": { + "multiSelect": true, + "placeholder": { + "text": "Area" + }, + "wrapMultiSelectValues": true, + "style": { + "maxWidth": "280px" + } + }, + "type": "ia.input.dropdown" + }, + { + "meta": { + "name": "ddState" + }, + "position": { + "basis": "170px", + "shrink": 0 + }, + "propConfig": { + "props.value": { + "binding": { + "bidirectional": true, + "config": { + "path": "session.custom.PrimeControls.query.states" + }, + "type": "property" + } + } + }, + "props": { + "multiSelect": true, + "options": [ + { + "label": "Active", + "value": "active" + }, + { + "label": "Cleared", + "value": "cleared" + }, + { + "label": "Acked", + "value": "acked" + }, + { + "label": "Unacked", + "value": "unacked" + } + ], + "placeholder": { + "text": "State" + }, + "wrapMultiSelectValues": true, + "style": { + "maxWidth": "280px" + } + }, + "type": "ia.input.dropdown" + }, + { + "meta": { + "name": "txtSearch" + }, + "position": { + "basis": "240px", + "shrink": 1 + }, + "propConfig": { + "props.text": { + "binding": { + "bidirectional": true, + "config": { + "path": "session.custom.PrimeControls.query.search" + }, + "type": "property" + } + } + }, + "props": { + "deferUpdates": true, + "placeholder": "Search source/label..." + }, + "type": "ia.input.text-field" + }, + { + "events": { + "component": { + "onActionPerformed": { + "config": { + "script": "\tq = self.session.custom.PrimeControls.query\n\ttry:\n\t\tq.priorities = list(self.getSibling('ddPriority').props.value or [])\n\t\tq.areas = list(self.getSibling('ddArea').props.value or [])\n\t\tq.states = list(self.getSibling('ddState').props.value or [])\n\t\tq.search = str(self.getSibling('txtSearch').props.text or '')\n\texcept:\n\t\tpass\n\tq.refreshToken = q.refreshToken + 1" + }, + "scope": "G", + "type": "script" + } + } + }, + "meta": { + "name": "btnApply" + }, + "position": { + "basis": "80px", + "shrink": 0 + }, + "props": { + "primary": true, + "text": "Apply" + }, + "type": "ia.input.button" + }, + { + "events": { + "component": { + "onActionPerformed": { + "config": { + "script": "\tq = self.session.custom.PrimeControls.query\n\tq.priorities = []\n\tq.areas = []\n\tq.states = []\n\tq.search = ''\n\ttry:\n\t\tself.getSibling('ddPriority').props.value = []\n\t\tself.getSibling('ddArea').props.value = []\n\t\tself.getSibling('ddState').props.value = []\n\t\tself.getSibling('txtSearch').props.text = ''\n\texcept:\n\t\tpass\n\tq.refreshToken = q.refreshToken + 1" + }, + "scope": "G", + "type": "script" + } + } + }, + "meta": { + "name": "btnClear" + }, + "position": { + "basis": "80px", + "shrink": 0 + }, + "props": { + "primary": false, + "text": "Clear" + }, + "type": "ia.input.button" + }, + { + "meta": { + "name": "lblEventCount" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tn = value['meta']['event_count']\n\texcept:\n\t\tn = None\n\tif n is None:\n\t\treturn ''\n\treturn '%s events in range' % PrimeControls.fmt.num(n)", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted" + }, + "textStyle": { + "textAlign": "right" + } + }, + "type": "ia.display.label" + } + ], + "meta": { + "name": "root" + }, + "props": { + "alignItems": "center", + "direction": "row", + "style": { + "classes": "PrimeControls/Toolbar", + "gap": "8px" + } + }, + "type": "ia.container.flex" + } +} diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Shell/Header/resource.json b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Shell/Header/resource.json new file mode 100644 index 0000000..8fcbc6e --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Shell/Header/resource.json @@ -0,0 +1,16 @@ +{ + "scope": "G", + "version": 1, + "restricted": false, + "overridable": true, + "files": [ + "view.json" + ], + "attributes": { + "lastModificationSignature": "ad9c36584854ed67b341f681e7992b6264caf658ed334102fa088a1cc84012fd", + "lastModification": { + "actor": "admin", + "timestamp": "2026-07-16T20:47:54Z" + } + } +} diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Shell/Header/view.json b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Shell/Header/view.json new file mode 100644 index 0000000..d4f7347 --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Shell/Header/view.json @@ -0,0 +1,697 @@ +{ + "custom": {}, + "params": {}, + "propConfig": { + "params.bundle": { + "paramDirection": "input", + "persistent": false + } + }, + "props": { + "defaultSize": { + "height": 165, + "width": 1600 + } + }, + "root": { + "children": [ + { + "children": [ + { + "children": [ + { + "meta": { + "name": "lblTitle" + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Big" + }, + "text": "Alarm Analysis" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "lblSubtitle" + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted" + }, + "text": "Ignition 8.3 Build-a-Thon" + }, + "type": "ia.display.label" + } + ], + "meta": { + "name": "titles" + }, + "position": { + "shrink": 0 + }, + "props": { + "direction": "column", + "justify": "center" + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "meta": { + "name": "chipTruncated" + }, + "position": { + "shrink": 0 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn bool(value['meta']['truncated'])\n\texcept:\n\t\treturn False", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted", + "color": "#E8883A" + }, + "text": "data truncated \u2014 narrow the range" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "chipError" + }, + "position": { + "shrink": 0 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn bool(value['meta']['error'])\n\texcept:\n\t\treturn False", + "type": "script" + } + ], + "type": "property" + } + }, + "props.text": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn value['meta']['error'] or ''\n\texcept:\n\t\treturn ''", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted", + "color": "#D64550" + } + }, + "type": "ia.display.label" + } + ], + "meta": { + "name": "chips" + }, + "position": { + "grow": 1 + }, + "props": { + "alignItems": "center", + "justify": "flex-end", + "style": { + "gap": "8px", + "paddingRight": "12px" + } + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "events": { + "component": { + "onActionPerformed": { + "config": { + "script": "\tspans = {'4h': 4, '8h': 8, '24h': 24, '7d': 168}\n\tpreset = event.value\n\tif preset in spans:\n\t\tq = self.session.custom.PrimeControls.query\n\t\tnow = system.date.toMillis(system.date.now())\n\t\tq.endMs = now\n\t\tq.startMs = now - spans[preset] * 3600 * 1000\n\t\tq.refreshToken = q.refreshToken + 1" + }, + "scope": "G", + "type": "script" + } + } + }, + "meta": { + "name": "ddRange" + }, + "position": { + "basis": "150px", + "shrink": 0 + }, + "propConfig": { + "props.value": { + "binding": { + "config": { + "path": "session.custom.PrimeControls.query.rangePreset" + }, + "type": "property" + } + } + }, + "props": { + "options": [ + { + "label": "Last 4 hours", + "value": "4h" + }, + { + "label": "Last 8 hours", + "value": "8h" + }, + { + "label": "Last 24 hours", + "value": "24h" + }, + { + "label": "Last 7 days", + "value": "7d" + } + ], + "placeholder": { + "text": "Range" + } + }, + "type": "ia.input.dropdown" + }, + { + "events": { + "component": { + "onActionPerformed": { + "config": { + "script": "\tq = self.session.custom.PrimeControls.query\n\tspans = {'4h': 4, '8h': 8, '24h': 24, '7d': 168}\n\tnow = system.date.toMillis(system.date.now())\n\thours = spans.get(q.rangePreset)\n\tif hours is not None:\n\t\tspan = hours * 3600 * 1000\n\telse:\n\t\tspan = q.endMs - q.startMs\n\tif span > 0:\n\t\tq.endMs = now\n\t\tq.startMs = now - span\n\tq.refreshToken = q.refreshToken + 1" + }, + "scope": "G", + "type": "script" + } + } + }, + "meta": { + "name": "btnRefresh" + }, + "position": { + "basis": "90px", + "shrink": 0 + }, + "props": { + "image": { + "height": 15 + }, + "primary": false, + "text": "Refresh" + }, + "type": "ia.input.button" + }, + { + "events": { + "component": { + "onActionPerformed": { + "config": { + "script": "\tsystem.perspective.openPopup('PC_ShiftReport', 'PrimeControls/Popups/ShiftReport', params = {}, showCloseIcon = True, draggable = True)" + }, + "scope": "G", + "type": "script" + } + } + }, + "meta": { + "name": "btnShiftReport" + }, + "position": { + "basis": "120px", + "shrink": 0 + }, + "props": { + "image": { + "height": 15 + }, + "text": "Shift Report" + }, + "type": "ia.input.button" + } + ], + "meta": { + "name": "controls" + }, + "position": { + "shrink": 0 + }, + "props": { + "alignItems": "center", + "style": { + "gap": "8px" + } + }, + "type": "ia.container.flex" + } + ], + "meta": { + "name": "rowTop" + }, + "position": { + "basis": "55px", + "shrink": 0 + }, + "props": { + "alignItems": "center", + "justify": "space-between", + "style": { + "gap": "12px" + } + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "meta": { + "name": "gauge" + }, + "position": { + "basis": "230px", + "shrink": 0 + }, + "propConfig": { + "props.params.health": { + "binding": { + "config": { + "path": "view.params.bundle.health" + }, + "type": "property" + } + } + }, + "props": { + "path": "PrimeControls/Components/HealthGauge", + "style": { + "overflow": "hidden" + } + }, + "type": "ia.display.view" + }, + { + "meta": { + "name": "kpiActivations" + }, + "position": { + "basis": "100px", + "grow": 1 + }, + "propConfig": { + "props.params.delta": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn value['kpis']['activations']\n\texcept:\n\t\treturn None", + "type": "script" + } + ], + "type": "property" + } + }, + "props.params.value": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn PrimeControls.fmt.num(value['kpis']['activations']['value'])\n\texcept:\n\t\treturn '--'", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "params": { + "label": "Activations" + }, + "path": "PrimeControls/Components/KpiCard", + "style": { + "overflow": "hidden" + } + }, + "type": "ia.display.view" + }, + { + "meta": { + "name": "kpiRate" + }, + "position": { + "basis": "100px", + "grow": 1 + }, + "propConfig": { + "props.params.delta": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn value['kpis']['rate_per_hr']\n\texcept:\n\t\treturn None", + "type": "script" + } + ], + "type": "property" + } + }, + "props.params.value": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn PrimeControls.fmt.num(value['kpis']['rate_per_hr']['value'], 1)\n\texcept:\n\t\treturn '--'", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "params": { + "label": "Rate", + "unit": "/hr" + }, + "path": "PrimeControls/Components/KpiCard", + "style": { + "overflow": "hidden" + } + }, + "type": "ia.display.view" + }, + { + "meta": { + "name": "kpiActiveNow" + }, + "position": { + "basis": "100px", + "grow": 1 + }, + "propConfig": { + "props.params.delta": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn value['kpis']['active_now']\n\texcept:\n\t\treturn None", + "type": "script" + } + ], + "type": "property" + } + }, + "props.params.value": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn PrimeControls.fmt.num(value['kpis']['active_now']['value'])\n\texcept:\n\t\treturn '--'", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "params": { + "label": "Active Now" + }, + "path": "PrimeControls/Components/KpiCard", + "style": { + "overflow": "hidden" + } + }, + "type": "ia.display.view" + }, + { + "meta": { + "name": "kpiUnacked" + }, + "position": { + "basis": "100px", + "grow": 1 + }, + "propConfig": { + "props.params.delta": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn value['kpis']['unacked_now']\n\texcept:\n\t\treturn None", + "type": "script" + } + ], + "type": "property" + } + }, + "props.params.value": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn PrimeControls.fmt.num(value['kpis']['unacked_now']['value'])\n\texcept:\n\t\treturn '--'", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "params": { + "label": "Unacked" + }, + "path": "PrimeControls/Components/KpiCard", + "style": { + "overflow": "hidden" + } + }, + "type": "ia.display.view" + }, + { + "meta": { + "name": "kpiMtta" + }, + "position": { + "basis": "100px", + "grow": 1 + }, + "propConfig": { + "props.params.delta": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn value['kpis']['mtta_ms']\n\texcept:\n\t\treturn None", + "type": "script" + } + ], + "type": "property" + } + }, + "props.params.value": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn PrimeControls.fmt.dur(value['kpis']['mtta_ms']['value'])\n\texcept:\n\t\treturn '--'", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "params": { + "label": "MTTA" + }, + "path": "PrimeControls/Components/KpiCard", + "style": { + "overflow": "hidden" + } + }, + "type": "ia.display.view" + }, + { + "meta": { + "name": "kpiFlood" + }, + "position": { + "basis": "100px", + "grow": 1 + }, + "propConfig": { + "props.params.delta": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn value['kpis']['flood_pct']\n\texcept:\n\t\treturn None", + "type": "script" + } + ], + "type": "property" + } + }, + "props.params.value": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn PrimeControls.fmt.pct(value['kpis']['flood_pct']['value'], 1)\n\texcept:\n\t\treturn '--'", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "params": { + "label": "Flood Time" + }, + "path": "PrimeControls/Components/KpiCard", + "style": { + "overflow": "hidden" + } + }, + "type": "ia.display.view" + }, + { + "meta": { + "name": "kpiChatter" + }, + "position": { + "basis": "100px", + "grow": 1 + }, + "propConfig": { + "props.params.delta": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn value['kpis']['chatter_count']\n\texcept:\n\t\treturn None", + "type": "script" + } + ], + "type": "property" + } + }, + "props.params.value": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn PrimeControls.fmt.num(value['kpis']['chatter_count']['value'])\n\texcept:\n\t\treturn '--'", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "params": { + "label": "Chattering" + }, + "path": "PrimeControls/Components/KpiCard", + "style": { + "overflow": "hidden" + } + }, + "type": "ia.display.view" + } + ], + "meta": { + "name": "rowKpis" + }, + "position": { + "grow": 1 + }, + "props": { + "style": { + "gap": "10px" + } + }, + "type": "ia.container.flex" + } + ], + "meta": { + "name": "root" + }, + "props": { + "direction": "column", + "style": { + "classes": "PrimeControls/Toolbar", + "gap": "8px" + } + }, + "type": "ia.container.flex" + } +} diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Analysis/resource.json b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Analysis/resource.json new file mode 100644 index 0000000..276dced --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Analysis/resource.json @@ -0,0 +1,10 @@ +{ + "scope": "G", + "version": 1, + "restricted": false, + "overridable": true, + "files": [ + "view.json" + ], + "attributes": {} +} diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Analysis/view.json b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Analysis/view.json new file mode 100644 index 0000000..3519e36 --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Analysis/view.json @@ -0,0 +1,2463 @@ +{ + "custom": {}, + "params": { + "bundle": null + }, + "propConfig": { + "params.bundle": { + "paramDirection": "input", + "persistent": false + } + }, + "props": { + "defaultSize": { + "height": 760, + "width": 1600 + } + }, + "root": { + "children": [ + { + "meta": { + "name": "emptyAll" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn (not value) or value['meta']['event_count'] == 0\n\texcept:\n\t\treturn True", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "params": { + "icon": "inbox", + "message": "No alarm events in the selected range" + }, + "path": "PrimeControls/Components/EmptyState" + }, + "type": "ia.display.view" + }, + { + "children": [ + { + "children": [ + { + "children": [ + { + "meta": { + "name": "hmTitle" + }, + "position": { + "shrink": 0 + }, + "props": { + "style": { + "classes": "PrimeControls/CardTitle" + }, + "text": "Activations by Hour & Day of Week" + }, + "type": "ia.display.label" + }, + { + "children": [ + { + "children": [ + { + "meta": { + "name": "lblMon" + }, + "position": { + "basis": "34px", + "shrink": 0 + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted", + "fontSize": "10px", + "paddingRight": "6px", + "textAlign": "right" + }, + "text": "Mon" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "repMon" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "props.instances": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\thm = value['heatmap']\n\t\tmx = int(hm['max_count'] or 0)\n\t\treturn [{'count': int(c or 0), 'max': mx} for c in hm['rows'][0]][:24]\n\texcept:\n\t\treturn []", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "elementPosition": { + "basis": "0px" + }, + "path": "PrimeControls/Components/HeatmapCell", + "style": { + "overflow": "hidden" + }, + "useDefaultViewHeight": false, + "useDefaultViewWidth": false + }, + "type": "ia.display.flex-repeater" + } + ], + "meta": { + "name": "rowMon" + }, + "position": { + "grow": 1 + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "meta": { + "name": "lblTue" + }, + "position": { + "basis": "34px", + "shrink": 0 + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted", + "fontSize": "10px", + "paddingRight": "6px", + "textAlign": "right" + }, + "text": "Tue" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "repTue" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "props.instances": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\thm = value['heatmap']\n\t\tmx = int(hm['max_count'] or 0)\n\t\treturn [{'count': int(c or 0), 'max': mx} for c in hm['rows'][1]][:24]\n\texcept:\n\t\treturn []", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "elementPosition": { + "basis": "0px" + }, + "path": "PrimeControls/Components/HeatmapCell", + "style": { + "overflow": "hidden" + }, + "useDefaultViewHeight": false, + "useDefaultViewWidth": false + }, + "type": "ia.display.flex-repeater" + } + ], + "meta": { + "name": "rowTue" + }, + "position": { + "grow": 1 + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "meta": { + "name": "lblWed" + }, + "position": { + "basis": "34px", + "shrink": 0 + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted", + "fontSize": "10px", + "paddingRight": "6px", + "textAlign": "right" + }, + "text": "Wed" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "repWed" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "props.instances": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\thm = value['heatmap']\n\t\tmx = int(hm['max_count'] or 0)\n\t\treturn [{'count': int(c or 0), 'max': mx} for c in hm['rows'][2]][:24]\n\texcept:\n\t\treturn []", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "elementPosition": { + "basis": "0px" + }, + "path": "PrimeControls/Components/HeatmapCell", + "style": { + "overflow": "hidden" + }, + "useDefaultViewHeight": false, + "useDefaultViewWidth": false + }, + "type": "ia.display.flex-repeater" + } + ], + "meta": { + "name": "rowWed" + }, + "position": { + "grow": 1 + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "meta": { + "name": "lblThu" + }, + "position": { + "basis": "34px", + "shrink": 0 + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted", + "fontSize": "10px", + "paddingRight": "6px", + "textAlign": "right" + }, + "text": "Thu" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "repThu" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "props.instances": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\thm = value['heatmap']\n\t\tmx = int(hm['max_count'] or 0)\n\t\treturn [{'count': int(c or 0), 'max': mx} for c in hm['rows'][3]][:24]\n\texcept:\n\t\treturn []", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "elementPosition": { + "basis": "0px" + }, + "path": "PrimeControls/Components/HeatmapCell", + "style": { + "overflow": "hidden" + }, + "useDefaultViewHeight": false, + "useDefaultViewWidth": false + }, + "type": "ia.display.flex-repeater" + } + ], + "meta": { + "name": "rowThu" + }, + "position": { + "grow": 1 + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "meta": { + "name": "lblFri" + }, + "position": { + "basis": "34px", + "shrink": 0 + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted", + "fontSize": "10px", + "paddingRight": "6px", + "textAlign": "right" + }, + "text": "Fri" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "repFri" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "props.instances": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\thm = value['heatmap']\n\t\tmx = int(hm['max_count'] or 0)\n\t\treturn [{'count': int(c or 0), 'max': mx} for c in hm['rows'][4]][:24]\n\texcept:\n\t\treturn []", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "elementPosition": { + "basis": "0px" + }, + "path": "PrimeControls/Components/HeatmapCell", + "style": { + "overflow": "hidden" + }, + "useDefaultViewHeight": false, + "useDefaultViewWidth": false + }, + "type": "ia.display.flex-repeater" + } + ], + "meta": { + "name": "rowFri" + }, + "position": { + "grow": 1 + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "meta": { + "name": "lblSat" + }, + "position": { + "basis": "34px", + "shrink": 0 + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted", + "fontSize": "10px", + "paddingRight": "6px", + "textAlign": "right" + }, + "text": "Sat" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "repSat" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "props.instances": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\thm = value['heatmap']\n\t\tmx = int(hm['max_count'] or 0)\n\t\treturn [{'count': int(c or 0), 'max': mx} for c in hm['rows'][5]][:24]\n\texcept:\n\t\treturn []", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "elementPosition": { + "basis": "0px" + }, + "path": "PrimeControls/Components/HeatmapCell", + "style": { + "overflow": "hidden" + }, + "useDefaultViewHeight": false, + "useDefaultViewWidth": false + }, + "type": "ia.display.flex-repeater" + } + ], + "meta": { + "name": "rowSat" + }, + "position": { + "grow": 1 + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "meta": { + "name": "lblSun" + }, + "position": { + "basis": "34px", + "shrink": 0 + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted", + "fontSize": "10px", + "paddingRight": "6px", + "textAlign": "right" + }, + "text": "Sun" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "repSun" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "props.instances": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\thm = value['heatmap']\n\t\tmx = int(hm['max_count'] or 0)\n\t\treturn [{'count': int(c or 0), 'max': mx} for c in hm['rows'][6]][:24]\n\texcept:\n\t\treturn []", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "elementPosition": { + "basis": "0px" + }, + "path": "PrimeControls/Components/HeatmapCell", + "style": { + "overflow": "hidden" + }, + "useDefaultViewHeight": false, + "useDefaultViewWidth": false + }, + "type": "ia.display.flex-repeater" + } + ], + "meta": { + "name": "rowSun" + }, + "position": { + "grow": 1 + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "meta": { + "name": "axSpacer" + }, + "position": { + "basis": "34px", + "shrink": 0 + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "ax00" + }, + "position": { + "basis": "0px", + "grow": 1 + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted", + "fontSize": "9px" + }, + "text": "0" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "ax03" + }, + "position": { + "basis": "0px", + "grow": 1 + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted", + "fontSize": "9px" + }, + "text": "3" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "ax06" + }, + "position": { + "basis": "0px", + "grow": 1 + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted", + "fontSize": "9px" + }, + "text": "6" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "ax09" + }, + "position": { + "basis": "0px", + "grow": 1 + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted", + "fontSize": "9px" + }, + "text": "9" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "ax12" + }, + "position": { + "basis": "0px", + "grow": 1 + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted", + "fontSize": "9px" + }, + "text": "12" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "ax15" + }, + "position": { + "basis": "0px", + "grow": 1 + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted", + "fontSize": "9px" + }, + "text": "15" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "ax18" + }, + "position": { + "basis": "0px", + "grow": 1 + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted", + "fontSize": "9px" + }, + "text": "18" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "ax21" + }, + "position": { + "basis": "0px", + "grow": 1 + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted", + "fontSize": "9px" + }, + "text": "21" + }, + "type": "ia.display.label" + } + ], + "meta": { + "name": "hourAxis" + }, + "position": { + "basis": "14px", + "shrink": 0 + }, + "type": "ia.container.flex" + } + ], + "meta": { + "name": "hmGrid" + }, + "position": { + "grow": 1 + }, + "props": { + "direction": "column" + }, + "type": "ia.container.flex" + } + ], + "meta": { + "name": "cardHeatmap" + }, + "position": { + "basis": "0px", + "grow": 3 + }, + "props": { + "direction": "column", + "style": { + "classes": "PrimeControls/Card", + "gap": "8px" + } + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "meta": { + "name": "pTitle" + }, + "position": { + "shrink": 0 + }, + "props": { + "style": { + "classes": "PrimeControls/CardTitle" + }, + "text": "Priority Mix vs ISA 80/15/5" + }, + "type": "ia.display.label" + }, + { + "children": [ + { + "children": [ + { + "meta": { + "name": "donut" + }, + "position": { + "basis": "95%" + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tp = value['priority']['pct']\n\t\treturn (p['low'] or 0) + (p['medium'] or 0) + (p['high'] or 0) > 0\n\texcept:\n\t\treturn False", + "type": "script" + } + ], + "type": "property" + } + }, + "props.data": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tp = value['priority']['pct']\n\t\treturn [{'label': 'Low', 'value': round(float(p['low'] or 0), 1)}, {'label': 'Medium', 'value': round(float(p['medium'] or 0), 1)}, {'label': 'High', 'value': round(float(p['high'] or 0), 1)}]\n\texcept:\n\t\treturn []", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "colors": [ + "#5B9BD5", + "#E5C453", + "#E8883A" + ], + "cutoutRadius": 60, + "showLabels": false, + "showLegend": false, + "tooltipFormat": "{category}: {value.value.formatNumber('#.0')}%" + }, + "type": "ia.chart.pie" + }, + { + "meta": { + "name": "donutEmpty" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tp = value['priority']['pct']\n\t\treturn (p['low'] or 0) + (p['medium'] or 0) + (p['high'] or 0) <= 0\n\texcept:\n\t\treturn True", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "params": { + "icon": "pie_chart", + "message": "No priority data in range" + }, + "path": "PrimeControls/Components/EmptyState" + }, + "type": "ia.display.view" + } + ], + "meta": { + "name": "pieWrap" + }, + "position": { + "basis": "0px", + "grow": 1 + }, + "props": { + "direction": "column" + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "children": [ + { + "meta": { + "name": "chipLow" + }, + "position": { + "basis": "72px", + "shrink": 0 + }, + "props": { + "style": { + "borderRadius": "10px", + "classes": "PrimeControls/Priority/Low", + "fontSize": "11px", + "padding": "2px 8px", + "textAlign": "center" + }, + "text": "Low" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "valLow" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tpr = value['priority']\n\t\treturn '%s now vs %s target' % (PrimeControls.fmt.pct(pr['pct']['low'], 1), PrimeControls.fmt.pct(pr['target']['low'], 0))\n\texcept:\n\t\treturn '--'", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted" + } + }, + "type": "ia.display.label" + } + ], + "meta": { + "name": "targetLow" + }, + "position": { + "shrink": 0 + }, + "props": { + "alignItems": "center", + "style": { + "gap": "8px" + } + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "meta": { + "name": "chipMedium" + }, + "position": { + "basis": "72px", + "shrink": 0 + }, + "props": { + "style": { + "borderRadius": "10px", + "classes": "PrimeControls/Priority/Medium", + "fontSize": "11px", + "padding": "2px 8px", + "textAlign": "center" + }, + "text": "Medium" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "valMedium" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tpr = value['priority']\n\t\treturn '%s now vs %s target' % (PrimeControls.fmt.pct(pr['pct']['medium'], 1), PrimeControls.fmt.pct(pr['target']['medium'], 0))\n\texcept:\n\t\treturn '--'", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted" + } + }, + "type": "ia.display.label" + } + ], + "meta": { + "name": "targetMedium" + }, + "position": { + "shrink": 0 + }, + "props": { + "alignItems": "center", + "style": { + "gap": "8px" + } + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "meta": { + "name": "chipHigh" + }, + "position": { + "basis": "72px", + "shrink": 0 + }, + "props": { + "style": { + "borderRadius": "10px", + "classes": "PrimeControls/Priority/High", + "fontSize": "11px", + "padding": "2px 8px", + "textAlign": "center" + }, + "text": "High" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "valHigh" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tpr = value['priority']\n\t\treturn '%s now vs %s target' % (PrimeControls.fmt.pct(pr['pct']['high'], 1), PrimeControls.fmt.pct(pr['target']['high'], 0))\n\texcept:\n\t\treturn '--'", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted" + } + }, + "type": "ia.display.label" + } + ], + "meta": { + "name": "targetHigh" + }, + "position": { + "shrink": 0 + }, + "props": { + "alignItems": "center", + "style": { + "gap": "8px" + } + }, + "type": "ia.container.flex" + } + ], + "meta": { + "name": "targets" + }, + "position": { + "basis": "0px", + "grow": 1 + }, + "props": { + "direction": "column", + "justify": "center", + "style": { + "gap": "10px" + } + }, + "type": "ia.container.flex" + } + ], + "meta": { + "name": "pBody" + }, + "position": { + "grow": 1 + }, + "props": { + "style": { + "gap": "8px" + } + }, + "type": "ia.container.flex" + }, + { + "meta": { + "name": "pCaption" + }, + "position": { + "shrink": 0 + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tsad = value['priority']['sum_abs_dev']\n\t\tif sad is None:\n\t\t\treturn ''\n\t\treturn 'Deviation from ISA 80/15/5 target: %s pts' % PrimeControls.fmt.num(sad, 1)\n\texcept:\n\t\treturn ''", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted" + } + }, + "type": "ia.display.label" + } + ], + "meta": { + "name": "cardPriority" + }, + "position": { + "basis": "0px", + "grow": 2 + }, + "props": { + "direction": "column", + "style": { + "classes": "PrimeControls/Card", + "gap": "8px" + } + }, + "type": "ia.container.flex" + } + ], + "meta": { + "name": "rowTop" + }, + "position": { + "basis": "300px", + "grow": 1, + "shrink": 0 + }, + "props": { + "style": { + "gap": "12px" + } + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "children": [ + { + "meta": { + "name": "mtTitle" + }, + "position": { + "shrink": 0 + }, + "props": { + "style": { + "classes": "PrimeControls/CardTitle" + }, + "text": "Response Times (MTTA / MTTR)" + }, + "type": "ia.display.label" + }, + { + "children": [ + { + "children": [ + { + "meta": { + "name": "lblMtta" + }, + "position": { + "shrink": 0 + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted" + }, + "text": "MTTA (mean time to acknowledge)" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "valMtta" + }, + "position": { + "shrink": 0 + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn PrimeControls.fmt.dur(value['mtta_mttr']['mtta']['mean_ms'])\n\texcept:\n\t\treturn '--'", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Kpi" + } + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "subMtta" + }, + "position": { + "shrink": 0 + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tm = value['mtta_mttr']['mtta']\n\t\treturn 'median %s over %s acks' % (PrimeControls.fmt.dur(m['median_ms']), PrimeControls.fmt.num(m['count']))\n\texcept:\n\t\treturn '--'", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted" + } + }, + "type": "ia.display.label" + } + ], + "meta": { + "name": "colMtta" + }, + "position": { + "basis": "0px", + "grow": 1 + }, + "props": { + "direction": "column" + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "meta": { + "name": "lblMttr" + }, + "position": { + "shrink": 0 + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted" + }, + "text": "MTTR (mean time to resolve)" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "valMttr" + }, + "position": { + "shrink": 0 + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn PrimeControls.fmt.dur(value['mtta_mttr']['mttr']['mean_ms'])\n\texcept:\n\t\treturn '--'", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Kpi" + } + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "subMttr" + }, + "position": { + "shrink": 0 + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tm = value['mtta_mttr']['mttr']\n\t\treturn 'median %s over %s clears' % (PrimeControls.fmt.dur(m['median_ms']), PrimeControls.fmt.num(m['count']))\n\texcept:\n\t\treturn '--'", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted" + } + }, + "type": "ia.display.label" + } + ], + "meta": { + "name": "colMttr" + }, + "position": { + "basis": "0px", + "grow": 1 + }, + "props": { + "direction": "column" + }, + "type": "ia.container.flex" + } + ], + "meta": { + "name": "mtHead" + }, + "position": { + "shrink": 0 + }, + "props": { + "style": { + "gap": "16px" + } + }, + "type": "ia.container.flex" + }, + { + "meta": { + "name": "trendChart" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tfor r in (value['mtta_mttr']['trend'] or []):\n\t\t\tif r['mtta_ms'] is not None or r['mttr_ms'] is not None:\n\t\t\t\treturn True\n\texcept:\n\t\tpass\n\treturn False", + "type": "script" + } + ], + "type": "property" + } + }, + "props.dataSources.trend": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\timport time\n\tout = []\n\ttry:\n\t\trows = value['mtta_mttr']['trend'] or []\n\texcept:\n\t\trows = []\n\tfor r in rows:\n\t\ttry:\n\t\t\tt0 = r['t0']\n\t\t\tif t0 is None:\n\t\t\t\tcontinue\n\t\t\tts = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(t0 / 1000.0))\n\t\t\ta = r['mtta_ms']\n\t\t\tc = r['mttr_ms']\n\t\t\tout.append({'t': ts, 'mtta': None if a is None else round(a / 60000.0, 2), 'mttr': None if c is None else round(c / 60000.0, 2)})\n\t\texcept:\n\t\t\tpass\n\treturn out", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "dataSources": {}, + "legend": { + "labels": { + "font": { + "size": 11 + } + }, + "markers": { + "height": 12, + "width": 12 + } + }, + "scrollBars": { + "horizontal": { + "enabled": false + }, + "vertical": { + "enabled": false + } + }, + "series": [ + { + "candlestick": { + "appearance": { + "deriveFieldsFromData": { + "fill": { + "color": "", + "opacity": "" + }, + "stroke": { + "color": "", + "opacity": "", + "width": "" + } + }, + "fill": { + "color": "", + "opacity": 1 + }, + "heatRules": { + "dataField": "", + "enabled": false, + "max": "", + "min": "" + }, + "stacked": false, + "stroke": { + "color": "", + "opacity": 1, + "width": 1 + } + }, + "high": { + "x": "", + "y": "" + }, + "low": { + "x": "", + "y": "" + }, + "open": { + "x": "", + "y": "" + } + }, + "column": { + "appearance": { + "deriveFieldsFromData": { + "fill": { + "color": "", + "opacity": "" + }, + "stroke": { + "color": "", + "opacity": "", + "width": "" + } + }, + "fill": { + "color": "", + "opacity": 1 + }, + "heatRules": { + "dataField": "", + "enabled": false, + "max": "", + "min": "" + }, + "height": null, + "stacked": false, + "stroke": { + "color": "", + "opacity": 1, + "width": 1 + }, + "width": null + }, + "open": { + "x": "", + "y": "" + } + }, + "data": { + "source": "trend", + "x": "t", + "y": "mtta" + }, + "defaultState": { + "visible": true + }, + "hiddenInLegend": false, + "label": { + "text": "MTTA" + }, + "line": { + "appearance": { + "bullets": [ + { + "deriveFieldsFromData": { + "fill": { + "color": "", + "opacity": "" + }, + "rotation": "", + "stroke": { + "color": "", + "opacity": "", + "width": "" + } + }, + "enabled": true, + "fill": { + "color": "#3B7DD8", + "opacity": 1 + }, + "heatRules": { + "dataField": "", + "enabled": false, + "max": 100, + "min": 2 + }, + "label": { + "hideOversized": true, + "position": { + "dx": 0, + "dy": 0 + }, + "text": "{value}", + "truncate": true + }, + "radius": 3, + "render": "circle", + "rotation": 0, + "stroke": { + "color": "", + "opacity": 1, + "width": 1 + }, + "tooltip": { + "background": { + "color": "", + "opacity": 1 + }, + "cornerRadius": 3, + "enabled": true, + "getFillFromObject": true, + "pointerLength": 4, + "text": "{name}: [bold]{valueY.formatNumber('#,###.#')} min[/]" + } + } + ], + "connect": false, + "fill": { + "color": "", + "opacity": 0 + }, + "minDistance": 0.5, + "stroke": { + "color": "#3B7DD8", + "dashArray": "", + "opacity": 1, + "width": 2 + }, + "tensionX": 1, + "tensionY": 1 + }, + "open": { + "x": "", + "y": "" + } + }, + "name": "mtta", + "render": "line", + "stepLine": { + "appearance": { + "bullets": [ + { + "deriveFieldsFromData": { + "fill": { + "color": "", + "opacity": "" + }, + "rotation": "", + "stroke": { + "color": "", + "opacity": "", + "width": "" + } + }, + "enabled": true, + "fill": { + "color": "", + "opacity": 1 + }, + "heatRules": { + "dataField": "", + "enabled": false, + "max": 100, + "min": 2 + }, + "label": { + "hideOversized": true, + "position": { + "dx": 0, + "dy": 0 + }, + "text": "{value}", + "truncate": true + }, + "radius": 5, + "render": "circle", + "rotation": 0, + "stroke": { + "color": "", + "opacity": 1, + "width": 1 + }, + "tooltip": { + "background": { + "color": "", + "opacity": 1 + }, + "cornerRadius": 3, + "enabled": true, + "getFillFromObject": true, + "pointerLength": 4, + "text": "{name}: [bold]{valueY}[/]" + } + } + ], + "connect": true, + "fill": { + "color": "", + "opacity": 0 + }, + "minDistance": 0.5, + "stroke": { + "color": "", + "dashArray": "", + "opacity": 1, + "width": 3 + }, + "tensionX": 1, + "tensionY": 1 + }, + "open": { + "x": "", + "y": "" + } + }, + "tooltip": { + "background": { + "color": "", + "opacity": 1 + }, + "cornerRadius": 3, + "enabled": true, + "pointerLength": 4, + "text": "{name}: [bold]{valueY.formatNumber('#,###.#')} min[/]" + }, + "visible": true, + "xAxis": "t", + "yAxis": "y", + "zIndex": 0 + }, + { + "candlestick": { + "appearance": { + "deriveFieldsFromData": { + "fill": { + "color": "", + "opacity": "" + }, + "stroke": { + "color": "", + "opacity": "", + "width": "" + } + }, + "fill": { + "color": "", + "opacity": 1 + }, + "heatRules": { + "dataField": "", + "enabled": false, + "max": "", + "min": "" + }, + "stacked": false, + "stroke": { + "color": "", + "opacity": 1, + "width": 1 + } + }, + "high": { + "x": "", + "y": "" + }, + "low": { + "x": "", + "y": "" + }, + "open": { + "x": "", + "y": "" + } + }, + "column": { + "appearance": { + "deriveFieldsFromData": { + "fill": { + "color": "", + "opacity": "" + }, + "stroke": { + "color": "", + "opacity": "", + "width": "" + } + }, + "fill": { + "color": "", + "opacity": 1 + }, + "heatRules": { + "dataField": "", + "enabled": false, + "max": "", + "min": "" + }, + "height": null, + "stacked": false, + "stroke": { + "color": "", + "opacity": 1, + "width": 1 + }, + "width": null + }, + "open": { + "x": "", + "y": "" + } + }, + "data": { + "source": "trend", + "x": "t", + "y": "mttr" + }, + "defaultState": { + "visible": true + }, + "hiddenInLegend": false, + "label": { + "text": "MTTR" + }, + "line": { + "appearance": { + "bullets": [ + { + "deriveFieldsFromData": { + "fill": { + "color": "", + "opacity": "" + }, + "rotation": "", + "stroke": { + "color": "", + "opacity": "", + "width": "" + } + }, + "enabled": true, + "fill": { + "color": "#E8883A", + "opacity": 1 + }, + "heatRules": { + "dataField": "", + "enabled": false, + "max": 100, + "min": 2 + }, + "label": { + "hideOversized": true, + "position": { + "dx": 0, + "dy": 0 + }, + "text": "{value}", + "truncate": true + }, + "radius": 3, + "render": "circle", + "rotation": 0, + "stroke": { + "color": "", + "opacity": 1, + "width": 1 + }, + "tooltip": { + "background": { + "color": "", + "opacity": 1 + }, + "cornerRadius": 3, + "enabled": true, + "getFillFromObject": true, + "pointerLength": 4, + "text": "{name}: [bold]{valueY.formatNumber('#,###.#')} min[/]" + } + } + ], + "connect": false, + "fill": { + "color": "", + "opacity": 0 + }, + "minDistance": 0.5, + "stroke": { + "color": "#E8883A", + "dashArray": "", + "opacity": 1, + "width": 2 + }, + "tensionX": 1, + "tensionY": 1 + }, + "open": { + "x": "", + "y": "" + } + }, + "name": "mttr", + "render": "line", + "stepLine": { + "appearance": { + "bullets": [], + "connect": true, + "fill": { + "color": "", + "opacity": 0 + }, + "minDistance": 0.5, + "stroke": { + "color": "", + "dashArray": "", + "opacity": 1, + "width": 3 + }, + "tensionX": 1, + "tensionY": 1 + }, + "open": { + "x": "", + "y": "" + } + }, + "tooltip": { + "background": { + "color": "", + "opacity": 1 + }, + "cornerRadius": 3, + "enabled": true, + "pointerLength": 4, + "text": "{name}: [bold]{valueY.formatNumber('#,###.#')} min[/]" + }, + "visible": true, + "xAxis": "t", + "yAxis": "y", + "zIndex": 0 + } + ], + "xAxes": [ + { + "appearance": { + "font": { + "size": 10, + "weight": 500 + }, + "grid": { + "color": "", + "dashArray": "", + "minDistance": 60, + "opacity": 0.3, + "position": 0.5 + }, + "inside": false, + "labels": { + "color": "", + "horizontalCenter": "middle", + "opacity": 1, + "rotation": 0, + "verticalCenter": "middle" + }, + "opposite": false + }, + "category": { + "break": { + "enabled": false, + "endCategory": "", + "size": 0.05, + "startCategory": "" + } + }, + "date": { + "baseInterval": { + "count": 1, + "enabled": false, + "skipEmptyPeriods": false, + "timeUnit": "hour" + }, + "break": { + "enabled": false, + "endDate": "", + "size": 0.05, + "startDate": "" + }, + "format": "M/d HH:mm", + "inputFormat": "yyyy-MM-dd HH:mm:ss", + "range": { + "max": "", + "min": "", + "useStrict": false + } + }, + "inversed": false, + "label": { + "color": "", + "enabled": false, + "text": "" + }, + "name": "t", + "render": "date", + "tooltip": { + "background": { + "color": "", + "opacity": 1 + }, + "cornerRadius": 3, + "enabled": true, + "pointerLength": 4, + "text": "" + }, + "value": { + "break": { + "enabled": false, + "endValue": 100, + "size": 0.05, + "startValue": 0 + }, + "format": "#,###.##", + "logarithmic": false, + "range": { + "max": "", + "min": "", + "useStrict": false + } + }, + "visible": true + } + ], + "yAxes": [ + { + "appearance": { + "font": { + "size": 10, + "weight": 500 + }, + "grid": { + "color": "", + "dashArray": "", + "minDistance": null, + "opacity": 0.3, + "position": 0.5 + }, + "inside": false, + "labels": { + "color": "", + "horizontalCenter": "middle", + "opacity": 1, + "rotation": 0, + "verticalCenter": "middle" + }, + "opposite": false + }, + "category": { + "break": { + "enabled": false, + "endCategory": "", + "size": 0.05, + "startCategory": "" + } + }, + "date": { + "baseInterval": { + "count": 1, + "enabled": false, + "skipEmptyPeriods": false, + "timeUnit": "hour" + }, + "break": { + "enabled": false, + "endDate": "", + "size": 0.05, + "startDate": "" + }, + "format": "M/d/yyyy HH:mm:ss", + "inputFormat": "yyyy-MM-dd kk:mm:ss", + "range": { + "max": "", + "min": "", + "useStrict": false + } + }, + "inversed": false, + "label": { + "color": "", + "enabled": true, + "text": "minutes" + }, + "name": "y", + "render": "value", + "tooltip": { + "background": { + "color": "", + "opacity": 1 + }, + "cornerRadius": 3, + "enabled": false, + "pointerLength": 4, + "text": "" + }, + "value": { + "break": { + "enabled": false, + "endValue": 100, + "size": 0.05, + "startValue": 0 + }, + "format": "#,###.#", + "logarithmic": false, + "range": { + "max": "", + "min": 0, + "useStrict": false + } + }, + "visible": true + } + ] + }, + "type": "ia.chart.xy" + }, + { + "meta": { + "name": "trendEmpty" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tfor r in (value['mtta_mttr']['trend'] or []):\n\t\t\tif r['mtta_ms'] is not None or r['mttr_ms'] is not None:\n\t\t\t\treturn False\n\texcept:\n\t\tpass\n\treturn True", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "params": { + "icon": "timeline", + "message": "Not enough ack/clear data for a trend" + }, + "path": "PrimeControls/Components/EmptyState" + }, + "type": "ia.display.view" + } + ], + "meta": { + "name": "cardMtta" + }, + "position": { + "basis": "0px", + "grow": 1 + }, + "props": { + "direction": "column", + "style": { + "classes": "PrimeControls/Card", + "gap": "8px" + } + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "children": [ + { + "meta": { + "name": "flTitle" + }, + "props": { + "style": { + "classes": "PrimeControls/CardTitle" + }, + "text": "Flood Episodes" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "flCaption" + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tp = value['floods']['pct_time_in_flood']\n\t\tif p is None:\n\t\t\treturn ''\n\t\treturn '%s of window time in flood' % PrimeControls.fmt.pct(p, 1)\n\texcept:\n\t\treturn ''", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted" + } + }, + "type": "ia.display.label" + } + ], + "meta": { + "name": "flTitleRow" + }, + "position": { + "shrink": 0 + }, + "props": { + "alignItems": "center", + "justify": "space-between" + }, + "type": "ia.container.flex" + }, + { + "meta": { + "name": "floodTable" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn len(value['floods']['episodes'] or []) > 0\n\texcept:\n\t\treturn False", + "type": "script" + } + ], + "type": "property" + } + }, + "props.data": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\tout = []\n\ttry:\n\t\teps = value['floods']['episodes'] or []\n\texcept:\n\t\teps = []\n\tfor e in eps:\n\t\ttry:\n\t\t\ttop = e['top_source_label'] or ''\n\t\t\tif top and e['top_source_count'] is not None:\n\t\t\t\ttop = '%s (%s)' % (top, PrimeControls.fmt.num(e['top_source_count']))\n\t\t\tout.append({'start': PrimeControls.fmt.day_clock(e['start_ms']), 'duration': PrimeControls.fmt.dur(e['duration_ms']), 'events': PrimeControls.fmt.num(e['event_count']), 'peak': PrimeControls.fmt.num(e['peak_bin_count']), 'top': top or '--', 'area': e['top_area'] or '--'})\n\t\texcept:\n\t\t\tpass\n\treturn out", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "columns": [ + { + "align": "center", + "editable": false, + "field": "start", + "header": { + "align": "center", + "justify": "left", + "style": { + "classes": "" + }, + "title": "Start" + }, + "justify": "auto", + "render": "string", + "resizable": true, + "sort": "none", + "sortable": false, + "strictWidth": false, + "visible": true, + "width": 1.3 + }, + { + "align": "center", + "editable": false, + "field": "duration", + "header": { + "align": "center", + "justify": "left", + "style": { + "classes": "" + }, + "title": "Duration" + }, + "justify": "auto", + "render": "string", + "resizable": true, + "sort": "none", + "sortable": false, + "strictWidth": false, + "visible": true, + "width": 1 + }, + { + "align": "center", + "editable": false, + "field": "events", + "header": { + "align": "center", + "justify": "left", + "style": { + "classes": "" + }, + "title": "Events" + }, + "justify": "right", + "render": "string", + "resizable": true, + "sort": "none", + "sortable": false, + "strictWidth": false, + "visible": true, + "width": 0.7 + }, + { + "align": "center", + "editable": false, + "field": "peak", + "header": { + "align": "center", + "justify": "left", + "style": { + "classes": "" + }, + "title": "Peak / 10 min" + }, + "justify": "right", + "render": "string", + "resizable": true, + "sort": "none", + "sortable": false, + "strictWidth": false, + "visible": true, + "width": 0.9 + }, + { + "align": "center", + "editable": false, + "field": "top", + "header": { + "align": "center", + "justify": "left", + "style": { + "classes": "" + }, + "title": "Top Contributor" + }, + "justify": "auto", + "render": "string", + "resizable": true, + "sort": "none", + "sortable": false, + "strictWidth": false, + "visible": true, + "width": 2 + }, + { + "align": "center", + "editable": false, + "field": "area", + "header": { + "align": "center", + "justify": "left", + "style": { + "classes": "" + }, + "title": "Area" + }, + "justify": "auto", + "render": "string", + "resizable": true, + "sort": "none", + "sortable": false, + "strictWidth": false, + "visible": true, + "width": 1 + } + ], + "pager": { + "bottom": false + }, + "rows": { + "height": 34 + }, + "selection": { + "enableRowSelection": false + }, + "cells": { + "style": { + "padding": "5px" + } + } + }, + "type": "ia.display.table" + }, + { + "meta": { + "name": "floodEmpty" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn len(value['floods']['episodes'] or []) == 0\n\texcept:\n\t\treturn True", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "params": { + "icon": "waves", + "message": "No flood episodes in range" + }, + "path": "PrimeControls/Components/EmptyState" + }, + "type": "ia.display.view" + } + ], + "meta": { + "name": "cardFloods" + }, + "position": { + "basis": "0px", + "grow": 1 + }, + "props": { + "direction": "column", + "style": { + "classes": "PrimeControls/Card", + "gap": "8px" + } + }, + "type": "ia.container.flex" + } + ], + "meta": { + "name": "rowBottom" + }, + "position": { + "basis": "340px", + "grow": 1, + "shrink": 0 + }, + "props": { + "style": { + "gap": "12px" + } + }, + "type": "ia.container.flex" + } + ], + "meta": { + "name": "content" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn bool(value) and value['meta']['event_count'] > 0\n\texcept:\n\t\treturn False", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "direction": "column", + "style": { + "gap": "12px" + } + }, + "type": "ia.container.flex" + } + ], + "meta": { + "name": "root" + }, + "props": { + "direction": "column", + "style": { + "gap": "12px" + } + }, + "type": "ia.container.flex" + } +} diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Tabs/BadActors/resource.json b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Tabs/BadActors/resource.json new file mode 100644 index 0000000..012eddf --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Tabs/BadActors/resource.json @@ -0,0 +1,16 @@ +{ + "scope": "G", + "version": 1, + "restricted": false, + "overridable": true, + "files": [ + "view.json" + ], + "attributes": { + "lastModificationSignature": "7504c7695ab45f2fc1d0d6dc8071e96746ac068f99e878d3ca1dcf804c9eea76", + "lastModification": { + "actor": "admin", + "timestamp": "2026-07-16T20:38:41Z" + } + } +} diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Tabs/BadActors/view.json b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Tabs/BadActors/view.json new file mode 100644 index 0000000..b231024 --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Tabs/BadActors/view.json @@ -0,0 +1,1095 @@ +{ + "custom": {}, + "params": {}, + "propConfig": { + "params.bundle": { + "paramDirection": "input", + "persistent": false + } + }, + "props": { + "defaultSize": { + "width": 1600 + } + }, + "root": { + "children": [ + { + "meta": { + "name": "wholeEmpty" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tif not value:\n\t\t\treturn True\n\t\treturn int(value['meta']['event_count'] or 0) == 0\n\texcept:\n\t\treturn True", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "params": { + "icon": "inbox", + "message": "No alarm events in the selected range" + }, + "path": "PrimeControls/Components/EmptyState" + }, + "type": "ia.display.view" + }, + { + "children": [ + { + "children": [ + { + "meta": { + "name": "lblParetoTitle" + }, + "position": { + "shrink": 0 + }, + "props": { + "style": { + "classes": "PrimeControls/CardTitle" + }, + "text": "Top Sources by Activations" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "lblParetoCaption" + }, + "position": { + "shrink": 0 + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\trows = value['pareto']['rows'] or []\n\t\ttotal = value['pareto']['total_activations'] or 0\n\texcept:\n\t\trows = []\n\t\ttotal = 0\n\tif not rows or not total:\n\t\treturn 'No activations in the selected range'\n\tn = min(len(rows), 10)\n\ttry:\n\t\tcum = rows[n - 1]['cum_pct']\n\texcept:\n\t\tcum = None\n\tif cum is None:\n\t\treturn 'Top %d sources of %s activations' % (n, PrimeControls.fmt.num(total))\n\treturn 'Top %d sources = %s of all %s activations' % (n, PrimeControls.fmt.pct(cum), PrimeControls.fmt.num(total))", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted" + } + }, + "type": "ia.display.label" + }, + { + "events": { + "component": { + "onRowClick": { + "config": { + "script": "\ttry:\n\t\trow = event.value\n\t\tsrc = row['source']\n\t\tlbl = row['label']\n\texcept:\n\t\treturn\n\ttry:\n\t\tsrc = src['value']\n\texcept:\n\t\tpass\n\ttry:\n\t\tlbl = lbl['value']\n\texcept:\n\t\tpass\n\tif not src:\n\t\treturn\n\ttry:\n\t\tsystem.perspective.openPopup('PC_AlarmDetail', 'PrimeControls/Popups/AlarmDetail', params = {'source': src, 'label': lbl}, showCloseIcon = True, draggable = True)\n\texcept:\n\t\tpass" + }, + "scope": "G", + "type": "script" + } + } + }, + "meta": { + "name": "tblPareto" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn bool(value['pareto']['rows'])\n\texcept:\n\t\treturn False", + "type": "script" + } + ], + "type": "property" + } + }, + "props.data": { + "binding": { + "config": { + "struct": { + "bundle": "{view.params.bundle}", + "focus": "{session.custom.PrimeControls.ui.badActorFocus}" + }, + "waitOnAll": false + }, + "transforms": [ + { + "code": "\ttry:\n\t\trows = value['bundle']['pareto']['rows'] or []\n\texcept:\n\t\trows = []\n\ttry:\n\t\ttotal = value['bundle']['pareto']['total_activations'] or 0\n\texcept:\n\t\ttotal = 0\n\ttry:\n\t\tfocus = value['focus'] or ''\n\texcept:\n\t\tfocus = ''\n\thi = {'backgroundColor': 'rgba(59, 125, 216, 0.18)', 'fontWeight': 'bold'}\n\tpri = ['Diagnostic', 'Low', 'Medium', 'High', 'Critical']\n\tout = []\n\trank = 0\n\tfor r in rows[:10]:\n\t\ttry:\n\t\t\trank = rank + 1\n\t\t\tsrc = r['source'] or ''\n\t\t\tcnt = r['count'] or 0\n\t\t\td = {}\n\t\t\td['rank'] = rank\n\t\t\td['source'] = src\n\t\t\td['label'] = r['label'] or src\n\t\t\td['area'] = r['area'] or ''\n\t\t\td['count'] = cnt\n\t\t\tif total > 0:\n\t\t\t\td['share'] = round(cnt * 100.0 / total, 1)\n\t\t\telse:\n\t\t\t\td['share'] = 0\n\t\t\ttry:\n\t\t\t\td['cum'] = PrimeControls.fmt.pct(r['cum_pct'])\n\t\t\texcept:\n\t\t\t\td['cum'] = '--'\n\t\t\tif focus and src == focus:\n\t\t\t\tfor k in ('label', 'area', 'count', 'cum'):\n\t\t\t\t\td[k] = {'value': d[k], 'style': hi}\n\t\t\tpname = r['priority_name'] or ''\n\t\t\tif pname in pri:\n\t\t\t\td['priority'] = {'value': pname, 'style': {'classes': 'PrimeControls/PrioritySoft/' + pname}}\n\t\t\telse:\n\t\t\t\td['priority'] = pname\n\t\t\tout.append(d)\n\t\texcept:\n\t\t\tpass\n\treturn out", + "type": "script" + } + ], + "type": "expr-struct" + } + } + }, + "props": { + "cells": { + "style": { + "padding": "5px" + } + }, + "columns": [ + { + "field": "source", + "header": { + "title": "source" + }, + "visible": false + }, + { + "field": "rank", + "header": { + "justify": "center", + "title": "#" + }, + "justify": "center", + "width": 0.5 + }, + { + "field": "label", + "header": { + "title": "Source" + }, + "width": 3 + }, + { + "field": "area", + "header": { + "title": "Area" + }, + "width": 1.5 + }, + { + "field": "priority", + "header": { + "justify": "center", + "title": "Priority" + }, + "justify": "center", + "width": 1.3 + }, + { + "field": "count", + "header": { + "justify": "right", + "title": "Count" + }, + "justify": "right", + "render": "number", + "width": 1 + }, + { + "field": "share", + "header": { + "title": "Share of Total" + }, + "number": "progress", + "progressBar": { + "bar": { + "color": "#3B7DD8" + }, + "max": 100, + "min": 0 + }, + "render": "number", + "width": 2.4 + }, + { + "field": "cum", + "header": { + "justify": "right", + "title": "Cumulative" + }, + "justify": "right", + "width": 1.3 + } + ], + "pager": { + "bottom": false + }, + "rows": { + "height": 34 + } + }, + "type": "ia.display.table" + }, + { + "meta": { + "name": "emptyPareto" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn not value['pareto']['rows']\n\texcept:\n\t\treturn True", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "params": { + "icon": "bar_chart", + "message": "No activations in the selected range" + }, + "path": "PrimeControls/Components/EmptyState" + }, + "type": "ia.display.view" + } + ], + "meta": { + "name": "cardPareto" + }, + "position": { + "basis": "320px", + "grow": 1 + }, + "props": { + "direction": "column", + "style": { + "classes": "PrimeControls/Card", + "gap": "4px" + } + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "children": [ + { + "meta": { + "name": "lblFocus" + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "struct": { + "bundle": "{view.params.bundle}", + "focus": "{session.custom.PrimeControls.ui.badActorFocus}" + }, + "waitOnAll": false + }, + "transforms": [ + { + "code": "\ttry:\n\t\tsrc = value['focus'] or ''\n\texcept:\n\t\tsrc = ''\n\tif not src:\n\t\treturn ''\n\tlabel = src\n\ttry:\n\t\tb = value['bundle']\n\t\tlists = [b['pareto']['rows'], b['chattering'], b['standing']['rows'], b['fleeting']['sources']]\n\t\tfor rows in lists:\n\t\t\tfor r in (rows or []):\n\t\t\t\tif r['source'] == src:\n\t\t\t\t\tlabel = r['label'] or src\n\t\t\t\t\traise StopIteration\n\texcept:\n\t\tpass\n\treturn 'Focused: %s' % label", + "type": "script" + } + ], + "type": "expr-struct" + } + } + }, + "props": { + "style": { + "whiteSpace": "nowrap" + } + }, + "type": "ia.display.label", + "position": { + "basis": "auto", + "grow": 0, + "shrink": 0 + } + }, + { + "events": { + "dom": { + "onClick": { + "config": { + "script": "\tself.session.custom.PrimeControls.ui.badActorFocus = ''" + }, + "scope": "G", + "type": "script" + } + } + }, + "meta": { + "name": "icoClearFocus" + }, + "position": { + "basis": "20px", + "shrink": 0 + }, + "props": { + "path": "material/close", + "style": { + "cursor": "pointer", + "fontSize": "16px" + } + }, + "type": "ia.display.icon" + } + ], + "meta": { + "name": "chipFocus" + }, + "position": { + "shrink": 0 + }, + "props": { + "alignItems": "center", + "style": { + "classes": "PrimeControls/Chip/Flat", + "gap": "6px" + } + }, + "type": "ia.container.flex" + }, + { + "meta": { + "name": "lblFocusHint" + }, + "position": { + "grow": 1 + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted" + }, + "text": "Matching rows are highlighted below \u2014 click the X to clear" + }, + "type": "ia.display.label" + } + ], + "meta": { + "name": "rowFocus" + }, + "position": { + "basis": "36px", + "shrink": 0 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "expression": "{session.custom.PrimeControls.ui.badActorFocus} != \"\"" + }, + "type": "expr" + } + } + }, + "props": { + "alignItems": "center", + "style": { + "gap": "10px" + } + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "children": [ + { + "meta": { + "name": "lblChatterTitle" + }, + "position": { + "shrink": 0 + }, + "props": { + "style": { + "classes": "PrimeControls/CardTitle" + }, + "text": "Chattering" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "lblChatterCaption" + }, + "position": { + "shrink": 0 + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted" + }, + "text": "Rapid-cycling alarms (>10/hr, median gap <= 2 min)" + }, + "type": "ia.display.label" + }, + { + "events": { + "component": { + "onRowClick": { + "config": { + "script": "\ttry:\n\t\trow = event.value\n\t\tsrc = row['source']\n\t\tlbl = row['label']\n\texcept:\n\t\treturn\n\ttry:\n\t\tsrc = src['value']\n\texcept:\n\t\tpass\n\ttry:\n\t\tlbl = lbl['value']\n\texcept:\n\t\tpass\n\tif not src:\n\t\treturn\n\ttry:\n\t\tsystem.perspective.openPopup('PC_AlarmDetail', 'PrimeControls/Popups/AlarmDetail', params = {'source': src, 'label': lbl}, showCloseIcon = True, draggable = True)\n\texcept:\n\t\tpass" + }, + "scope": "G", + "type": "script" + } + } + }, + "meta": { + "name": "tblChatter" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn bool(value['chattering'])\n\texcept:\n\t\treturn False", + "type": "script" + } + ], + "type": "property" + } + }, + "props.data": { + "binding": { + "config": { + "struct": { + "bundle": "{view.params.bundle}", + "focus": "{session.custom.PrimeControls.ui.badActorFocus}" + }, + "waitOnAll": false + }, + "transforms": [ + { + "code": "\ttry:\n\t\trows = value['bundle']['chattering'] or []\n\texcept:\n\t\trows = []\n\ttry:\n\t\tfocus = value['focus'] or ''\n\texcept:\n\t\tfocus = ''\n\thi = {'backgroundColor': 'rgba(59, 125, 216, 0.18)', 'fontWeight': 'bold'}\n\tpri = ['Diagnostic', 'Low', 'Medium', 'High', 'Critical']\n\tout = []\n\tfor r in rows:\n\t\ttry:\n\t\t\tsrc = r['source'] or ''\n\t\t\td = {}\n\t\t\td['source'] = src\n\t\t\td['label'] = r['label'] or src\n\t\t\td['area'] = r['area'] or ''\n\t\t\td['count'] = r['count'] or 0\n\t\t\td['per_hour'] = PrimeControls.fmt.num(r['per_hour'], 1)\n\t\t\tgap = r['median_gap_s']\n\t\t\tif gap is None:\n\t\t\t\td['gap'] = '--'\n\t\t\telse:\n\t\t\t\td['gap'] = PrimeControls.fmt.dur(gap * 1000.0)\n\t\t\tif focus and src == focus:\n\t\t\t\tfor k in ('label', 'area', 'count', 'per_hour', 'gap'):\n\t\t\t\t\td[k] = {'value': d[k], 'style': hi}\n\t\t\tpname = r['priority_name'] or ''\n\t\t\tif pname in pri:\n\t\t\t\td['priority'] = {'value': pname, 'style': {'classes': 'PrimeControls/PrioritySoft/' + pname}}\n\t\t\telse:\n\t\t\t\td['priority'] = pname\n\t\t\tout.append(d)\n\t\texcept:\n\t\t\tpass\n\treturn out", + "type": "script" + } + ], + "type": "expr-struct" + } + } + }, + "props": { + "cells": { + "style": { + "padding": "5px" + } + }, + "columns": [ + { + "field": "source", + "header": { + "title": "source" + }, + "visible": false + }, + { + "field": "label", + "header": { + "title": "Source" + }, + "width": 3 + }, + { + "field": "area", + "header": { + "title": "Area" + }, + "width": 1.6 + }, + { + "field": "priority", + "header": { + "justify": "center", + "title": "Priority" + }, + "justify": "center", + "width": 1.4 + }, + { + "field": "count", + "header": { + "justify": "right", + "title": "Count" + }, + "justify": "right", + "render": "number", + "width": 1 + }, + { + "field": "per_hour", + "header": { + "justify": "right", + "title": "Per Hr" + }, + "justify": "right", + "width": 1.1 + }, + { + "field": "gap", + "header": { + "justify": "right", + "title": "Med Gap" + }, + "justify": "right", + "width": 1.2 + } + ], + "pager": { + "bottom": false + }, + "rows": { + "height": 34 + } + }, + "type": "ia.display.table" + }, + { + "meta": { + "name": "emptyChatter" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn not value['chattering']\n\texcept:\n\t\treturn True", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "params": { + "icon": "check_circle", + "message": "No chattering alarms in range" + }, + "path": "PrimeControls/Components/EmptyState" + }, + "type": "ia.display.view" + } + ], + "meta": { + "name": "cardChatter" + }, + "position": { + "basis": "0px", + "grow": 1 + }, + "props": { + "direction": "column", + "style": { + "classes": "PrimeControls/Card", + "gap": "4px" + } + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "meta": { + "name": "lblStandingTitle" + }, + "position": { + "shrink": 0 + }, + "props": { + "style": { + "classes": "PrimeControls/CardTitle" + }, + "text": "Standing / Stale" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "lblStandingCaption" + }, + "position": { + "shrink": 0 + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted" + }, + "text": "Alarms active for more than 24 h" + }, + "type": "ia.display.label" + }, + { + "events": { + "component": { + "onRowClick": { + "config": { + "script": "\ttry:\n\t\trow = event.value\n\t\tsrc = row['source']\n\t\tlbl = row['label']\n\texcept:\n\t\treturn\n\ttry:\n\t\tsrc = src['value']\n\texcept:\n\t\tpass\n\ttry:\n\t\tlbl = lbl['value']\n\texcept:\n\t\tpass\n\tif not src:\n\t\treturn\n\ttry:\n\t\tsystem.perspective.openPopup('PC_AlarmDetail', 'PrimeControls/Popups/AlarmDetail', params = {'source': src, 'label': lbl}, showCloseIcon = True, draggable = True)\n\texcept:\n\t\tpass" + }, + "scope": "G", + "type": "script" + } + } + }, + "meta": { + "name": "tblStanding" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn bool(value['standing']['rows'])\n\texcept:\n\t\treturn False", + "type": "script" + } + ], + "type": "property" + } + }, + "props.data": { + "binding": { + "config": { + "struct": { + "bundle": "{view.params.bundle}", + "focus": "{session.custom.PrimeControls.ui.badActorFocus}" + }, + "waitOnAll": false + }, + "transforms": [ + { + "code": "\ttry:\n\t\trows = value['bundle']['standing']['rows'] or []\n\texcept:\n\t\trows = []\n\ttry:\n\t\tfocus = value['focus'] or ''\n\texcept:\n\t\tfocus = ''\n\thi = {'backgroundColor': 'rgba(59, 125, 216, 0.18)', 'fontWeight': 'bold'}\n\tpri = ['Diagnostic', 'Low', 'Medium', 'High', 'Critical']\n\tout = []\n\tfor r in rows:\n\t\ttry:\n\t\t\tsrc = r['source'] or ''\n\t\t\td = {}\n\t\t\td['source'] = src\n\t\t\td['label'] = r['label'] or src\n\t\t\td['area'] = r['area'] or ''\n\t\t\td['age'] = PrimeControls.fmt.dur(r['age_ms'])\n\t\t\tif r['unacked']:\n\t\t\t\td['unacked'] = {'value': 'Yes', 'style': {'color': '#D64550', 'fontWeight': 'bold'}}\n\t\t\telse:\n\t\t\t\td['unacked'] = ''\n\t\t\tif focus and src == focus:\n\t\t\t\tfor k in ('label', 'area', 'age'):\n\t\t\t\t\td[k] = {'value': d[k], 'style': hi}\n\t\t\tpname = r['priority_name'] or ''\n\t\t\tif pname in pri:\n\t\t\t\td['priority'] = {'value': pname, 'style': {'classes': 'PrimeControls/PrioritySoft/' + pname}}\n\t\t\telse:\n\t\t\t\td['priority'] = pname\n\t\t\tout.append(d)\n\t\texcept:\n\t\t\tpass\n\treturn out", + "type": "script" + } + ], + "type": "expr-struct" + } + } + }, + "props": { + "cells": { + "style": { + "padding": "5px" + } + }, + "columns": [ + { + "field": "source", + "header": { + "title": "source" + }, + "visible": false + }, + { + "field": "label", + "header": { + "title": "Source" + }, + "width": 3 + }, + { + "field": "area", + "header": { + "title": "Area" + }, + "width": 1.6 + }, + { + "field": "priority", + "header": { + "justify": "center", + "title": "Priority" + }, + "justify": "center", + "width": 1.4 + }, + { + "field": "age", + "header": { + "justify": "right", + "title": "Active For" + }, + "justify": "right", + "width": 1.3 + }, + { + "field": "unacked", + "header": { + "justify": "center", + "title": "Unacked" + }, + "justify": "center", + "width": 1.1 + } + ], + "pager": { + "bottom": false + }, + "rows": { + "height": 34 + } + }, + "type": "ia.display.table" + }, + { + "meta": { + "name": "emptyStanding" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn not value['standing']['rows']\n\texcept:\n\t\treturn True", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "params": { + "icon": "check_circle", + "message": "No standing alarms over 24 h" + }, + "path": "PrimeControls/Components/EmptyState" + }, + "type": "ia.display.view" + } + ], + "meta": { + "name": "cardStanding" + }, + "position": { + "basis": "0px", + "grow": 1 + }, + "props": { + "direction": "column", + "style": { + "classes": "PrimeControls/Card", + "gap": "4px" + } + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "meta": { + "name": "lblFleetingTitle" + }, + "position": { + "shrink": 0 + }, + "props": { + "style": { + "classes": "PrimeControls/CardTitle" + }, + "text": "Fleeting" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "lblFleetingCaption" + }, + "position": { + "shrink": 0 + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\ttot = value['fleeting']['total']\n\texcept:\n\t\ttot = None\n\tif not tot:\n\t\treturn 'Activations lasting under 10 s'\n\treturn '%s fleeting activations (under 10 s)' % PrimeControls.fmt.num(tot)", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted" + } + }, + "type": "ia.display.label" + }, + { + "events": { + "component": { + "onRowClick": { + "config": { + "script": "\ttry:\n\t\trow = event.value\n\t\tsrc = row['source']\n\t\tlbl = row['label']\n\texcept:\n\t\treturn\n\ttry:\n\t\tsrc = src['value']\n\texcept:\n\t\tpass\n\ttry:\n\t\tlbl = lbl['value']\n\texcept:\n\t\tpass\n\tif not src:\n\t\treturn\n\ttry:\n\t\tsystem.perspective.openPopup('PC_AlarmDetail', 'PrimeControls/Popups/AlarmDetail', params = {'source': src, 'label': lbl}, showCloseIcon = True, draggable = True)\n\texcept:\n\t\tpass" + }, + "scope": "G", + "type": "script" + } + } + }, + "meta": { + "name": "tblFleeting" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn bool(value['fleeting']['sources'])\n\texcept:\n\t\treturn False", + "type": "script" + } + ], + "type": "property" + } + }, + "props.data": { + "binding": { + "config": { + "struct": { + "bundle": "{view.params.bundle}", + "focus": "{session.custom.PrimeControls.ui.badActorFocus}" + }, + "waitOnAll": false + }, + "transforms": [ + { + "code": "\ttry:\n\t\trows = value['bundle']['fleeting']['sources'] or []\n\texcept:\n\t\trows = []\n\ttry:\n\t\tfocus = value['focus'] or ''\n\texcept:\n\t\tfocus = ''\n\thi = {'backgroundColor': 'rgba(59, 125, 216, 0.18)', 'fontWeight': 'bold'}\n\tpri = ['Diagnostic', 'Low', 'Medium', 'High', 'Critical']\n\tout = []\n\tfor r in rows:\n\t\ttry:\n\t\t\tsrc = r['source'] or ''\n\t\t\td = {}\n\t\t\td['source'] = src\n\t\t\td['label'] = r['label'] or src\n\t\t\td['area'] = r['area'] or ''\n\t\t\td['count'] = r['count'] or 0\n\t\t\tmed = r['median_s']\n\t\t\tif med is None:\n\t\t\t\td['median'] = '--'\n\t\t\telse:\n\t\t\t\td['median'] = PrimeControls.fmt.dur(med * 1000.0)\n\t\t\tif focus and src == focus:\n\t\t\t\tfor k in ('label', 'area', 'count', 'median'):\n\t\t\t\t\td[k] = {'value': d[k], 'style': hi}\n\t\t\tpname = r['priority_name'] or ''\n\t\t\tif pname in pri:\n\t\t\t\td['priority'] = {'value': pname, 'style': {'classes': 'PrimeControls/PrioritySoft/' + pname}}\n\t\t\telse:\n\t\t\t\td['priority'] = pname\n\t\t\tout.append(d)\n\t\texcept:\n\t\t\tpass\n\treturn out", + "type": "script" + } + ], + "type": "expr-struct" + } + } + }, + "props": { + "cells": { + "style": { + "padding": "5px" + } + }, + "columns": [ + { + "field": "source", + "header": { + "title": "source" + }, + "visible": false + }, + { + "field": "label", + "header": { + "title": "Source" + }, + "width": 3 + }, + { + "field": "area", + "header": { + "title": "Area" + }, + "width": 1.6 + }, + { + "field": "priority", + "header": { + "justify": "center", + "title": "Priority" + }, + "justify": "center", + "width": 1.4 + }, + { + "field": "count", + "header": { + "justify": "right", + "title": "Count" + }, + "justify": "right", + "render": "number", + "width": 1 + }, + { + "field": "median", + "header": { + "justify": "right", + "title": "Med Dur" + }, + "justify": "right", + "width": 1.2 + } + ], + "pager": { + "bottom": false + }, + "rows": { + "height": 34 + } + }, + "type": "ia.display.table" + }, + { + "meta": { + "name": "emptyFleeting" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn not value['fleeting']['sources']\n\texcept:\n\t\treturn True", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "params": { + "icon": "check_circle", + "message": "No fleeting activations in range" + }, + "path": "PrimeControls/Components/EmptyState" + }, + "type": "ia.display.view" + } + ], + "meta": { + "name": "cardFleeting" + }, + "position": { + "basis": "0px", + "grow": 1 + }, + "props": { + "direction": "column", + "style": { + "classes": "PrimeControls/Card", + "gap": "4px" + } + }, + "type": "ia.container.flex" + } + ], + "meta": { + "name": "rowTables" + }, + "position": { + "basis": "280px", + "grow": 1 + }, + "props": { + "style": { + "gap": "12px" + } + }, + "type": "ia.container.flex" + } + ], + "meta": { + "name": "content" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn bool(value) and int(value['meta']['event_count'] or 0) > 0\n\texcept:\n\t\treturn False", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "direction": "column", + "style": { + "gap": "12px" + } + }, + "type": "ia.container.flex" + } + ], + "meta": { + "name": "root" + }, + "props": { + "direction": "column", + "style": { + "gap": "12px" + } + }, + "type": "ia.container.flex" + } +} diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Journal/resource.json b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Journal/resource.json new file mode 100644 index 0000000..276dced --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Journal/resource.json @@ -0,0 +1,10 @@ +{ + "scope": "G", + "version": 1, + "restricted": false, + "overridable": true, + "files": [ + "view.json" + ], + "attributes": {} +} diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Journal/view.json b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Journal/view.json new file mode 100644 index 0000000..14a4ca2 --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Journal/view.json @@ -0,0 +1,479 @@ +{ + "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": false + } + }, + "props": { + "defaultSize": { + "height": 800, + "width": 1600 + } + }, + "root": { + "children": [ + { + "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" + }, + { + "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" + } + }, + "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" + } + } + }, + "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/Toolbar", + "gap": "8px" + } + }, + "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 + }, + "cells": { + "style": { + "padding": "5px" + } + } + }, + "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": { + "name": "root" + }, + "props": { + "direction": "column", + "style": { + "gap": "12px" + } + }, + "type": "ia.container.flex" + } +} diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Overview/resource.json b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Overview/resource.json new file mode 100644 index 0000000..276dced --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Overview/resource.json @@ -0,0 +1,10 @@ +{ + "scope": "G", + "version": 1, + "restricted": false, + "overridable": true, + "files": [ + "view.json" + ], + "attributes": {} +} diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Overview/view.json b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Overview/view.json new file mode 100644 index 0000000..f5d44cd --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Overview/view.json @@ -0,0 +1,1807 @@ +{ + "custom": {}, + "params": { + "bundle": null + }, + "propConfig": { + "params.bundle": { + "paramDirection": "input", + "persistent": false + } + }, + "props": { + "defaultSize": { + "height": 900, + "width": 1600 + } + }, + "root": { + "children": [ + { + "meta": { + "name": "emptyWhole" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\ttry:\n\t\treturn not (int(value['meta']['event_count']) > 0)\n\texcept:\n\t\treturn True", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "params": { + "icon": "inbox", + "message": "No alarm events in the selected range" + }, + "path": "PrimeControls/Components/EmptyState" + }, + "type": "ia.display.view" + }, + { + "children": [ + { + "meta": { + "name": "repPriority" + }, + "position": { + "basis": "120px", + "shrink": 0 + }, + "propConfig": { + "props.instances": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\trows = []\n\ttry:\n\t\tfor r in (value['priority']['raw'] or []):\n\t\t\ttry:\n\t\t\t\trows.append({'name': r['name'], 'level': int(r['level']), 'count': int(r['count'])})\n\t\t\texcept:\n\t\t\t\tpass\n\texcept:\n\t\trows = []\n\trows.sort(key=lambda r: -r['level'])\n\ttotal = 0\n\tfor r in rows:\n\t\ttotal = total + r['count']\n\tout = []\n\tfor r in rows:\n\t\tif total > 0:\n\t\t\tp = 100.0 * r['count'] / total\n\t\telse:\n\t\t\tp = 0.0\n\t\tout.append({'priority_name': r['name'], 'count': r['count'], 'pct': p})\n\treturn out", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "direction": "row", + "elementPosition": { + "basis": "0px", + "grow": 1, + "shrink": 1 + }, + "path": "PrimeControls/Components/PriorityCard", + "style": { + "gap": "10px", + "overflow": "visible" + }, + "useDefaultViewHeight": false, + "useDefaultViewWidth": false + }, + "type": "ia.display.flex-repeater" + }, + { + "children": [ + { + "meta": { + "name": "kpiActiveNow" + }, + "position": { + "basis": "100px", + "grow": 1 + }, + "propConfig": { + "props.params.delta": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\ttry:\n\t\treturn value['kpis']['active_now']\n\texcept:\n\t\treturn None", + "type": "script" + } + ], + "type": "property" + } + }, + "props.params.value": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\ttry:\n\t\treturn PrimeControls.fmt.num(value['kpis']['active_now']['value'])\n\texcept:\n\t\treturn '--'", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "params": { + "label": "Active Now" + }, + "path": "PrimeControls/Components/KpiCard" + }, + "type": "ia.display.view" + }, + { + "meta": { + "name": "kpiUnacked" + }, + "position": { + "basis": "100px", + "grow": 1 + }, + "propConfig": { + "props.params.delta": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\ttry:\n\t\treturn value['kpis']['unacked_now']\n\texcept:\n\t\treturn None", + "type": "script" + } + ], + "type": "property" + } + }, + "props.params.value": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\ttry:\n\t\treturn PrimeControls.fmt.num(value['kpis']['unacked_now']['value'])\n\texcept:\n\t\treturn '--'", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "params": { + "label": "Unacked" + }, + "path": "PrimeControls/Components/KpiCard" + }, + "type": "ia.display.view" + }, + { + "meta": { + "name": "kpiShelved" + }, + "position": { + "basis": "100px", + "grow": 1 + }, + "propConfig": { + "props.params.delta": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\ttry:\n\t\treturn value['kpis']['shelved_now']\n\texcept:\n\t\treturn None", + "type": "script" + } + ], + "type": "property" + } + }, + "props.params.value": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\ttry:\n\t\treturn PrimeControls.fmt.num(value['kpis']['shelved_now']['value'])\n\texcept:\n\t\treturn '--'", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "params": { + "label": "Shelved" + }, + "path": "PrimeControls/Components/KpiCard" + }, + "type": "ia.display.view" + }, + { + "meta": { + "name": "kpiStanding" + }, + "position": { + "basis": "100px", + "grow": 1 + }, + "propConfig": { + "props.params.delta": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\ttry:\n\t\treturn value['kpis']['standing_count']\n\texcept:\n\t\treturn None", + "type": "script" + } + ], + "type": "property" + } + }, + "props.params.value": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\ttry:\n\t\treturn PrimeControls.fmt.num(value['kpis']['standing_count']['value'])\n\texcept:\n\t\treturn '--'", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "params": { + "label": "Standing" + }, + "path": "PrimeControls/Components/KpiCard" + }, + "type": "ia.display.view" + } + ], + "meta": { + "name": "rowStatus" + }, + "position": { + "basis": "104px", + "shrink": 0 + }, + "props": { + "alignItems": "stretch", + "style": { + "gap": "10px" + } + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "children": [ + { + "meta": { + "name": "lblRateTitle" + }, + "position": { + "shrink": 0 + }, + "props": { + "style": { + "classes": "PrimeControls/CardTitle" + }, + "text": "Alarm Rate Timeline" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "lblRateCaption" + }, + "position": { + "grow": 1 + }, + "props": { + "style": { + "classes": "PrimeControls/Text/Muted" + }, + "text": "flood bins in red \u00b7 dashed line = ISA target rate", + "textStyle": { + "textAlign": "right" + } + }, + "type": "ia.display.label" + } + ], + "meta": { + "name": "rateHeader" + }, + "position": { + "shrink": 0 + }, + "props": { + "alignItems": "center", + "style": { + "gap": "8px" + } + }, + "type": "ia.container.flex" + }, + { + "meta": { + "name": "chartRate" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\ttry:\n\t\treturn len(value['rate']['bins']) > 0\n\texcept:\n\t\treturn False", + "type": "script" + } + ], + "type": "property" + } + }, + "props.dataSources.rate": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\ttry:\n\t\trate = value['rate']\n\t\tbins = rate['bins'] or []\n\texcept:\n\t\treturn []\n\ttry:\n\t\ttarget = float(rate['target_per_bin'])\n\texcept:\n\t\ttarget = None\n\ttry:\n\t\tuse_day = float(value['meta']['window_hours']) > 24\n\texcept:\n\t\tuse_day = False\n\tout = []\n\tfor b in bins:\n\t\ttry:\n\t\t\tt0 = b['t0']\n\t\t\tc = int(b['count'])\n\t\texcept:\n\t\t\tcontinue\n\t\ttry:\n\t\t\tflood = bool(b['flood'])\n\t\texcept:\n\t\t\tflood = False\n\t\tif use_day:\n\t\t\tlbl = PrimeControls.fmt.day_clock(t0)\n\t\telse:\n\t\t\tlbl = PrimeControls.fmt.clock(t0)\n\t\trow = {'t': lbl, 'count_base': (0 if flood else c), 'count_flood': (c if flood else 0)}\n\t\tif target is not None:\n\t\t\trow['target'] = target\n\t\tout.append(row)\n\treturn out", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "background": { + "color": "", + "gradient": { + "colors": [], + "direction": "linear", + "rotation": 0 + }, + "opacity": 1, + "render": "none" + }, + "cursor": { + "enabled": true, + "lineX": { + "enabled": true, + "stroke": { + "color": "", + "dashArray": "", + "opacity": 0.4, + "width": 1 + } + }, + "lineY": { + "enabled": false, + "stroke": { + "color": "", + "dashArray": "", + "opacity": 0.4, + "width": 1 + } + }, + "series": "" + }, + "enableTransitions": false, + "legend": { + "absolute": { + "x": 0, + "y": 0 + }, + "enabled": true, + "labels": { + "font": { + "color": "", + "size": 12, + "weight": 400 + }, + "text": "[bold]{name}[/]" + }, + "markers": { + "cornerRadius": { + "bottomLeft": 0, + "bottomRight": 0, + "topLeft": 0, + "topRight": 0 + }, + "enabled": true, + "height": 12, + "icon": { + "color": "", + "height": 12, + "horizontalCenter": "left", + "path": "", + "verticalCenter": "top", + "width": 12 + }, + "mirrorLookOfSeries": true, + "stroke": { + "color": "", + "opacity": 0, + "width": 1 + }, + "width": 12 + }, + "position": "bottom" + }, + "scrollBars": { + "horizontal": { + "enabled": false, + "series": "" + }, + "vertical": { + "enabled": false, + "series": "" + } + }, + "series": [ + { + "candlestick": { + "appearance": { + "deriveFieldsFromData": { + "fill": { + "color": "", + "opacity": "" + }, + "stroke": { + "color": "", + "opacity": "", + "width": "" + } + }, + "fill": { + "color": "", + "opacity": 1 + }, + "heatRules": { + "dataField": "", + "enabled": false, + "max": "", + "min": "" + }, + "stacked": false, + "stroke": { + "color": "", + "opacity": 1, + "width": 1 + } + }, + "high": { + "x": "", + "y": "" + }, + "low": { + "x": "", + "y": "" + }, + "open": { + "x": "", + "y": "" + } + }, + "column": { + "appearance": { + "deriveFieldsFromData": { + "fill": { + "color": "", + "opacity": "" + }, + "stroke": { + "color": "", + "opacity": "", + "width": "" + } + }, + "fill": { + "color": "#3B7DD8", + "opacity": 1 + }, + "heatRules": { + "dataField": "", + "enabled": false, + "max": "", + "min": "" + }, + "height": null, + "stacked": true, + "stroke": { + "color": "", + "opacity": 0, + "width": 1 + }, + "width": 80 + }, + "open": { + "x": "", + "y": "" + } + }, + "data": { + "source": "rate", + "x": "t", + "y": "count_base" + }, + "defaultState": { + "visible": true + }, + "hiddenInLegend": false, + "label": { + "text": "Alarms" + }, + "line": { + "appearance": { + "bullets": [ + { + "deriveFieldsFromData": { + "fill": { + "color": "", + "opacity": "" + }, + "rotation": "", + "stroke": { + "color": "", + "opacity": "", + "width": "" + } + }, + "enabled": false, + "fill": { + "color": "", + "opacity": 1 + }, + "heatRules": { + "dataField": "", + "enabled": false, + "max": 100, + "min": 2 + }, + "label": { + "hideOversized": true, + "position": { + "dx": 0, + "dy": 0 + }, + "text": "{value}", + "truncate": true + }, + "radius": 5, + "render": "circle", + "rotation": 0, + "stroke": { + "color": "", + "opacity": 1, + "width": 1 + }, + "tooltip": { + "background": { + "color": "", + "opacity": 1 + }, + "cornerRadius": 3, + "enabled": true, + "getFillFromObject": true, + "pointerLength": 4, + "text": "{name}: [bold]{valueY}[/]" + } + } + ], + "connect": true, + "fill": { + "color": "", + "opacity": 0 + }, + "minDistance": 0.5, + "stroke": { + "color": "", + "dashArray": "", + "opacity": 1, + "width": 3 + }, + "tensionX": 1, + "tensionY": 1 + }, + "open": { + "x": "", + "y": "" + } + }, + "name": "counts", + "render": "column", + "stepLine": { + "appearance": { + "bullets": [ + { + "deriveFieldsFromData": { + "fill": { + "color": "", + "opacity": "" + }, + "rotation": "", + "stroke": { + "color": "", + "opacity": "", + "width": "" + } + }, + "enabled": false, + "fill": { + "color": "", + "opacity": 1 + }, + "heatRules": { + "dataField": "", + "enabled": false, + "max": 100, + "min": 2 + }, + "label": { + "hideOversized": true, + "position": { + "dx": 0, + "dy": 0 + }, + "text": "{value}", + "truncate": true + }, + "radius": 5, + "render": "circle", + "rotation": 0, + "stroke": { + "color": "", + "opacity": 1, + "width": 1 + }, + "tooltip": { + "background": { + "color": "", + "opacity": 1 + }, + "cornerRadius": 3, + "enabled": true, + "getFillFromObject": true, + "pointerLength": 4, + "text": "{name}: [bold]{valueY}[/]" + } + } + ], + "connect": true, + "fill": { + "color": "", + "opacity": 0 + }, + "minDistance": 0.5, + "stroke": { + "color": "", + "dashArray": "", + "opacity": 1, + "width": 3 + }, + "tensionX": 1, + "tensionY": 1 + }, + "open": { + "x": "", + "y": "" + } + }, + "tooltip": { + "background": { + "color": "", + "opacity": 1 + }, + "cornerRadius": 3, + "enabled": true, + "pointerLength": 4, + "text": "{categoryX}: [bold]{valueY}[/] alarms" + }, + "visible": true, + "xAxis": "time", + "yAxis": "count", + "zIndex": 0 + }, + { + "candlestick": { + "appearance": { + "deriveFieldsFromData": { + "fill": { + "color": "", + "opacity": "" + }, + "stroke": { + "color": "", + "opacity": "", + "width": "" + } + }, + "fill": { + "color": "", + "opacity": 1 + }, + "heatRules": { + "dataField": "", + "enabled": false, + "max": "", + "min": "" + }, + "stacked": false, + "stroke": { + "color": "", + "opacity": 1, + "width": 1 + } + }, + "high": { + "x": "", + "y": "" + }, + "low": { + "x": "", + "y": "" + }, + "open": { + "x": "", + "y": "" + } + }, + "column": { + "appearance": { + "deriveFieldsFromData": { + "fill": { + "color": "", + "opacity": "" + }, + "stroke": { + "color": "", + "opacity": "", + "width": "" + } + }, + "fill": { + "color": "#D64550", + "opacity": 1 + }, + "heatRules": { + "dataField": "", + "enabled": false, + "max": "", + "min": "" + }, + "height": null, + "stacked": true, + "stroke": { + "color": "", + "opacity": 0, + "width": 1 + }, + "width": 80 + }, + "open": { + "x": "", + "y": "" + } + }, + "data": { + "source": "rate", + "x": "t", + "y": "count_flood" + }, + "defaultState": { + "visible": true + }, + "hiddenInLegend": false, + "label": { + "text": "Flood bins" + }, + "line": { + "appearance": { + "bullets": [ + { + "deriveFieldsFromData": { + "fill": { + "color": "", + "opacity": "" + }, + "rotation": "", + "stroke": { + "color": "", + "opacity": "", + "width": "" + } + }, + "enabled": false, + "fill": { + "color": "", + "opacity": 1 + }, + "heatRules": { + "dataField": "", + "enabled": false, + "max": 100, + "min": 2 + }, + "label": { + "hideOversized": true, + "position": { + "dx": 0, + "dy": 0 + }, + "text": "{value}", + "truncate": true + }, + "radius": 5, + "render": "circle", + "rotation": 0, + "stroke": { + "color": "", + "opacity": 1, + "width": 1 + }, + "tooltip": { + "background": { + "color": "", + "opacity": 1 + }, + "cornerRadius": 3, + "enabled": true, + "getFillFromObject": true, + "pointerLength": 4, + "text": "{name}: [bold]{valueY}[/]" + } + } + ], + "connect": true, + "fill": { + "color": "", + "opacity": 0 + }, + "minDistance": 0.5, + "stroke": { + "color": "", + "dashArray": "", + "opacity": 1, + "width": 3 + }, + "tensionX": 1, + "tensionY": 1 + }, + "open": { + "x": "", + "y": "" + } + }, + "name": "flood", + "render": "column", + "stepLine": { + "appearance": { + "bullets": [ + { + "deriveFieldsFromData": { + "fill": { + "color": "", + "opacity": "" + }, + "rotation": "", + "stroke": { + "color": "", + "opacity": "", + "width": "" + } + }, + "enabled": false, + "fill": { + "color": "", + "opacity": 1 + }, + "heatRules": { + "dataField": "", + "enabled": false, + "max": 100, + "min": 2 + }, + "label": { + "hideOversized": true, + "position": { + "dx": 0, + "dy": 0 + }, + "text": "{value}", + "truncate": true + }, + "radius": 5, + "render": "circle", + "rotation": 0, + "stroke": { + "color": "", + "opacity": 1, + "width": 1 + }, + "tooltip": { + "background": { + "color": "", + "opacity": 1 + }, + "cornerRadius": 3, + "enabled": true, + "getFillFromObject": true, + "pointerLength": 4, + "text": "{name}: [bold]{valueY}[/]" + } + } + ], + "connect": true, + "fill": { + "color": "", + "opacity": 0 + }, + "minDistance": 0.5, + "stroke": { + "color": "", + "dashArray": "", + "opacity": 1, + "width": 3 + }, + "tensionX": 1, + "tensionY": 1 + }, + "open": { + "x": "", + "y": "" + } + }, + "tooltip": { + "background": { + "color": "", + "opacity": 1 + }, + "cornerRadius": 3, + "enabled": true, + "pointerLength": 4, + "text": "{categoryX}: [bold]{valueY}[/] alarms (flood)" + }, + "visible": true, + "xAxis": "time", + "yAxis": "count", + "zIndex": 1 + }, + { + "candlestick": { + "appearance": { + "deriveFieldsFromData": { + "fill": { + "color": "", + "opacity": "" + }, + "stroke": { + "color": "", + "opacity": "", + "width": "" + } + }, + "fill": { + "color": "", + "opacity": 1 + }, + "heatRules": { + "dataField": "", + "enabled": false, + "max": "", + "min": "" + }, + "stacked": false, + "stroke": { + "color": "", + "opacity": 1, + "width": 1 + } + }, + "high": { + "x": "", + "y": "" + }, + "low": { + "x": "", + "y": "" + }, + "open": { + "x": "", + "y": "" + } + }, + "column": { + "appearance": { + "deriveFieldsFromData": { + "fill": { + "color": "", + "opacity": "" + }, + "stroke": { + "color": "", + "opacity": "", + "width": "" + } + }, + "fill": { + "color": "", + "opacity": 1 + }, + "heatRules": { + "dataField": "", + "enabled": false, + "max": "", + "min": "" + }, + "height": null, + "stacked": false, + "stroke": { + "color": "", + "opacity": 0, + "width": 1 + }, + "width": 80 + }, + "open": { + "x": "", + "y": "" + } + }, + "data": { + "source": "rate", + "x": "t", + "y": "target" + }, + "defaultState": { + "visible": true + }, + "hiddenInLegend": false, + "label": { + "text": "ISA target" + }, + "line": { + "appearance": { + "bullets": [ + { + "deriveFieldsFromData": { + "fill": { + "color": "", + "opacity": "" + }, + "rotation": "", + "stroke": { + "color": "", + "opacity": "", + "width": "" + } + }, + "enabled": false, + "fill": { + "color": "", + "opacity": 1 + }, + "heatRules": { + "dataField": "", + "enabled": false, + "max": 100, + "min": 2 + }, + "label": { + "hideOversized": true, + "position": { + "dx": 0, + "dy": 0 + }, + "text": "{value}", + "truncate": true + }, + "radius": 5, + "render": "circle", + "rotation": 0, + "stroke": { + "color": "", + "opacity": 1, + "width": 1 + }, + "tooltip": { + "background": { + "color": "", + "opacity": 1 + }, + "cornerRadius": 3, + "enabled": true, + "getFillFromObject": true, + "pointerLength": 4, + "text": "{name}: [bold]{valueY}[/]" + } + } + ], + "connect": true, + "fill": { + "color": "", + "opacity": 0 + }, + "minDistance": 0.5, + "stroke": { + "color": "#E8883A", + "dashArray": "6,4", + "opacity": 1, + "width": 2 + }, + "tensionX": 1, + "tensionY": 1 + }, + "open": { + "x": "", + "y": "" + } + }, + "name": "target", + "render": "line", + "stepLine": { + "appearance": { + "bullets": [ + { + "deriveFieldsFromData": { + "fill": { + "color": "", + "opacity": "" + }, + "rotation": "", + "stroke": { + "color": "", + "opacity": "", + "width": "" + } + }, + "enabled": false, + "fill": { + "color": "", + "opacity": 1 + }, + "heatRules": { + "dataField": "", + "enabled": false, + "max": 100, + "min": 2 + }, + "label": { + "hideOversized": true, + "position": { + "dx": 0, + "dy": 0 + }, + "text": "{value}", + "truncate": true + }, + "radius": 5, + "render": "circle", + "rotation": 0, + "stroke": { + "color": "", + "opacity": 1, + "width": 1 + }, + "tooltip": { + "background": { + "color": "", + "opacity": 1 + }, + "cornerRadius": 3, + "enabled": true, + "getFillFromObject": true, + "pointerLength": 4, + "text": "{name}: [bold]{valueY}[/]" + } + } + ], + "connect": true, + "fill": { + "color": "", + "opacity": 0 + }, + "minDistance": 0.5, + "stroke": { + "color": "", + "dashArray": "", + "opacity": 1, + "width": 3 + }, + "tensionX": 1, + "tensionY": 1 + }, + "open": { + "x": "", + "y": "" + } + }, + "tooltip": { + "background": { + "color": "", + "opacity": 1 + }, + "cornerRadius": 3, + "enabled": false, + "pointerLength": 4, + "text": "" + }, + "visible": true, + "xAxis": "time", + "yAxis": "count", + "zIndex": 2 + } + ], + "style": { + "classes": "" + }, + "subtitle": { + "appearance": { + "color": "", + "font": { + "size": "", + "weight": 500 + }, + "padding": { + "bottom": 0, + "left": 0, + "right": 0, + "top": 0 + } + }, + "text": "" + }, + "title": { + "appearance": { + "color": "", + "font": { + "size": "", + "weight": 700 + }, + "padding": { + "bottom": 0, + "left": 0, + "right": 0, + "top": 0 + } + }, + "text": "" + }, + "xAxes": [ + { + "appearance": { + "font": { + "size": 11, + "weight": 500 + }, + "grid": { + "color": "", + "dashArray": "", + "minDistance": 140, + "opacity": 0, + "position": 0.5 + }, + "inside": false, + "labels": { + "color": "", + "horizontalCenter": "middle", + "opacity": 1, + "rotation": 0, + "verticalCenter": "top" + }, + "opposite": false + }, + "category": { + "break": { + "enabled": false, + "endCategory": "", + "size": 0.05, + "startCategory": "" + } + }, + "date": { + "baseInterval": { + "count": 1, + "enabled": false, + "skipEmptyPeriods": false, + "timeUnit": "hour" + }, + "break": { + "enabled": false, + "endDate": "", + "size": 0.05, + "startDate": "" + }, + "format": "M/d", + "inputFormat": "yyyy-MM-dd kk:mm:ss", + "range": { + "max": "", + "min": "", + "useStrict": false + } + }, + "inversed": false, + "label": { + "color": "", + "enabled": false, + "text": "" + }, + "name": "time", + "render": "category", + "tooltip": { + "background": { + "color": "", + "opacity": 1 + }, + "cornerRadius": 3, + "enabled": false, + "pointerLength": 4, + "text": "" + }, + "value": { + "break": { + "enabled": false, + "endValue": 100, + "size": 0.05, + "startValue": 0 + }, + "format": "#,###.##", + "logarithmic": false, + "range": { + "max": "", + "min": "", + "useStrict": false + } + }, + "visible": true + } + ], + "yAxes": [ + { + "appearance": { + "font": { + "size": 11, + "weight": 500 + }, + "grid": { + "color": "", + "dashArray": "3,3", + "minDistance": 30, + "opacity": 0.4, + "position": 0.5 + }, + "inside": false, + "labels": { + "color": "", + "horizontalCenter": "middle", + "opacity": 1, + "rotation": 0, + "verticalCenter": "middle" + }, + "opposite": false + }, + "category": { + "break": { + "enabled": false, + "endCategory": "", + "size": 0.05, + "startCategory": "" + } + }, + "date": { + "baseInterval": { + "count": 1, + "enabled": false, + "skipEmptyPeriods": false, + "timeUnit": "hour" + }, + "break": { + "enabled": false, + "endDate": "", + "size": 0.05, + "startDate": "" + }, + "format": "M/d", + "inputFormat": "yyyy-MM-dd kk:mm:ss", + "range": { + "max": "", + "min": "", + "useStrict": false + } + }, + "inversed": false, + "label": { + "color": "", + "enabled": true, + "text": "Alarms / 10 min" + }, + "name": "count", + "render": "value", + "tooltip": { + "background": { + "color": "", + "opacity": 1 + }, + "cornerRadius": 3, + "enabled": false, + "pointerLength": 4, + "text": "" + }, + "value": { + "break": { + "enabled": false, + "endValue": 100, + "size": 0.05, + "startValue": 0 + }, + "format": "#,###", + "logarithmic": false, + "range": { + "max": "", + "min": 0, + "useStrict": false + } + }, + "visible": true + } + ], + "dataSources": { + "rate": [] + } + }, + "type": "ia.chart.xy" + }, + { + "meta": { + "name": "emptyRate" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\ttry:\n\t\treturn len(value['rate']['bins']) == 0\n\texcept:\n\t\treturn True", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "params": { + "icon": "bar_chart", + "message": "No rate data in the selected range" + }, + "path": "PrimeControls/Components/EmptyState" + }, + "type": "ia.display.view" + } + ], + "meta": { + "name": "cardRate" + }, + "position": { + "basis": "0px", + "grow": 3 + }, + "props": { + "direction": "column", + "style": { + "classes": "PrimeControls/Card", + "gap": "6px", + "minHeight": "260px" + } + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "children": [ + { + "meta": { + "name": "lblInsightsTitle" + }, + "position": { + "shrink": 0 + }, + "props": { + "style": { + "classes": "PrimeControls/CardTitle" + }, + "text": "What Changed" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "repInsights" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "props.instances": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\tout = []\n\ttry:\n\t\trows = value['insights'] or []\n\texcept:\n\t\trows = []\n\tfor r in rows:\n\t\ttry:\n\t\t\tout.append({'severity': int(r['severity']), 'icon': r['icon'], 'text': r['text'], 'tab': r['tab']})\n\t\texcept:\n\t\t\tpass\n\treturn out", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "direction": "column", + "elementPosition": { + "basis": "32px", + "grow": 0, + "shrink": 0 + }, + "path": "PrimeControls/Components/InsightRow", + "style": { + "gap": "4px", + "overflow": "auto" + }, + "useDefaultViewHeight": false, + "useDefaultViewWidth": false + }, + "type": "ia.display.flex-repeater" + }, + { + "meta": { + "name": "emptyInsights" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\ttry:\n\t\treturn len(value['insights']) == 0\n\texcept:\n\t\treturn True", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "params": { + "icon": "insights", + "message": "No notable changes in this window" + }, + "path": "PrimeControls/Components/EmptyState" + }, + "type": "ia.display.view" + } + ], + "meta": { + "name": "cardInsights" + }, + "position": { + "basis": "0px", + "grow": 1 + }, + "props": { + "direction": "column", + "style": { + "classes": "PrimeControls/Card", + "gap": "6px" + } + }, + "type": "ia.container.flex" + }, + { + "children": [ + { + "meta": { + "name": "lblTopTitle" + }, + "position": { + "shrink": 0 + }, + "props": { + "style": { + "classes": "PrimeControls/CardTitle" + }, + "text": "Top 5 Sources" + }, + "type": "ia.display.label" + }, + { + "meta": { + "name": "repSources" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "props.instances": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\tout = []\n\ttry:\n\t\trows = value['top_sources'] or []\n\texcept:\n\t\trows = []\n\tfor r in rows:\n\t\ttry:\n\t\t\tout.append({'rank': int(r['rank']), 'source': r['source'], 'label': r['label'], 'count': int(r['count']), 'pct': float(r['pct'])})\n\t\texcept:\n\t\t\tpass\n\treturn out", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "direction": "column", + "elementPosition": { + "basis": "34px", + "grow": 0, + "shrink": 0 + }, + "path": "PrimeControls/Components/SourceRow", + "style": { + "gap": "4px", + "overflow": "auto" + }, + "useDefaultViewHeight": false, + "useDefaultViewWidth": false + }, + "type": "ia.display.flex-repeater" + }, + { + "meta": { + "name": "emptyTop" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\ttry:\n\t\treturn len(value['top_sources']) == 0\n\texcept:\n\t\treturn True", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "params": { + "icon": "leaderboard", + "message": "No alarm sources in this window" + }, + "path": "PrimeControls/Components/EmptyState" + }, + "type": "ia.display.view" + } + ], + "meta": { + "name": "cardTop" + }, + "position": { + "basis": "0px", + "grow": 1 + }, + "props": { + "direction": "column", + "style": { + "classes": "PrimeControls/Card", + "gap": "6px" + } + }, + "type": "ia.container.flex" + } + ], + "meta": { + "name": "rowBottom" + }, + "position": { + "basis": "0px", + "grow": 2 + }, + "props": { + "style": { + "gap": "12px", + "minHeight": "200px" + } + }, + "type": "ia.container.flex" + } + ], + "meta": { + "name": "content" + }, + "position": { + "grow": 1 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "path": "view.params.bundle" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\ttry:\n\t\treturn int(value['meta']['event_count']) > 0\n\texcept:\n\t\treturn False", + "type": "script" + } + ], + "type": "property" + } + } + }, + "props": { + "direction": "column", + "style": { + "gap": "12px" + } + }, + "type": "ia.container.flex" + } + ], + "meta": { + "name": "root" + }, + "props": { + "direction": "column", + "style": { + "gap": "12px" + } + }, + "type": "ia.container.flex" + } +} diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.vision/client-tags/data.bin b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.vision/client-tags/data.bin new file mode 100644 index 0000000000000000000000000000000000000000..0e2f305d86d6c418f3ad35adba6d19ff2bc39ffc GIT binary patch literal 342 zcmV-c0jd5UiwFP!00000|8-G4PXaL*ek&peh~Q?7!%ZeG&2dIV;)WrH7+v@SwG^zj z^t|>k{s`k@jGMcQiT^?ue~znfsd3V#&GX#*)#si*?cBfgzlLXTcOQqxkFW0lumnJ! z)#t+r{JmOuRskwjxKpM@=xBB6F5RbTl6I0#a*%W}T*E;eaTy?Lr;B`opDO@cql+RQ z7AkIcaQD)*x7UvQN2A#Hhg&M<{8DgdkwyR|t~{&bMD3G^25}8D*5$|=81WSZV11IK ztQ(VG(YeU2G&78s0Jgc&RL;f)cXBRR;f!Vuvve-)0)C5|s5v*rA*})o{_C4C?o580 zX-Qd@Dak|FUMt6@miCntGdE(>fl*`OMV4`%lUi8EbhZIO7LX`d2&E{P1?4E%1jH9r oi1(lx!~oe0{|`0aQjfx8K%+*2A3)+C;wLq~0RT+6G~fXM0L1j4;{X5v literal 0 HcmV?d00001 diff --git a/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.vision/client-tags/resource.json b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.vision/client-tags/resource.json new file mode 100644 index 0000000..68d8f0f --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/com.inductiveautomation.vision/client-tags/resource.json @@ -0,0 +1,16 @@ +{ + "scope": "C", + "version": 1, + "restricted": false, + "overridable": true, + "files": [ + "data.bin" + ], + "attributes": { + "lastModificationSignature": "bec66caddb4dc4262af703ea30d4054ff4fd98964ec2f9ab27680a735575894c", + "lastModification": { + "actor": "admin", + "timestamp": "2026-07-16T21:22:51Z" + } + } +} \ No newline at end of file diff --git a/ignition/gateway/projects/AlarmAnalysis/ignition/global-props/data.bin b/ignition/gateway/projects/AlarmAnalysis/ignition/global-props/data.bin new file mode 100644 index 0000000000000000000000000000000000000000..98e0fa08d0d7f2d7c9d0090ec453d7fdf967973f GIT binary patch literal 457 zcmV;)0XF_0iwFP!00000|BX`1PTN2fy$;VLJVKy=Dpl&Dn=a}fXg5GA9ttH0R&1&p zdy)){J&|V|iH|_+5WDW#An^mN;fL6ACgVbpyTJ1J-kE#OeVp<2*UOu&r@e!_i^m^3 zSGNyDR3QrfoZX*T`v0?rRU=wA!nQ<@b7fm>+2}lr#4k<+!iz}Jg`I?rJZnaGPeo)6 z+zF!ZkxoLH#(8ArNN{d-!dB`ul>JnW9RL!TVWu4|+*Z2Fs*~xV%g`X2>ajAyw>uFj zcREefL{l5N9RS#q>Tf$!Xd{7L0d$zVxdOZz_5?|V2X7H9V;3?#gn}=HL;fql{X5K{d-NEW2HqvohvX{0}l-m4D}29hZ6rdHCaiI|Bd!IBeb9 literal 0 HcmV?d00001 diff --git a/ignition/gateway/projects/AlarmAnalysis/ignition/global-props/resource.json b/ignition/gateway/projects/AlarmAnalysis/ignition/global-props/resource.json new file mode 100644 index 0000000..64f0add --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/ignition/global-props/resource.json @@ -0,0 +1,16 @@ +{ + "scope": "A", + "version": 1, + "restricted": false, + "overridable": true, + "files": [ + "data.bin" + ], + "attributes": { + "lastModificationSignature": "e32343f8745d6acdf441eeb5996366a03b8b8f43d6c91e30251d2889f5311492", + "lastModification": { + "actor": "admin", + "timestamp": "2026-07-16T16:25:30Z" + } + } +} \ No newline at end of file diff --git a/ignition/gateway/projects/AlarmAnalysis/ignition/script-python/PrimeControls/alarms/code.py b/ignition/gateway/projects/AlarmAnalysis/ignition/script-python/PrimeControls/alarms/code.py new file mode 100644 index 0000000..c84a252 --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/ignition/script-python/PrimeControls/alarms/code.py @@ -0,0 +1,402 @@ +from __future__ import division, print_function + +import sys + +from PrimeControls import calc, fmt + + +def _err_text(): + try: + return "%s" % (sys.exc_info()[1],) + except: + return "unknown error" + + +def _now_ms(): + try: + return system.date.toMillis(system.date.now()) + except: + import time + return int(time.time() * 1000) + + +def _ms_of(java_date): + try: + if java_date is None: + return None + return java_date.getTime() + except: + return None + + +def _norm_event(evt): + try: + source = str(evt.getSource() or "") + state = None + try: + if evt.getAckData() is not None: + state = "ack" + elif evt.getClearedData() is not None: + state = "clear" + elif evt.getActiveData() is not None: + state = "active" + except: + state = None + if state is None: + s = "" + try: + s = str(evt.getState() or "") + except: + pass + if s.startswith("Cleared"): + state = "clear" + elif "Acknowledged" in s and not s.startswith("Active,"): + state = "ack" + else: + state = "active" + ts = None + try: + ts = _ms_of(evt.get("eventTime")) + except: + ts = None + if ts is None: + return None + prio_raw = None + try: + p = evt.getPriority() + try: + prio_raw = int(p.ordinal()) + except: + prio_raw = str(p) + except: + prio_raw = None + lvl, name = calc.normalize_priority(prio_raw) + display_path = "" + try: + display_path = str(evt.getDisplayPath() or "") + except: + pass + is_system = source.startswith("evt:") or ":/alm:" not in source + try: + if evt.get("isSystemEvent"): + is_system = True + except: + pass + ack_user = None + try: + u = evt.get("ackUserName") + if u: + ack_user = str(u) + except: + pass + event_id = None + try: + event_id = str(evt.getId()) + except: + pass + return {"event_id": event_id, "source": source, "display_path": display_path, + "priority": lvl, "priority_name": name, "state": state, "ts": ts, + "is_system": is_system, "ack_user": ack_user} + except: + return None + + +def _fetch_journal(start_ms, end_ms, max_events, include_data=False): + """Returns (rows, dropped, truncated, error).""" + rows = [] + dropped = 0 + truncated = False + try: + kwargs = {"startDate": int(start_ms), "endDate": int(end_ms)} + if include_data: + # Required for ack attribution: without includeData the journal + # omits associated data, so evt.get("ackUserName") is always None. + kwargs["includeData"] = True + result = system.alarm.queryJournal(**kwargs) + for evt in result: + r = _norm_event(evt) + if r is None: + dropped += 1 + continue + rows.append(r) + if len(rows) >= max_events: + truncated = True + break + return rows, dropped, truncated, None + except: + return rows, dropped, truncated, _err_text() + + +def _fetch_status(): + out = [] + try: + result = system.alarm.queryStatus(state=["ActiveUnacked", "ActiveAcked"]) + for evt in result: + try: + source = str(evt.getSource() or "") + if source.startswith("evt:") or ":/alm:" not in source: + continue + active_ms = None + try: + data = evt.getActiveData() + if data is not None: + active_ms = _ms_of(data.get("eventTime")) + except: + active_ms = None + if active_ms is None: + try: + active_ms = _ms_of(evt.get("eventTime")) + except: + pass + prio_raw = None + try: + p = evt.getPriority() + try: + prio_raw = int(p.ordinal()) + except: + prio_raw = str(p) + except: + pass + lvl, name = calc.normalize_priority(prio_raw) + unacked = False + try: + unacked = "Unacknowledged" in str(evt.getState() or "") + except: + pass + display_path = "" + try: + display_path = str(evt.getDisplayPath() or "") + except: + pass + out.append({"source": source, "display_path": display_path, + "priority": lvl, "priority_name": name, + "active_ms": active_ms, "unacked": unacked}) + except: + continue + except: + pass + return out + + +def _fetch_shelved(): + try: + paths = system.alarm.getShelvedPaths() + return len(list(paths)) if paths is not None else 0 + except: + return None + + +def _clean_filters(options): + f = {"priorities": [], "areas": [], "states": [], "search": ""} + if not options: + return f + try: + for k in ("priorities", "areas", "states"): + v = options.get(k) + if v: + f[k] = [x for x in list(v)] + f["priorities"] = [int(x) for x in f["priorities"]] + f["areas"] = [str(x) for x in f["areas"]] + f["states"] = [str(x) for x in f["states"]] + f["search"] = str(options.get("search") or "") + except: + pass + return f + + +# ---------------- Perspective entry points ---------------- + + +def getDashboardBundle(startMs, endMs, options=None): + try: + s = int(startMs) + e = int(endMs) + opts = calc.opts_merged(None) + filters = _clean_filters(options) + span = max(e - s, 1) + rows, dropped, truncated, err = _fetch_journal( + s - span, e, int(opts["max_events"])) + active_now = _fetch_status() + shelved = _fetch_shelved() + bundle = calc.build_bundle(rows, active_now, shelved, s, e, _now_ms(), + {"filters": filters}) + bundle["meta"]["dropped_rows"] = dropped + bundle["meta"]["truncated"] = truncated + if err: + bundle["meta"]["error"] = err + bundle["insights"] = fmt.insights(bundle) + try: + if bundle.get("overview"): + bundle["overview"]["insights"] = fmt.insights(bundle["overview"]) + except: + pass + return bundle + except: + try: + bundle = calc.build_bundle([], [], None, int(startMs or 0), + int(endMs or 0), _now_ms(), None) + except: + bundle = calc.build_bundle([], [], None, 0, 0, 0, None) + bundle["meta"]["error"] = _err_text() + bundle["insights"] = [] + return bundle + + +STATE_LABELS = {"active": "Active", "clear": "Cleared", "ack": "Acked"} + + +def _journal_rows(start_ms, end_ms, filters, max_rows): + rows, dropped, truncated, err = _fetch_journal(start_ms, end_ms, max_rows, + include_data=True) + out = [] + prios = set(filters.get("priorities") or []) + areas = set(filters.get("areas") or []) + states = set(filters.get("states") or []) + search = (filters.get("search") or "").strip().lower() + for r in rows: + if r["is_system"]: + continue + parsed = calc.parse_source(r["source"], r["display_path"]) + if prios and r["priority"] not in prios: + continue + if areas and parsed["area"] not in areas: + continue + if states: + tag = {"active": "active", "clear": "cleared", "ack": "acked"}[r["state"]] + if tag not in states and not ("unacked" in states and r["state"] == "active"): + continue + if search and search not in (r["source"] + " " + parsed["label"]).lower(): + continue + out.append({"id": r["event_id"], "time_ms": r["ts"], + "time_label": fmt.day_clock(r["ts"]), + "source": r["source"], "label": parsed["label"], + "area": parsed["area"], "state": r["state"], + "state_label": STATE_LABELS.get(r["state"], r["state"]), + "priority": r["priority"], "priority_name": r["priority_name"], + "ack_user": r["ack_user"] or ""}) + out.sort(key=lambda r: -(r["time_ms"] or 0)) + return out, truncated, err + + +def getJournalPage(startMs, endMs, filters=None, page=0, pageSize=50): + try: + f = _clean_filters(filters) + rows, truncated, err = _journal_rows(int(startMs), int(endMs), f, + int(calc.DEFAULTS["max_events"])) + page = max(0, int(page or 0)) + page_size = max(1, int(pageSize or 50)) + start = page * page_size + return {"rows": rows[start:start + page_size], "total": len(rows), + "page": page, "page_size": page_size, "truncated": truncated, + "error": err} + except: + return {"rows": [], "total": 0, "page": 0, "page_size": int(pageSize or 50), + "truncated": False, "error": _err_text()} + + +def journalCsv(startMs, endMs, filters=None, maxRows=10000): + try: + f = _clean_filters(filters) + rows, truncated, err = _journal_rows(int(startMs), int(endMs), f, int(maxRows)) + headers = ["time", "state", "priority", "label", "area", "source", "ack_user"] + data = [[r["time_label"], r["state_label"], r["priority_name"], r["label"], + r["area"], r["source"], r["ack_user"]] for r in rows] + ds = system.dataset.toDataSet(headers, data) + return system.dataset.toCSV(ds) + except: + return "error,%s" % _err_text().replace(",", ";") + + +def getSourceDetail(source, startMs, endMs): + try: + s = int(startMs) + e = int(endMs) + src = str(source or "") + rows, dropped, truncated, err = _fetch_journal(s, e, int(calc.DEFAULTS["max_events"]), + include_data=True) + mine = [r for r in rows if r["source"] == src] + instances, _mode = calc.correlate_instances(mine) + parsed = calc.parse_source(src, mine[0]["display_path"] if mine else "") + day_bins = calc.rate_bins(instances, s, e, {"bin_ms": 86400000, "max_bins": 400}) + daily = [{"t0": b["t0"], "count": b["count"]} for b in day_bins["bins"]] + ttas = [i["tta_ms"] for i in instances if i.get("tta_ms") is not None] + durs = [i["duration_ms"] for i in instances if i.get("duration_ms") is not None] + fleet = len([i for i in instances + if i.get("duration_ms") is not None + and i["duration_ms"] < calc.DEFAULTS["fleeting_s"] * 1000]) + ack_users = {} + for r in mine: + if r["state"] == "ack" and r.get("ack_user"): + ack_users[r["ack_user"]] = ack_users.get(r["ack_user"], 0) + 1 + top_ack = [{"user": u, "count": c} for u, c in + sorted(ack_users.items(), key=lambda kv: -kv[1])[:5]] + events = [] + for r in sorted(mine, key=lambda r: -r["ts"])[:100]: + events.append({"id": r["event_id"], "time_ms": r["ts"], + "time_label": fmt.day_clock(r["ts"]), + "state_label": STATE_LABELS.get(r["state"], r["state"]), + "priority_name": r["priority_name"], + "ack_user": r["ack_user"] or ""}) + return {"label": parsed["label"], "area": parsed["area"], "events": events, + "daily": daily, + "stats": {"count": len([i for i in instances if i["active_ms"] is not None]), + "avg_tta_ms": (sum(ttas) / len(ttas)) if ttas else None, + "avg_active_ms": (sum(durs) / len(durs)) if durs else None, + "fleeting_count": fleet, "top_ack_users": top_ack}, + "error": err} + except: + return {"label": str(source or ""), "area": "", "events": [], "daily": [], + "stats": {"count": 0, "avg_tta_ms": None, "avg_active_ms": None, + "fleeting_count": 0, "top_ack_users": []}, + "error": _err_text()} + + +def getEventData(eventId, aroundMs, source): + try: + eid = str(eventId or "") + mid = int(aroundMs) + props = [] + seen = {} + try: + result = system.alarm.queryJournal(startDate=mid - 3600000, + endDate=mid + 3600000, + includeData=True) + except: + result = system.alarm.queryJournal(startDate=mid - 3600000, + endDate=mid + 3600000) + for evt in result: + try: + if str(evt.getId()) != eid: + continue + try: + names = evt.getProperties() + except: + names = [] + for p in names or []: + try: + n = str(p) + v = evt.get(p) + if n not in seen: + seen[n] = True + props.append({"name": n, "value": str(v)}) + except: + continue + except: + continue + props.sort(key=lambda kv: kv["name"]) + return {"props": props, "error": None} + except: + return {"props": [], "error": _err_text()} + + +def getShiftReportText(shiftHours=8, anchorHour=0, options=None): + try: + bounds = fmt.shift_bounds(_now_ms(), int(shiftHours), int(anchorHour)) + s, e = bounds["previous"] + bundle = getDashboardBundle(s, e, options) + report = fmt.shift_report(bundle, bounds["label"]) + return {"text": report["text"], "start_ms": s, "end_ms": e, + "label": bounds["label"]} + except: + return {"text": "Shift report unavailable: %s" % _err_text(), + "start_ms": 0, "end_ms": 0, "label": ""} diff --git a/ignition/gateway/projects/AlarmAnalysis/ignition/script-python/PrimeControls/alarms/resource.json b/ignition/gateway/projects/AlarmAnalysis/ignition/script-python/PrimeControls/alarms/resource.json new file mode 100644 index 0000000..11225a4 --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/ignition/script-python/PrimeControls/alarms/resource.json @@ -0,0 +1,17 @@ +{ + "scope": "A", + "version": 1, + "restricted": false, + "overridable": true, + "files": [ + "code.py" + ], + "attributes": { + "hintScope": 2, + "lastModificationSignature": "ea536367797dbf88597c914b522bb3a79c7fa959ae200f6b12fcbb135126cefa", + "lastModification": { + "actor": "admin", + "timestamp": "2026-07-16T21:23:55Z" + } + } +} \ No newline at end of file diff --git a/ignition/gateway/projects/AlarmAnalysis/ignition/script-python/PrimeControls/calc/code.py b/ignition/gateway/projects/AlarmAnalysis/ignition/script-python/PrimeControls/calc/code.py new file mode 100644 index 0000000..61733b8 --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/ignition/script-python/PrimeControls/calc/code.py @@ -0,0 +1,807 @@ +from __future__ import division, print_function + +DEFAULTS = { + "bin_ms": 600000, "flood_per_10min": 10, "flood_end_per_10min": 5, + "target_per_hr": 6.0, + "chatter_per_hr": 10.0, "chatter_gap_s": 120, "chatter_min_count": 5, + "fleeting_s": 10, "fleeting_min_count": 3, + "standing_hours": 24, "trend_buckets": 24, + "max_events": 50000, "max_bins": 5000, + "delta_flat_pct": 5.0, "insights_max": 5, + "priority_target": {"low": 80.0, "medium": 15.0, "high": 5.0}, + "health_weights": {"rate": 0.30, "flood": 0.25, "chatter": 0.20, + "standing": 0.15, "priority": 0.10}, + "grade_cuts": [(90, "A"), (80, "B"), (70, "C"), (60, "D"), (0, "F")], + "list_cap": 50, "active_now_cap": 200, +} + +PRIORITY_NAMES = {0: "Diagnostic", 1: "Low", 2: "Medium", 3: "High", 4: "Critical"} + + +def opts_merged(opts): + out = dict(DEFAULTS) + if opts: + for k in opts: + out[k] = opts[k] + return out + + +def clamp(v, lo, hi): + return max(lo, min(hi, v)) + + +def _median(values): + vs = sorted(v for v in values if v is not None) + n = len(vs) + if n == 0: + return None + if n % 2: + return float(vs[n // 2]) + return (vs[n // 2 - 1] + vs[n // 2]) / 2.0 + + +def _mean(values): + vs = [v for v in values if v is not None] + if not vs: + return None + return sum(vs) / float(len(vs)) + + +def _safe_div(a, b): + try: + if not b: + return None + return a / float(b) + except Exception: + return None + + +# ---------------- path parsing ---------------- + + +def parse_source(source, display_path): + source = source or "" + display_path = display_path or "" + tag_segments = [] + alarm_name = "" + if ":/tag:" in source: + tag_part = source.split(":/tag:", 1)[1] + tag_path = tag_part.split(":", 1)[0] + tag_segments = [s for s in tag_path.split("/") if s] + if ":/alm:" in source: + alarm_name = source.split(":/alm:", 1)[1].split(":", 1)[0] + if display_path: + segments = [s for s in display_path.split("/") if s] + elif tag_segments: + segments = list(tag_segments) + if alarm_name and (not segments or alarm_name.lower() != segments[-1].lower()): + segments.append(alarm_name) + else: + segments = [source] if source else ["unknown"] + if len(segments) >= 2: + label = "/".join(segments[-2:]) + else: + label = segments[0] + base = tag_segments if tag_segments else segments + if len(base) >= 2: + area = base[-2] + else: + area = base[0] if base else "unknown" + return {"label": label, "area": area, "segments": segments} + + +def normalize_priority(value): + try: + iv = int(value) + if 0 <= iv <= 4: + return iv, PRIORITY_NAMES[iv] + except Exception: + pass + s = str(value).strip().lower() if value is not None else "" + for lvl in PRIORITY_NAMES: + if PRIORITY_NAMES[lvl].lower() in s and s: + return lvl, PRIORITY_NAMES[lvl] + return -1, (str(value) if value is not None else "Unknown") + + +# ---------------- correlation ---------------- + + +def _new_instance(row): + lvl, name = row.get("priority", -1), row.get("priority_name", "Unknown") + parsed = parse_source(row.get("source"), row.get("display_path")) + return {"event_id": row.get("event_id"), "source": row.get("source") or "", + "label": parsed["label"], "area": parsed["area"], + "priority": lvl, "priority_name": name, + "active_ms": None, "clear_ms": None, "ack_ms": None, + "duration_ms": None, "tta_ms": None, "open": True, + "ack_user": None, "n_events": 0} + + +def _finalize(inst): + a, c, k = inst["active_ms"], inst["clear_ms"], inst["ack_ms"] + inst["duration_ms"] = (c - a) if (a is not None and c is not None and c >= a) else None + inst["tta_ms"] = (k - a) if (a is not None and k is not None and k >= a) else None + inst["open"] = c is None + return inst + + +def correlate_instances(rows): + rows = [r for r in (rows or []) if r and not r.get("is_system") and r.get("ts") is not None] + rows.sort(key=lambda r: r["ts"]) + if not rows: + return [], "uuid" + with_id = [r for r in rows if r.get("event_id")] + mode = "uuid" if len(with_id) >= 0.9 * len(rows) else "source-sequence" + instances = [] + if mode == "uuid": + groups, order = {}, [] + for i, r in enumerate(rows): + k = r.get("event_id") or ("_noid_%d" % i) + if k not in groups: + groups[k] = [] + order.append(k) + groups[k].append(r) + for k in order: + grp = groups[k] + ident = None + for r in grp: + if r["state"] == "active": + ident = r + break + inst = _new_instance(ident or grp[0]) + inst["n_events"] = len(grp) + acts = [r["ts"] for r in grp if r["state"] == "active"] + clears = [r["ts"] for r in grp if r["state"] == "clear"] + acks = [r["ts"] for r in grp if r["state"] == "ack"] + inst["active_ms"] = min(acts) if acts else None + inst["clear_ms"] = min(clears) if clears else None + inst["ack_ms"] = min(acks) if acks else None + for r in grp: + if r.get("ack_user"): + inst["ack_user"] = r["ack_user"] + break + instances.append(_finalize(inst)) + else: + open_by_src = {} + for r in rows: + src = r.get("source") or "" + st = r.get("state") + if st == "active": + if src in open_by_src: + instances.append(_finalize(open_by_src.pop(src))) + inst = _new_instance(r) + inst["active_ms"] = r["ts"] + inst["n_events"] = 1 + open_by_src[src] = inst + else: + inst = open_by_src.get(src) + if inst is None: + inst = _new_instance(r) + open_by_src[src] = inst + inst["n_events"] = inst.get("n_events", 0) + 1 + if st == "clear" and inst["clear_ms"] is None: + inst["clear_ms"] = r["ts"] + elif st == "ack" and inst["ack_ms"] is None: + inst["ack_ms"] = r["ts"] + if r.get("ack_user"): + inst["ack_user"] = r["ack_user"] + for src in open_by_src: + instances.append(_finalize(open_by_src[src])) + instances.sort(key=lambda i: i["active_ms"] if i["active_ms"] is not None else 0) + return instances, mode + + +def partition_instances(instances, start_ms, end_ms): + prior_start = start_ms - (end_ms - start_ms) + current, prior, orphans = [], [], [] + for inst in instances or []: + a = inst.get("active_ms") + if a is None: + orphans.append(inst) + elif start_ms <= a <= end_ms: + current.append(inst) + elif prior_start <= a < start_ms: + prior.append(inst) + else: + orphans.append(inst) + return current, prior, orphans + + +def apply_filters(instances, filters): + if not filters: + return instances + prios = set(filters.get("priorities") or []) + areas = set(filters.get("areas") or []) + states = set(filters.get("states") or []) + search = (filters.get("search") or "").strip().lower() + if not prios and not areas and not states and not search: + return instances + out = [] + for inst in instances: + if prios and inst["priority"] not in prios: + continue + if areas and inst["area"] not in areas: + continue + if states: + tags = set() + if inst["open"]: + tags.add("active") + if inst["clear_ms"] is not None: + tags.add("cleared") + if inst["ack_ms"] is not None: + tags.add("acked") + else: + tags.add("unacked") + if not (states & tags): + continue + if search: + hay = (inst["source"] + " " + inst["label"]).lower() + if search not in hay: + continue + out.append(inst) + return out + + +def apply_status_filters(status_rows, filters): + """Same filter semantics as apply_filters, but for queryStatus rows + (active-now list): keys source/display_path/priority/unacked.""" + if not filters: + return status_rows + prios = set(filters.get("priorities") or []) + areas = set(filters.get("areas") or []) + states = set(filters.get("states") or []) + search = (filters.get("search") or "").strip().lower() + if not prios and not areas and not states and not search: + return status_rows + out = [] + for s in status_rows or []: + if prios and s.get("priority") not in prios: + continue + parsed = parse_source(s.get("source"), s.get("display_path")) + if areas and parsed["area"] not in areas: + continue + if states: + tags = set(["active"]) + tags.add("unacked" if s.get("unacked") else "acked") + if not (states & tags): + continue + if search: + hay = ((s.get("source") or "") + " " + parsed["label"]).lower() + if search not in hay: + continue + out.append(s) + return out + + +# ---------------- rate / floods ---------------- + + +def rate_bins(instances, start_ms, end_ms, opts=None): + o = opts_merged(opts) + bin_ms = int(o["bin_ms"]) + empty = {"bins": [], "bin_ms": bin_ms, + "target_per_bin": o["target_per_hr"] * bin_ms / 3600000.0, + "flood_threshold": 1, "max_count": 0} + if end_ms is None or start_ms is None or end_ms <= start_ms: + return empty + while (end_ms - start_ms) // bin_ms + 1 > o["max_bins"]: + bin_ms *= 2 + origin = (start_ms // bin_ms) * bin_ms + n = int((end_ms - origin) // bin_ms) + 1 + counts = {} + for inst in instances or []: + a = inst.get("active_ms") + if a is None or a < origin or a > end_ms: + continue + idx = int((a - origin) // bin_ms) + counts[idx] = counts.get(idx, 0) + 1 + flood_threshold = max(1, int(round(o["flood_per_10min"] * bin_ms / 600000.0))) + bins = [] + max_count = 0 + for i in range(n): + c = counts.get(i, 0) + max_count = max(max_count, c) + bins.append({"t0": origin + i * bin_ms, "t1": origin + (i + 1) * bin_ms, + "count": c, "flood": c > flood_threshold}) + return {"bins": bins, "bin_ms": bin_ms, + "target_per_bin": o["target_per_hr"] * bin_ms / 3600000.0, + "flood_threshold": flood_threshold, "max_count": max_count} + + +def flood_episodes(bins_result, instances, start_ms, end_ms, opts=None): + o = opts_merged(opts) + bins = (bins_result or {}).get("bins") or [] + bin_ms = (bins_result or {}).get("bin_ms") or o["bin_ms"] + thr = (bins_result or {}).get("flood_threshold") or 1 + end_thr = max(1, int(round(o["flood_end_per_10min"] * bin_ms / 600000.0))) + episodes = [] + cur = None + for b in bins: + if cur is None: + if b["count"] > thr: + cur = {"start_ms": b["t0"], "end_ms": b["t1"], "peak_bin_count": b["count"]} + else: + if b["count"] >= end_thr: + cur["end_ms"] = b["t1"] + cur["peak_bin_count"] = max(cur["peak_bin_count"], b["count"]) + else: + episodes.append(cur) + cur = None + if cur is not None: + episodes.append(cur) + for ep in episodes: + ep["duration_ms"] = ep["end_ms"] - ep["start_ms"] + src_counts = {} + area_counts = {} + total = 0 + for inst in instances or []: + a = inst.get("active_ms") + if a is not None and ep["start_ms"] <= a < ep["end_ms"]: + total += 1 + key = (inst["source"], inst["label"]) + src_counts[key] = src_counts.get(key, 0) + 1 + area_counts[inst["area"]] = area_counts.get(inst["area"], 0) + 1 + ep["event_count"] = total + if src_counts: + best = sorted(src_counts.items(), key=lambda kv: (-kv[1], kv[0][1]))[0] + ep["top_source_label"] = best[0][1] + ep["top_source_count"] = best[1] + else: + ep["top_source_label"] = "" + ep["top_source_count"] = 0 + if area_counts: + ep["top_area"] = sorted(area_counts.items(), key=lambda kv: (-kv[1], kv[0]))[0][0] + else: + ep["top_area"] = "" + span = (end_ms - start_ms) if (end_ms and start_ms and end_ms > start_ms) else 0 + pct = 0.0 + if span: + pct = 100.0 * sum(ep["duration_ms"] for ep in episodes) / span + return {"episodes": episodes, "pct_time_in_flood": pct} + + +# ---------------- detections ---------------- + + +def chattering(instances, start_ms, end_ms, opts=None): + o = opts_merged(opts) + window_h = _safe_div(end_ms - start_ms, 3600000.0) if (end_ms and start_ms) else None + if not window_h or window_h <= 0: + return [] + groups = {} + for inst in instances or []: + a = inst.get("active_ms") + if a is None: + continue + groups.setdefault(inst["source"], []).append(inst) + out = [] + for src in groups: + insts = groups[src] + count = len(insts) + if count < o["chatter_min_count"]: + continue + per_hour = count / window_h + times = sorted(i["active_ms"] for i in insts) + gaps = [times[i + 1] - times[i] for i in range(len(times) - 1)] + med_gap = _median(gaps) + if per_hour > o["chatter_per_hr"] and med_gap is not None and med_gap <= o["chatter_gap_s"] * 1000: + ref = insts[0] + out.append({"source": src, "label": ref["label"], "area": ref["area"], + "priority_name": ref["priority_name"], "count": count, + "per_hour": per_hour, "median_gap_s": med_gap / 1000.0}) + out.sort(key=lambda r: (-r["per_hour"], r["label"])) + return out[:int(o["list_cap"])] + + +def fleeting(instances, opts=None): + o = opts_merged(opts) + thresh = o["fleeting_s"] * 1000 + groups = {} + total = 0 + for inst in instances or []: + d = inst.get("duration_ms") + if d is not None and d < thresh: + total += 1 + groups.setdefault(inst["source"], []).append(inst) + sources = [] + for src in groups: + insts = groups[src] + if len(insts) < o["fleeting_min_count"]: + continue + ref = insts[0] + sources.append({"source": src, "label": ref["label"], "area": ref["area"], + "priority_name": ref["priority_name"], "count": len(insts), + "median_s": (_median([i["duration_ms"] for i in insts]) or 0) / 1000.0}) + sources.sort(key=lambda r: (-r["count"], r["label"])) + return {"total": total, "sources": sources[:int(o["list_cap"])]} + + +def standing(active_now, open_instances, now_ms, opts=None): + o = opts_merged(opts) + thresh = o["standing_hours"] * 3600000.0 + rows = [] + if active_now: + mode = "status" + for s in active_now: + a = s.get("active_ms") + age = (now_ms - a) if (a is not None and now_ms) else None + parsed = parse_source(s.get("source"), s.get("display_path")) + rows.append({"source": s.get("source") or "", "label": parsed["label"], + "area": parsed["area"], + "priority_name": s.get("priority_name", "Unknown"), + "active_ms": a, "age_ms": age, + "age_h": (age / 3600000.0) if age is not None else None, + "unacked": bool(s.get("unacked"))}) + else: + mode = "journal" + for inst in open_instances or []: + a = inst.get("active_ms") + age = (now_ms - a) if (a is not None and now_ms) else None + rows.append({"source": inst["source"], "label": inst["label"], + "area": inst["area"], "priority_name": inst["priority_name"], + "active_ms": a, "age_ms": age, + "age_h": (age / 3600000.0) if age is not None else None, + "unacked": inst.get("ack_ms") is None}) + rows.sort(key=lambda r: -(r["age_ms"] or -1)) + count = len([r for r in rows if r["age_ms"] is not None and r["age_ms"] > thresh]) + return {"mode": mode, "count": count, "rows": rows[:int(o["list_cap"])]} + + +# ---------------- MTTA / MTTR ---------------- + + +def mtta_mttr(instances, start_ms, end_ms, opts=None): + o = opts_merged(opts) + ttas = [i["tta_ms"] for i in instances or [] if i.get("tta_ms") is not None and i["tta_ms"] >= 0] + durs = [i["duration_ms"] for i in instances or [] if i.get("duration_ms") is not None and i["duration_ms"] >= 0] + result = {"mtta": {"mean_ms": _mean(ttas), "median_ms": _median(ttas), "count": len(ttas)}, + "mttr": {"mean_ms": _mean(durs), "median_ms": _median(durs), "count": len(durs)}, + "trend": []} + if not end_ms or not start_ms or end_ms <= start_ms: + return result + span = end_ms - start_ms + bin_ms = int(o["bin_ms"]) + width = max(bin_ms, ((span // int(o["trend_buckets"])) // bin_ms + 1) * bin_ms) + origin = (start_ms // width) * width + n = int((end_ms - origin) // width) + 1 + buckets = {} + for inst in instances or []: + a = inst.get("active_ms") + if a is None: + continue + idx = int((a - origin) // width) + b = buckets.setdefault(idx, {"tta": [], "dur": []}) + if inst.get("tta_ms") is not None and inst["tta_ms"] >= 0: + b["tta"].append(inst["tta_ms"]) + if inst.get("duration_ms") is not None and inst["duration_ms"] >= 0: + b["dur"].append(inst["duration_ms"]) + for i in range(n): + b = buckets.get(i, {"tta": [], "dur": []}) + result["trend"].append({"t0": origin + i * width, + "mtta_ms": _median(b["tta"]), + "mttr_ms": _median(b["dur"]), + "ack_n": len(b["tta"]), "clear_n": len(b["dur"])}) + return result + + +# ---------------- pareto / heatmap / priority ---------------- + + +def pareto(instances, opts=None): + counts = {} + refs = {} + total = 0 + for inst in instances or []: + if inst.get("active_ms") is None: + continue + total += 1 + src = inst["source"] + counts[src] = counts.get(src, 0) + 1 + if src not in refs: + refs[src] = inst + ordered = sorted(counts.items(), key=lambda kv: (-kv[1], refs[kv[0]]["label"]))[:10] + rows = [] + cum = 0 + for i, (src, c) in enumerate(ordered): + cum += c + ref = refs[src] + rows.append({"rank": i + 1, "source": src, "label": ref["label"], + "area": ref["area"], "priority_name": ref["priority_name"], + "count": c, "pct": 100.0 * c / total if total else 0.0, + "cum_pct": 100.0 * cum / total if total else 0.0}) + return {"total_activations": total, "rows": rows} + + +def top_sources(pareto_result, n=5): + return list((pareto_result or {}).get("rows") or [])[:n] + + +def heatmap(instances, opts=None): + import time as _time + rows = [[0] * 24 for _ in range(7)] + total = 0 + mx = 0 + for inst in instances or []: + a = inst.get("active_ms") + if a is None: + continue + lt = _time.localtime(a / 1000.0) + rows[lt.tm_wday][lt.tm_hour] += 1 + total += 1 + if rows[lt.tm_wday][lt.tm_hour] > mx: + mx = rows[lt.tm_wday][lt.tm_hour] + return {"rows": rows, "row_labels": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"], + "max_count": mx, "total": total} + + +def priority_distribution(instances, opts=None): + o = opts_merged(opts) + raw_counts = {} + buckets = {"low": 0, "medium": 0, "high": 0, "other": 0} + for inst in instances or []: + lvl = inst.get("priority", -1) + name = inst.get("priority_name", "Unknown") + key = (lvl, name) + raw_counts[key] = raw_counts.get(key, 0) + 1 + if lvl in (0, 1): + buckets["low"] += 1 + elif lvl == 2: + buckets["medium"] += 1 + elif lvl in (3, 4): + buckets["high"] += 1 + else: + buckets["other"] += 1 + raw = [{"name": k[1], "level": k[0], "count": raw_counts[k]} + for k in sorted(raw_counts.keys())] + bucketed = buckets["low"] + buckets["medium"] + buckets["high"] + pct = {"low": 0.0, "medium": 0.0, "high": 0.0} + sum_abs_dev = None + target = o["priority_target"] + if bucketed: + for k in pct: + pct[k] = 100.0 * buckets[k] / bucketed + sum_abs_dev = (abs(pct["low"] - target["low"]) + + abs(pct["medium"] - target["medium"]) + + abs(pct["high"] - target["high"])) + return {"raw": raw, "buckets": buckets, "pct": pct, + "target": dict(target), "sum_abs_dev": sum_abs_dev} + + +# ---------------- health / deltas ---------------- + + +def _piecewise(x, points): + if x is None: + return None + pts = sorted(points) + if x <= pts[0][0]: + return float(pts[0][1]) + if x >= pts[-1][0]: + return float(pts[-1][1]) + for i in range(len(pts) - 1): + x0, y0 = pts[i] + x1, y1 = pts[i + 1] + if x0 <= x <= x1: + if x1 == x0: + return float(y1) + return y0 + (y1 - y0) * (x - x0) / float(x1 - x0) + return float(pts[-1][1]) + + +def health_score(rate_per_hr, pct_flood, chatter_count, standing_count, + sum_abs_dev, has_activity, opts=None): + o = opts_merged(opts) + if not has_activity: + rate_s, flood_s, chatter_s = 100.0, 100.0, 100.0 + else: + rate_s = _piecewise(rate_per_hr, [(6, 100), (12, 60), (60, 0)]) + flood_s = _piecewise(pct_flood, [(0, 100), (1, 80), (5, 0)]) + chatter_s = clamp(100.0 - 25.0 * chatter_count, 0, 100) if chatter_count is not None else None + standing_s = clamp(100.0 - 20.0 * standing_count, 0, 100) if standing_count is not None else None + priority_s = clamp(100.0 - sum_abs_dev, 0, 100) if sum_abs_dev is not None else None + weights = o["health_weights"] + subs = [ + {"key": "rate", "label": "Alarm Rate", "score": rate_s, "weight": weights["rate"], + "detail": ("%.1f alarms/hr vs %.0f/hr ISA target" % (rate_per_hr, o["target_per_hr"])) + if rate_per_hr is not None else "no data"}, + {"key": "flood", "label": "Time in Flood", "score": flood_s, "weight": weights["flood"], + "detail": ("%.1f%% of window in flood (target <1%%)" % pct_flood) + if pct_flood is not None else "no data"}, + {"key": "chatter", "label": "Chattering Alarms", "score": chatter_s, "weight": weights["chatter"], + "detail": ("%d chattering alarm(s) (target 0)" % chatter_count) + if chatter_count is not None else "no data"}, + {"key": "standing", "label": "Standing Alarms", "score": standing_s, "weight": weights["standing"], + "detail": ("%d standing alarm(s) >24h (target 0)" % standing_count) + if standing_count is not None else "no data"}, + {"key": "priority", "label": "Priority Distribution", "score": priority_s, "weight": weights["priority"], + "detail": ("%.0f pts deviation from ISA 80/15/5" % sum_abs_dev) + if sum_abs_dev is not None else "no data"}, + ] + num = 0.0 + den = 0.0 + for s in subs: + if s["score"] is not None: + num += s["score"] * s["weight"] + den += s["weight"] + score = (num / den) if den else None + grade = None + if score is not None: + for cut, g in o["grade_cuts"]: + if score >= cut: + grade = g + break + if grade is None: + grade = "F" + return {"grade": grade, "score": score, "subs": subs} + + +def delta(current, prior, higher_is_worse=True, flat_pct=5.0): + d = {"value": current, "prior": prior, "delta_pct": None, "dir": "none", "good": None} + cur0 = current or 0 + pri0 = prior or 0 + if prior is None: + d["dir"] = "new" if cur0 > 0 else "none" + return d + if pri0 == 0: + d["dir"] = "new" if cur0 > 0 else "none" + return d + if current is None: + return d + dp = 100.0 * (current - prior) / float(prior) + d["delta_pct"] = dp + if abs(dp) < flat_pct: + d["dir"] = "flat" + return d + d["dir"] = "up" if dp > 0 else "down" + d["good"] = (d["dir"] == "down") == bool(higher_is_worse) + return d + + +# ---------------- the bundle ---------------- + + +def build_bundle(rows, active_now, shelved_count, start_ms, end_ms, now_ms, opts=None): + o = opts_merged(opts) + try: + start_ms = int(start_ms or 0) + end_ms = int(end_ms or 0) + now_ms = int(now_ms or 0) + except Exception: + start_ms, end_ms, now_ms = 0, 0, 0 + filters = (opts or {}).get("filters") if opts else None + instances, mode = correlate_instances(rows) + all_instances = instances + all_active_now = active_now + instances = apply_filters(instances, filters) + active_now = apply_status_filters(active_now, filters) + + # Filter dropdown options enumerate the UNfiltered data, so applying a + # filter never collapses the available choices. + areas = sorted(set([i["area"] for i in all_instances] + + [parse_source(s.get("source"), s.get("display_path"))["area"] + for s in all_active_now or []])) + prios_seen = sorted(set([(i["priority"], i["priority_name"]) + for i in all_instances])) + + bundle = _assemble(instances, active_now, shelved_count, start_ms, end_ms, + now_ms, mode, areas, prios_seen, o) + if filters_active(filters): + # Overview tab stays a plant-wide (unfiltered) summary; every other + # consumer binds the filtered top-level sections. + bundle["overview"] = _assemble(all_instances, all_active_now, + shelved_count, start_ms, end_ms, + now_ms, mode, areas, prios_seen, o) + else: + bundle["overview"] = None + return bundle + + +def filters_active(filters): + if not filters: + return False + try: + return bool(filters.get("priorities") or filters.get("areas") or + filters.get("states") or + (filters.get("search") or "").strip()) + except Exception: + return False + + +def _assemble(instances, active_now, shelved_count, start_ms, end_ms, now_ms, + mode, areas, prios_seen, o): + window_h = _safe_div(end_ms - start_ms, 3600000.0) or 0.0 + filtered_events = 0 + for i in instances: + filtered_events += i.get("n_events", 0) + current, prior, orphans = partition_instances(instances, start_ms, end_ms) + + bins = rate_bins(current, start_ms, end_ms, o) + floods = flood_episodes(bins, current, start_ms, end_ms, o) + chat = chattering(current, start_ms, end_ms, o) + fleet = fleeting(current, o) + open_current = [i for i in current if i["open"]] + stand = standing(active_now, open_current, now_ms, o) + mm = mtta_mttr(current, start_ms, end_ms, o) + par = pareto(current, o) + prio = priority_distribution(current, o) + heat = heatmap(current, o) + + # prior mirrors (cheap subset for deltas) + p_start = start_ms - (end_ms - start_ms) + p_bins = rate_bins(prior, p_start, start_ms, o) + p_floods = flood_episodes(p_bins, prior, p_start, start_ms, o) + p_chat = chattering(prior, p_start, start_ms, o) + p_fleet = fleeting(prior, o) + p_mm = mtta_mttr(prior, p_start, start_ms, o) + + n_act = len(current) + p_act = len(prior) + rate = _safe_div(n_act, window_h) + p_rate = _safe_div(p_act, window_h) + unacked_now = len([s for s in active_now or [] if s.get("unacked")]) + flat = o["delta_flat_pct"] + + kpis = { + "activations": delta(n_act, p_act, True, flat), + "rate_per_hr": delta(rate, p_rate, True, flat), + "active_now": delta(len(active_now or []), None, True, flat), + "unacked_now": delta(unacked_now, None, True, flat), + "shelved_now": delta(shelved_count, None, True, flat), + "mtta_ms": delta(mm["mtta"]["median_ms"], p_mm["mtta"]["median_ms"], True, flat), + "mttr_ms": delta(mm["mttr"]["median_ms"], p_mm["mttr"]["median_ms"], True, flat), + "flood_pct": delta(floods["pct_time_in_flood"], p_floods["pct_time_in_flood"], True, flat), + "flood_count": delta(len(floods["episodes"]), len(p_floods["episodes"]), True, flat), + "chatter_count": delta(len(chat), len(p_chat), True, flat), + "standing_count": delta(stand["count"], None, True, flat), + "fleeting_count": delta(fleet["total"], p_fleet["total"], True, flat), + } + + has_activity = n_act > 0 + health = health_score(rate if has_activity else None, + floods["pct_time_in_flood"] if has_activity else None, + len(chat), stand["count"] if (active_now or open_current) or has_activity else None, + prio["sum_abs_dev"], has_activity, o) + if not has_activity and not (active_now or []): + health = {"grade": None, "score": None, "subs": health["subs"]} + + active_now_rows = [] + for s in (active_now or [])[:int(o["active_now_cap"])]: + parsed = parse_source(s.get("source"), s.get("display_path")) + a = s.get("active_ms") + active_now_rows.append({"source": s.get("source") or "", "label": parsed["label"], + "area": parsed["area"], + "priority_name": s.get("priority_name", "Unknown"), + "active_ms": a, + "age_ms": (now_ms - a) if a is not None else None, + "unacked": bool(s.get("unacked"))}) + + return { + "meta": {"start_ms": start_ms, "end_ms": end_ms, "prior_start_ms": p_start, + "now_ms": now_ms, "window_hours": window_h, + "event_count": filtered_events, "activation_count": n_act, + "dropped_rows": 0, "truncated": False, + "correlation_mode": mode, "standing_mode": stand["mode"], + "areas": areas, + "priorities_seen": [{"value": p[0], "label": p[1]} for p in prios_seen], + "source_count": len(set(i["source"] for i in current)), + "error": None, "opts": { + "bin_ms": bins["bin_ms"], "target_per_hr": o["target_per_hr"], + "flood_per_10min": o["flood_per_10min"], + "chatter_per_hr": o["chatter_per_hr"], + "fleeting_s": o["fleeting_s"], "standing_hours": o["standing_hours"]}}, + "kpis": kpis, + "health": health, + "insights": [], + "rate": bins, + "floods": floods, + "priority": prio, + "heatmap": heat, + "mtta_mttr": mm, + "pareto": par, + "top_sources": top_sources(par, 5), + "chattering": chat, + "fleeting": fleet, + "standing": stand, + "active_now": active_now_rows, + } diff --git a/ignition/gateway/projects/AlarmAnalysis/ignition/script-python/PrimeControls/calc/resource.json b/ignition/gateway/projects/AlarmAnalysis/ignition/script-python/PrimeControls/calc/resource.json new file mode 100644 index 0000000..5aceefa --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/ignition/script-python/PrimeControls/calc/resource.json @@ -0,0 +1,17 @@ +{ + "scope": "A", + "version": 1, + "restricted": false, + "overridable": true, + "files": [ + "code.py" + ], + "attributes": { + "hintScope": 2, + "lastModificationSignature": "e8cf57d0aef30237c58323477564a9c4786705971ea155d0b861c359b561fe34", + "lastModification": { + "actor": "admin", + "timestamp": "2026-07-16T21:24:23Z" + } + } +} \ No newline at end of file diff --git a/ignition/gateway/projects/AlarmAnalysis/ignition/script-python/PrimeControls/fmt/code.py b/ignition/gateway/projects/AlarmAnalysis/ignition/script-python/PrimeControls/fmt/code.py new file mode 100644 index 0000000..e66f8e3 --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/ignition/script-python/PrimeControls/fmt/code.py @@ -0,0 +1,324 @@ +from __future__ import division, print_function + +import time as _time + +from PrimeControls import calc + +ARROW_UP = u"▲" +ARROW_DOWN = u"▼" +ARROW_FLAT = u"—" + + +def dur(ms): + if ms is None: + return "--" + try: + s = int(round(ms / 1000.0)) + except Exception: + return "--" + if s < 60: + return "%ds" % s + m = s // 60 + if m < 60: + return "%dm %ds" % (m, s % 60) if s % 60 else "%dm" % m + h = m // 60 + if h < 24: + return "%dh %dm" % (h, m % 60) if m % 60 else "%dh" % h + d = h // 24 + return "%dd %dh" % (d, h % 24) if h % 24 else "%dd" % d + + +def num(v, dec=0): + if v is None: + return "--" + try: + if dec: + s = ("%%.%df" % dec) % v + whole, frac = s.split(".") + return _group(whole) + "." + frac + return _group("%d" % round(v)) + except Exception: + return str(v) + + +def _group(digits): + neg = digits.startswith("-") + if neg: + digits = digits[1:] + out = "" + while len(digits) > 3: + out = "," + digits[-3:] + out + digits = digits[:-3] + return ("-" if neg else "") + digits + out + + +def pct(v, dec=0, signed=False): + if v is None: + return "--" + try: + fmtstr = "%%+.%df%%%%" % dec if signed else "%%.%df%%%%" % dec + return fmtstr % v + except Exception: + return str(v) + + +def clock(ms): + if ms is None: + return "--" + try: + return _time.strftime("%I:%M %p", _time.localtime(ms / 1000.0)).lstrip("0") + except Exception: + return "--" + + +def day_clock(ms): + if ms is None: + return "--" + try: + return _time.strftime("%a ", _time.localtime(ms / 1000.0)) + clock(ms) + except Exception: + return "--" + + +def delta_chip(d): + if not d: + return {"arrow": ARROW_FLAT, "text": "--", "good": None} + direction = d.get("dir") + if direction == "up": + arrow = ARROW_UP + elif direction == "down": + arrow = ARROW_DOWN + else: + arrow = ARROW_FLAT + if direction == "new": + text = "new" + elif direction == "none": + text = "--" + elif d.get("delta_pct") is not None: + text = pct(d["delta_pct"], 0, True) + else: + text = "--" + return {"arrow": arrow, "text": text, "good": d.get("good")} + + +def grade_color_key(grade): + return grade if grade in ("A", "B", "C", "D", "F") else "NA" + + +# ---------------- insights ---------------- + + +def insights(bundle, opts=None): + o = calc.opts_merged(opts) + if not bundle: + return [] + out = [] + for rule in (_i_rate, _i_floods, _i_chatter, _i_standing, + _i_priority, _i_pareto, _i_fleeting): + try: + r = rule(bundle) + if r: + out.append(r) + except Exception: + pass + out.sort(key=lambda i: (i["severity"], -i.get("magnitude", 0))) + out = out[:int(o["insights_max"])] + if not out: + k = bundle.get("kpis", {}).get("rate_per_hr", {}) + rate = k.get("value") + if bundle.get("meta", {}).get("activation_count", 0) == 0: + text = "No alarm events in the selected range." + else: + text = ("No significant changes vs the prior period; rate %.1f/hr is within " + "the ISA target of %.0f/hr." % (rate or 0.0, + bundle["meta"]["opts"]["target_per_hr"])) + out = [{"severity": 3, "icon": "check_circle", "text": text, + "tab": "overview", "magnitude": 0}] + for i in out: + i.pop("magnitude", None) + return out + + +def _i_rate(b): + k = b["kpis"]["activations"] + dp = k.get("delta_pct") + cur = k.get("value") or 0 + if dp is None or cur < 10: + return None + if dp >= 25: + sev = 0 if dp >= 100 else 1 + text = ("Alarm rate up %d%% vs prior period (%s -> %s activations)" + % (round(dp), num(k.get("prior")), num(cur))) + attr = _rate_attribution(b) + if attr: + text += " - largest increase from %s" % attr + return {"severity": sev, "icon": "trending_up", "text": text + ".", + "tab": "badactors", "magnitude": dp} + if dp <= -25: + return {"severity": 3, "icon": "trending_down", + "text": "Alarm rate down %d%% vs prior period - improving." % round(-dp), + "tab": "overview", "magnitude": -dp} + return None + + +def _rate_attribution(b): + rows = b.get("pareto", {}).get("rows") or [] + if rows: + return rows[0]["label"] + return None + + +def _i_floods(b): + eps = b.get("floods", {}).get("episodes") or [] + if not eps: + return None + worst = sorted(eps, key=lambda e: -e["event_count"])[0] + total_min = sum(e["duration_ms"] for e in eps) / 60000.0 + text = ("%d flood episode(s) totaling %s; worst %s-%s (%d events, mostly %s)." + % (len(eps), dur(total_min * 60000), day_clock(worst["start_ms"]), + clock(worst["end_ms"]), worst["event_count"], + worst["top_source_label"] or worst["top_area"] or "n/a")) + return {"severity": 0, "icon": "flood", "text": text, "tab": "analysis", + "magnitude": len(eps)} + + +def _i_chatter(b): + rows = b.get("chattering") or [] + if not rows: + return None + top = rows[0] + if len(rows) == 1: + text = ("%s is chattering (%.0f re-triggers/hr, median gap %.0fs)." + % (top["label"], top["per_hour"], top["median_gap_s"])) + else: + text = ("%d alarms are chattering (>%.0f re-triggers/hr); worst is %s at %.0f/hr." + % (len(rows), b["meta"]["opts"]["chatter_per_hr"], top["label"], + top["per_hour"])) + return {"severity": 1, "icon": "repeat", "text": text, "tab": "badactors", + "magnitude": len(rows)} + + +def _i_standing(b): + st = b.get("standing") or {} + if not st.get("count"): + return None + rows = st.get("rows") or [] + oldest = rows[0] if rows else None + text = "%d alarm(s) standing >%dh" % (st["count"], b["meta"]["opts"]["standing_hours"]) + if oldest and oldest.get("age_h"): + text += "; oldest is %s at %.1f days" % (oldest["label"], oldest["age_h"] / 24.0) + return {"severity": 1, "icon": "schedule", "text": text + ".", "tab": "badactors", + "magnitude": st["count"]} + + +def _i_priority(b): + p = b.get("priority") or {} + if p.get("sum_abs_dev") is None: + return None + high = p["pct"]["high"] + target = p["target"]["high"] + if high - target >= 10: + return {"severity": 2, "icon": "priority_high", + "text": ("High/Critical alarms are %d%% of events vs %d%% ISA target - " + "priorities may be over-assigned." % (round(high), round(target))), + "tab": "analysis", "magnitude": high - target} + return None + + +def _i_pareto(b): + rows = b.get("pareto", {}).get("rows") or [] + for k, row in enumerate(rows[:3]): + if row["cum_pct"] >= 50: + n = k + 1 + return {"severity": 2, "icon": "insights", + "text": ("%d source(s) account for %d%% of all activations - start " + "with %s." % (n, round(row["cum_pct"]), rows[0]["label"])), + "tab": "badactors", "magnitude": row["cum_pct"]} + return None + + +def _i_fleeting(b): + fl = b.get("fleeting") or {} + if (fl.get("total") or 0) < 10: + return None + return {"severity": 2, "icon": "bolt", + "text": ("%d fleeting alarms (<%ds active) in range - likely deadband or " + "nuisance issues." % (fl["total"], b["meta"]["opts"]["fleeting_s"])), + "tab": "badactors", "magnitude": fl["total"]} + + +# ---------------- shift report ---------------- + + +def shift_bounds(now_ms, shift_hours=8, anchor_hour=0): + lt = _time.localtime(now_ms / 1000.0) + midnight = _time.mktime((lt.tm_year, lt.tm_mon, lt.tm_mday, 0, 0, 0, + lt.tm_wday, lt.tm_yday, -1)) + anchor = midnight + anchor_hour * 3600 + now_s = now_ms / 1000.0 + if now_s < anchor: + anchor -= 86400 + shift_s = shift_hours * 3600 + k = int((now_s - anchor) // shift_s) + cur_start = anchor + k * shift_s + cur = (int(cur_start * 1000), int((cur_start + shift_s) * 1000)) + prev = (int((cur_start - shift_s) * 1000), int(cur_start * 1000)) + label = "%s - %s" % (day_clock(prev[0]), day_clock(prev[1])) + return {"current": cur, "previous": prev, "label": label} + + +def _kpi_line(name, k, formatter): + chip = delta_chip(k) + val = formatter(k.get("value")) + extra = "" if chip["text"] == "--" else " (%s %s)" % (chip["arrow"], chip["text"]) + return "%-14s %s%s" % (name + ":", val, extra) + + +def shift_report(bundle, label): + if not bundle: + return {"text": "No data.", "sections": []} + k = bundle["kpis"] + h = bundle["health"] + subs = " | ".join("%s %s" % (s["key"], "%d" % s["score"] if s["score"] is not None else "n/a") + for s in h.get("subs", [])) + sections = [] + head = ["Shift: %s" % label, + "Generated: %s" % day_clock(bundle["meta"]["now_ms"]), + "Health: %s (%s) [%s]" % (h.get("grade") or "N/A", + "%d" % h["score"] if h.get("score") is not None else "-", + subs)] + sections.append({"heading": "ALARM SHIFT REPORT", "lines": head}) + kpis = [ + _kpi_line("Activations", k["activations"], num), + _kpi_line("Rate/hr", k["rate_per_hr"], lambda v: num(v, 1)), + _kpi_line("MTTA", k["mtta_ms"], dur), + _kpi_line("MTTR", k["mttr_ms"], dur), + _kpi_line("Flood time", k["flood_pct"], lambda v: pct(v, 1)), + _kpi_line("Chattering", k["chatter_count"], num), + _kpi_line("Standing", k["standing_count"], num), + _kpi_line("Fleeting", k["fleeting_count"], num), + ] + sections.append({"heading": "KPIs (vs prior shift)", "lines": kpis}) + actors = ["#%d %-40s %6s %s" % (r["rank"], r["label"][:40], num(r["count"]), + pct(r["cum_pct"])) + for r in bundle["pareto"]["rows"][:5]] or ["none"] + sections.append({"heading": "Top actors (count, cumulative %)", "lines": actors}) + floods = ["%s - %s %s, %d events, top: %s" + % (day_clock(e["start_ms"]), clock(e["end_ms"]), dur(e["duration_ms"]), + e["event_count"], e["top_source_label"] or e["top_area"]) + for e in bundle["floods"]["episodes"]] or ["none"] + sections.append({"heading": "Flood episodes", "lines": floods}) + standing_rows = bundle["standing"]["rows"][:8] + stand = ["%-40s active %s%s" % (r["label"][:40], dur(r["age_ms"]), + " (UNACKED)" if r["unacked"] else "") + for r in standing_rows] or ["none"] + sections.append({"heading": "Standing / handover attention", "lines": stand}) + sections.append({"heading": "Insights", + "lines": [i["text"] for i in bundle.get("insights") or []] or ["none"]}) + text_parts = [] + for s in sections: + text_parts.append(s["heading"]) + text_parts.append("-" * len(s["heading"])) + text_parts.extend(s["lines"]) + text_parts.append("") + return {"text": "\n".join(text_parts), "sections": sections} diff --git a/ignition/gateway/projects/AlarmAnalysis/ignition/script-python/PrimeControls/fmt/resource.json b/ignition/gateway/projects/AlarmAnalysis/ignition/script-python/PrimeControls/fmt/resource.json new file mode 100644 index 0000000..0cded86 --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/ignition/script-python/PrimeControls/fmt/resource.json @@ -0,0 +1,17 @@ +{ + "scope": "A", + "version": 1, + "restricted": false, + "overridable": true, + "files": [ + "code.py" + ], + "attributes": { + "hintScope": 2, + "lastModificationSignature": "359da2a962a9429e8d175cc280a6a33facda2c0995eb4ad29baa019e0a1cfdb1", + "lastModification": { + "actor": "admin", + "timestamp": "2026-07-16T21:24:30Z" + } + } +} \ No newline at end of file diff --git a/ignition/gateway/projects/AlarmAnalysis/project.json b/ignition/gateway/projects/AlarmAnalysis/project.json new file mode 100644 index 0000000..4c08e5f --- /dev/null +++ b/ignition/gateway/projects/AlarmAnalysis/project.json @@ -0,0 +1,7 @@ +{ + "title": "Alarm Analysis Dashboard", + "description": "", + "enabled": true, + "inheritable": false, + "parent": "" +} \ No newline at end of file diff --git a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/session-props/props.json b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/session-props/props.json index ceb384c..65147a3 100644 --- a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/session-props/props.json +++ b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/session-props/props.json @@ -3,12 +3,12 @@ "PrimeControls": { "query": { "areas": [], - "endMs": 0, + "endMs": 1784233045923, "priorities": [], "rangePreset": "8h", "refreshToken": 0, "search": "", - "startMs": 0, + "startMs": 1784204245923, "states": [] }, "ui": { @@ -83,7 +83,7 @@ } }, "props": { - "address": "172.23.0.1", + "address": "172.18.0.1", "device": {}, "geolocation": {}, "locale": "en-US", diff --git a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/session-props/resource.json b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/session-props/resource.json index edab5eb..414b15b 100644 --- a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/session-props/resource.json +++ b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/session-props/resource.json @@ -6,5 +6,11 @@ "files": [ "props.json" ], - "attributes": {} -} + "attributes": { + "lastModificationSignature": "3d8f163b8f2bb09be0bd014e7ee7d798223ddf7b5019b1a9fa95ab63e56b2958", + "lastModification": { + "actor": "admin", + "timestamp": "2026-07-16T20:18:00Z" + } + } +} \ No newline at end of file diff --git a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Components/HealthGauge/resource.json b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Components/HealthGauge/resource.json index 276dced..9f89d6f 100644 --- a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Components/HealthGauge/resource.json +++ b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Components/HealthGauge/resource.json @@ -4,7 +4,14 @@ "restricted": false, "overridable": true, "files": [ - "view.json" + "view.json", + "thumbnail.png" ], - "attributes": {} -} + "attributes": { + "lastModificationSignature": "44b87b06b842940d391b9bafd34557de0ac8a5005f6ee91844ac9dd2f18c0f04", + "lastModification": { + "actor": "admin", + "timestamp": "2026-07-16T20:14:21Z" + } + } +} \ No newline at end of file diff --git a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Components/HealthGauge/view.json b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Components/HealthGauge/view.json index fd3c8b3..67b7a53 100644 --- a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Components/HealthGauge/view.json +++ b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Components/HealthGauge/view.json @@ -1,8 +1,6 @@ { "custom": {}, - "params": { - "health": null - }, + "params": {}, "propConfig": { "params.health": { "paramDirection": "input", @@ -24,7 +22,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\tg = value['grade']\n\texcept:\n\t\tg = None\n\treturn 'PrimeControls/Grade/' + (g if g in ['A', 'B', 'C', 'D', 'F'] else 'NA')", + "code": "\ttry:\n\t\tg \u003d value[\u0027grade\u0027]\n\texcept:\n\t\tg \u003d None\n\treturn \u0027PrimeControls/Grade/\u0027 + (g if g in [\u0027A\u0027, \u0027B\u0027, \u0027C\u0027, \u0027D\u0027, \u0027F\u0027] else \u0027NA\u0027)", "type": "script" } ], @@ -38,7 +36,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\treturn value['grade'] or 'N/A'\n\texcept:\n\t\treturn 'N/A'", + "code": "\ttry:\n\t\treturn value[\u0027grade\u0027] or \u0027N/A\u0027\n\texcept:\n\t\treturn \u0027N/A\u0027", "type": "script" } ], @@ -85,7 +83,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\ts = value['score']\n\t\treturn 'Score %d - click for detail' % round(s) if s is not None else 'no data in range'\n\texcept:\n\t\treturn 'no data in range'", + "code": "\ttry:\n\t\ts \u003d value[\u0027score\u0027]\n\t\treturn \u0027Score %d - click for detail\u0027 % round(s) if s is not None else \u0027no data in range\u0027\n\texcept:\n\t\treturn \u0027no data in range\u0027", "type": "script" } ], @@ -114,7 +112,7 @@ "dom": { "onClick": { "config": { - "script": "\tsystem.perspective.openPopup('PC_HealthScore', 'PrimeControls/Popups/HealthScore', params = {'health': self.view.params.health}, showCloseIcon = True, draggable = True)" + "script": "\tsystem.perspective.openPopup(\u0027PC_HealthScore\u0027, \u0027PrimeControls/Popups/HealthScore\u0027, params \u003d {\u0027health\u0027: self.view.params.health}, showCloseIcon \u003d True, draggable \u003d True)" }, "scope": "G", "type": "script" @@ -125,7 +123,6 @@ "name": "root" }, "props": { - "direction": "row", "style": { "alignItems": "center", "cursor": "pointer", diff --git a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Components/KpiCard/resource.json b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Components/KpiCard/resource.json index 276dced..a85d975 100644 --- a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Components/KpiCard/resource.json +++ b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Components/KpiCard/resource.json @@ -4,7 +4,14 @@ "restricted": false, "overridable": true, "files": [ - "view.json" + "view.json", + "thumbnail.png" ], - "attributes": {} -} + "attributes": { + "lastModificationSignature": "75b1aa709e8638ac045f15b7a6d3543d023b8f3e886e2f9f1c263c0390c4fe9d", + "lastModification": { + "actor": "admin", + "timestamp": "2026-07-16T20:45:13Z" + } + } +} \ No newline at end of file diff --git a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Components/KpiCard/view.json b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Components/KpiCard/view.json index 912a05e..4140c30 100644 --- a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Components/KpiCard/view.json +++ b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Components/KpiCard/view.json @@ -1,11 +1,6 @@ { "custom": {}, - "params": { - "delta": null, - "label": null, - "unit": null, - "value": null - }, + "params": {}, "propConfig": { "params.delta": { "paramDirection": "input", @@ -43,10 +38,14 @@ }, "props": { "style": { - "classes": "PrimeControls/Text/Muted" + "classes": "PrimeControls/Text/Muted", + "lineHeight": "1.1" } }, - "type": "ia.display.label" + "type": "ia.display.label", + "position": { + "shrink": 0 + } }, { "children": [ @@ -72,7 +71,8 @@ }, "props": { "style": { - "classes": "PrimeControls/Text/Kpi" + "classes": "PrimeControls/Text/Kpi", + "lineHeight": "1.1" } }, "type": "ia.display.label" @@ -99,7 +99,8 @@ }, "props": { "style": { - "classes": "PrimeControls/Text/Muted" + "classes": "PrimeControls/Text/Muted", + "lineHeight": "1.1" } }, "type": "ia.display.label" @@ -108,8 +109,12 @@ "meta": { "name": "valueRow" }, + "position": { + "grow": 1, + "shrink": 1, + "basis": "auto" + }, "props": { - "direction": "row", "style": { "alignItems": "baseline", "gap": "4px" @@ -165,8 +170,16 @@ } } }, - "props": {}, - "type": "ia.display.label" + "props": { + "style": { + "alignSelf": "flex-start", + "padding": "0 6px" + } + }, + "type": "ia.display.label", + "position": { + "shrink": 0 + } } ], "meta": { @@ -176,9 +189,11 @@ "direction": "column", "style": { "classes": "PrimeControls/Card", - "justifyContent": "space-between" + "justifyContent": "space-between", + "overflow": "hidden", + "padding": "6px 12px" } }, "type": "ia.container.flex" } -} \ No newline at end of file +} diff --git a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Dashboard/resource.json b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Dashboard/resource.json index 276dced..f2f32c2 100644 --- a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Dashboard/resource.json +++ b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Dashboard/resource.json @@ -4,7 +4,14 @@ "restricted": false, "overridable": true, "files": [ - "view.json" + "view.json", + "thumbnail.png" ], - "attributes": {} -} + "attributes": { + "lastModificationSignature": "04bdfb9c4eecc4f8cf35a85994e6790ef8d62a1d1aa00553e4dc4193e171a110", + "lastModification": { + "actor": "admin", + "timestamp": "2026-07-16T20:31:35Z" + } + } +} \ No newline at end of file diff --git a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Dashboard/view.json b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Dashboard/view.json index 10e1923..dc8f982 100644 --- a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Dashboard/view.json +++ b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Dashboard/view.json @@ -64,7 +64,7 @@ "name": "filterBar" }, "position": { - "basis": "56px", + "basis": "auto", "shrink": 0 }, "propConfig": { @@ -75,10 +75,21 @@ }, "type": "property" } + }, + "position.display": { + "binding": { + "config": { + "expression": "{session.custom.PrimeControls.ui.selectedTab} != 0" + }, + "type": "expr" + } } }, "props": { - "path": "PrimeControls/Shell/FilterBar" + "path": "PrimeControls/Shell/FilterBar", + "style": { + "minHeight": "56px" + } }, "type": "ia.display.view" }, @@ -316,4 +327,4 @@ }, "type": "ia.container.flex" } -} \ No newline at end of file +} diff --git a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Popups/AlarmDetail/resource.json b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Popups/AlarmDetail/resource.json index 276dced..78beb47 100644 --- a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Popups/AlarmDetail/resource.json +++ b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Popups/AlarmDetail/resource.json @@ -4,7 +4,14 @@ "restricted": false, "overridable": true, "files": [ - "view.json" + "view.json", + "thumbnail.png" ], - "attributes": {} -} + "attributes": { + "lastModificationSignature": "1d6feda3a22e57c52a032bea5ed828b0b7836e249e46a68faa58af39207b9119", + "lastModification": { + "actor": "admin", + "timestamp": "2026-07-16T20:41:00Z" + } + } +} \ No newline at end of file diff --git a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Popups/AlarmDetail/view.json b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Popups/AlarmDetail/view.json index 81edce1..9addc44 100644 --- a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Popups/AlarmDetail/view.json +++ b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Popups/AlarmDetail/view.json @@ -1,20 +1,15 @@ { - "custom": { - "detail": null - }, - "params": { - "label": null, - "source": null - }, + "custom": {}, + "params": {}, "propConfig": { "custom.detail": { "binding": { "config": { - "expression": "{view.params.source} + '|' + {session.custom.PrimeControls.query.startMs} + '|' + {session.custom.PrimeControls.query.endMs}" + "expression": "{view.params.source} + \u0027|\u0027 + {session.custom.PrimeControls.query.startMs} + \u0027|\u0027 + {session.custom.PrimeControls.query.endMs}" }, "transforms": [ { - "code": "\ttry:\n\t\tsrc = self.view.params.source\n\texcept:\n\t\tsrc = None\n\tif not src:\n\t\treturn None\n\ttry:\n\t\tq = self.session.custom.PrimeControls.query\n\t\treturn PrimeControls.alarms.getSourceDetail(src, q.startMs, q.endMs)\n\texcept:\n\t\treturn None", + "code": "\ttry:\n\t\tsrc \u003d self.view.params.source\n\texcept:\n\t\tsrc \u003d None\n\tif not src:\n\t\treturn None\n\ttry:\n\t\tq \u003d self.session.custom.PrimeControls.query\n\t\treturn PrimeControls.alarms.getSourceDetail(src, q.startMs, q.endMs)\n\texcept:\n\t\treturn None", "type": "script" } ], @@ -52,7 +47,7 @@ }, "transforms": [ { - "code": "\tif value:\n\t\treturn value\n\ttry:\n\t\treturn self.view.params.source or 'Alarm Detail'\n\texcept:\n\t\treturn 'Alarm Detail'", + "code": "\tif value:\n\t\treturn value\n\ttry:\n\t\treturn self.view.params.source or \u0027Alarm Detail\u0027\n\texcept:\n\t\treturn \u0027Alarm Detail\u0027", "type": "script" } ], @@ -79,7 +74,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\treturn value['area'] or ''\n\texcept:\n\t\treturn ''", + "code": "\ttry:\n\t\treturn value[\u0027area\u0027] or \u0027\u0027\n\texcept:\n\t\treturn \u0027\u0027", "type": "script" } ], @@ -126,7 +121,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\treturn PrimeControls.fmt.num(value['stats']['count'])\n\texcept:\n\t\treturn '--'", + "code": "\ttry:\n\t\treturn PrimeControls.fmt.num(value[\u0027stats\u0027][\u0027count\u0027])\n\texcept:\n\t\treturn \u0027--\u0027", "type": "script" } ], @@ -158,7 +153,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\treturn PrimeControls.fmt.dur(value['stats']['avg_tta_ms'])\n\texcept:\n\t\treturn '--'", + "code": "\ttry:\n\t\treturn PrimeControls.fmt.dur(value[\u0027stats\u0027][\u0027avg_tta_ms\u0027])\n\texcept:\n\t\treturn \u0027--\u0027", "type": "script" } ], @@ -190,7 +185,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\treturn PrimeControls.fmt.dur(value['stats']['avg_active_ms'])\n\texcept:\n\t\treturn '--'", + "code": "\ttry:\n\t\treturn PrimeControls.fmt.dur(value[\u0027stats\u0027][\u0027avg_active_ms\u0027])\n\texcept:\n\t\treturn \u0027--\u0027", "type": "script" } ], @@ -222,7 +217,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\treturn PrimeControls.fmt.num(value['stats']['fleeting_count'])\n\texcept:\n\t\treturn '--'", + "code": "\ttry:\n\t\treturn PrimeControls.fmt.num(value[\u0027stats\u0027][\u0027fleeting_count\u0027])\n\texcept:\n\t\treturn \u0027--\u0027", "type": "script" } ], @@ -247,7 +242,6 @@ "shrink": 0 }, "props": { - "alignItems": "stretch", "style": { "gap": "10px" } @@ -276,7 +270,7 @@ "name": "repDaily" }, "position": { - "basis": "30px", + "basis": "72px", "shrink": 0 }, "propConfig": { @@ -287,7 +281,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\tdaily = list(value['daily'] or [])\n\texcept:\n\t\tdaily = []\n\tcounts = []\n\tfor d in daily:\n\t\ttry:\n\t\t\tcounts.append(int(d['count']))\n\t\texcept:\n\t\t\tcounts.append(0)\n\tif not counts:\n\t\treturn []\n\tm = max(counts)\n\treturn [{'count': c, 'max': m} for c in counts]", + "code": "\ttry:\n\t\tdaily \u003d list(value[\u0027daily\u0027] or [])\n\texcept:\n\t\tdaily \u003d []\n\tcounts \u003d []\n\tfor d in daily:\n\t\ttry:\n\t\t\tcounts.append(int(d[\u0027count\u0027]))\n\t\texcept:\n\t\t\tcounts.append(0)\n\tif not counts:\n\t\treturn []\n\tm \u003d max(counts)\n\treturn [{\u0027count\u0027: c, \u0027max\u0027: m} for c in counts]", "type": "script" } ], @@ -296,13 +290,15 @@ } }, "props": { - "direction": "row", "elementPosition": { - "basis": "12px", - "grow": 1, - "shrink": 1 + "basis": "8px" }, - "path": "PrimeControls/Components/HeatmapCell" + "path": "PrimeControls/Components/HeatmapCell", + "style": { + "gap": "2px" + }, + "useDefaultViewHeight": false, + "useDefaultViewWidth": false }, "type": "ia.display.flex-repeater" }, @@ -321,7 +317,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\tdaily = list(value['daily'] or [])\n\texcept:\n\t\tdaily = []\n\tif not daily:\n\t\treturn 'No activity in range'\n\ttry:\n\t\treturn 'Activations per day, %s - %s' % (PrimeControls.fmt.day_clock(daily[0]['t0']), PrimeControls.fmt.day_clock(daily[-1]['t0']))\n\texcept:\n\t\treturn 'Activations per day'", + "code": "\ttry:\n\t\tdaily \u003d list(value[\u0027daily\u0027] or [])\n\texcept:\n\t\tdaily \u003d []\n\tif not daily:\n\t\treturn \u0027No activity in range\u0027\n\ttry:\n\t\treturn \u0027Activations per day, %s - %s\u0027 % (PrimeControls.fmt.day_clock(daily[0][\u0027t0\u0027]), PrimeControls.fmt.day_clock(daily[-1][\u0027t0\u0027]))\n\texcept:\n\t\treturn \u0027Activations per day\u0027", "type": "script" } ], @@ -386,7 +382,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\treturn len(value['stats']['top_ack_users']) > 0\n\texcept:\n\t\treturn False", + "code": "\ttry:\n\t\treturn len(value[\u0027stats\u0027][\u0027top_ack_users\u0027]) \u003e 0\n\texcept:\n\t\treturn False", "type": "script" } ], @@ -400,7 +396,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\tu = value['stats']['top_ack_users'][0]\n\t\treturn '%s (%d)' % (u['user'], u['count'])\n\texcept:\n\t\treturn ''", + "code": "\ttry:\n\t\tu \u003d value[\u0027stats\u0027][\u0027top_ack_users\u0027][0]\n\t\treturn \u0027%s (%d)\u0027 % (u[\u0027user\u0027], u[\u0027count\u0027])\n\texcept:\n\t\treturn \u0027\u0027", "type": "script" } ], @@ -432,7 +428,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\treturn len(value['stats']['top_ack_users']) > 1\n\texcept:\n\t\treturn False", + "code": "\ttry:\n\t\treturn len(value[\u0027stats\u0027][\u0027top_ack_users\u0027]) \u003e 1\n\texcept:\n\t\treturn False", "type": "script" } ], @@ -446,7 +442,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\tu = value['stats']['top_ack_users'][1]\n\t\treturn '%s (%d)' % (u['user'], u['count'])\n\texcept:\n\t\treturn ''", + "code": "\ttry:\n\t\tu \u003d value[\u0027stats\u0027][\u0027top_ack_users\u0027][1]\n\t\treturn \u0027%s (%d)\u0027 % (u[\u0027user\u0027], u[\u0027count\u0027])\n\texcept:\n\t\treturn \u0027\u0027", "type": "script" } ], @@ -478,7 +474,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\treturn len(value['stats']['top_ack_users']) > 2\n\texcept:\n\t\treturn False", + "code": "\ttry:\n\t\treturn len(value[\u0027stats\u0027][\u0027top_ack_users\u0027]) \u003e 2\n\texcept:\n\t\treturn False", "type": "script" } ], @@ -492,7 +488,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\tu = value['stats']['top_ack_users'][2]\n\t\treturn '%s (%d)' % (u['user'], u['count'])\n\texcept:\n\t\treturn ''", + "code": "\ttry:\n\t\tu \u003d value[\u0027stats\u0027][\u0027top_ack_users\u0027][2]\n\t\treturn \u0027%s (%d)\u0027 % (u[\u0027user\u0027], u[\u0027count\u0027])\n\texcept:\n\t\treturn \u0027\u0027", "type": "script" } ], @@ -524,7 +520,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\treturn len(value['stats']['top_ack_users']) == 0\n\texcept:\n\t\treturn True", + "code": "\ttry:\n\t\treturn len(value[\u0027stats\u0027][\u0027top_ack_users\u0027]) \u003d\u003d 0\n\texcept:\n\t\treturn True", "type": "script" } ], @@ -604,7 +600,7 @@ }, "transforms": [ { - "code": "\tout = []\n\ttry:\n\t\trows = list(value['events'] or [])\n\texcept:\n\t\trows = []\n\tfor r in rows[:25]:\n\t\ttry:\n\t\t\tout.append({'time_label': r['time_label'], 'state_label': r['state_label'], 'ack_user': r['ack_user'] or ''})\n\t\texcept:\n\t\t\tpass\n\treturn out", + "code": "\tout \u003d []\n\ttry:\n\t\trows \u003d list(value[\u0027events\u0027] or [])\n\texcept:\n\t\trows \u003d []\n\tfor r in rows[:25]:\n\t\ttry:\n\t\t\tout.append({\u0027time_label\u0027: r[\u0027time_label\u0027], \u0027state_label\u0027: r[\u0027state_label\u0027], \u0027ack_user\u0027: r[\u0027ack_user\u0027] or \u0027\u0027})\n\t\texcept:\n\t\t\tpass\n\treturn out", "type": "script" } ], @@ -613,6 +609,11 @@ } }, "props": { + "cells": { + "style": { + "padding": "5px" + } + }, "columns": [ { "field": "time_label", @@ -649,8 +650,7 @@ } }, "pager": { - "bottom": false, - "top": false + "bottom": false } }, "type": "ia.display.table" @@ -686,7 +686,7 @@ }, "transforms": [ { - "code": "\tif value is None:\n\t\treturn False\n\ttry:\n\t\treturn not value['error']\n\texcept:\n\t\treturn True", + "code": "\tif value is None:\n\t\treturn False\n\ttry:\n\t\treturn not value[\u0027error\u0027]\n\texcept:\n\t\treturn True", "type": "script" } ], @@ -717,7 +717,7 @@ }, "transforms": [ { - "code": "\tif value is None:\n\t\treturn True\n\ttry:\n\t\treturn bool(value['error'])\n\texcept:\n\t\treturn False", + "code": "\tif value is None:\n\t\treturn True\n\ttry:\n\t\treturn bool(value[\u0027error\u0027])\n\texcept:\n\t\treturn False", "type": "script" } ], @@ -741,7 +741,7 @@ "component": { "onActionPerformed": { "config": { - "script": "\tsystem.perspective.closePopup('PC_AlarmDetail')" + "script": "\tsystem.perspective.closePopup(\u0027PC_AlarmDetail\u0027)" }, "scope": "G", "type": "script" @@ -756,7 +756,6 @@ "shrink": 0 }, "props": { - "primary": true, "text": "Close" }, "type": "ia.input.button" @@ -775,7 +774,6 @@ "type": "ia.container.flex" } ], - "custom": {}, "meta": { "name": "root" }, diff --git a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Popups/EventDetail/view.json b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Popups/EventDetail/view.json index 40b70ef..7fd848c 100644 --- a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Popups/EventDetail/view.json +++ b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Popups/EventDetail/view.json @@ -519,6 +519,11 @@ "pager": { "bottom": false, "top": false + }, + "cells": { + "style": { + "padding": "5px" + } } }, "type": "ia.display.table" @@ -657,4 +662,4 @@ }, "type": "ia.container.flex" } -} \ No newline at end of file +} diff --git a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Popups/HealthScore/view.json b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Popups/HealthScore/view.json index d816946..9f16e99 100644 --- a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Popups/HealthScore/view.json +++ b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Popups/HealthScore/view.json @@ -187,7 +187,9 @@ "path": "PrimeControls/Components/ScoreBar", "style": { "overflowY": "auto" - } + }, + "useDefaultViewWidth": false, + "useDefaultViewHeight": false }, "type": "ia.display.flex-repeater" }, @@ -313,4 +315,4 @@ }, "type": "ia.container.flex" } -} \ No newline at end of file +} diff --git a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Shell/FilterBar/view.json b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Shell/FilterBar/view.json index ed304ef..18da2e5 100644 --- a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Shell/FilterBar/view.json +++ b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Shell/FilterBar/view.json @@ -54,6 +54,10 @@ "multiSelect": true, "placeholder": { "text": "Priority" + }, + "wrapMultiSelectValues": true, + "style": { + "maxWidth": "280px" } }, "type": "ia.input.dropdown" @@ -95,6 +99,10 @@ "multiSelect": true, "placeholder": { "text": "Area" + }, + "wrapMultiSelectValues": true, + "style": { + "maxWidth": "280px" } }, "type": "ia.input.dropdown" @@ -140,6 +148,10 @@ ], "placeholder": { "text": "State" + }, + "wrapMultiSelectValues": true, + "style": { + "maxWidth": "280px" } }, "type": "ia.input.dropdown" @@ -174,7 +186,7 @@ "component": { "onActionPerformed": { "config": { - "script": "\tq = self.session.custom.PrimeControls.query\n\tq.refreshToken = q.refreshToken + 1" + "script": "\tq = self.session.custom.PrimeControls.query\n\ttry:\n\t\tq.priorities = list(self.getSibling('ddPriority').props.value or [])\n\t\tq.areas = list(self.getSibling('ddArea').props.value or [])\n\t\tq.states = list(self.getSibling('ddState').props.value or [])\n\t\tq.search = str(self.getSibling('txtSearch').props.text or '')\n\texcept:\n\t\tpass\n\tq.refreshToken = q.refreshToken + 1" }, "scope": "G", "type": "script" @@ -199,7 +211,7 @@ "component": { "onActionPerformed": { "config": { - "script": "\tq = self.session.custom.PrimeControls.query\n\tq.priorities = []\n\tq.areas = []\n\tq.states = []\n\tq.search = ''\n\tq.refreshToken = q.refreshToken + 1" + "script": "\tq = self.session.custom.PrimeControls.query\n\tq.priorities = []\n\tq.areas = []\n\tq.states = []\n\tq.search = ''\n\ttry:\n\t\tself.getSibling('ddPriority').props.value = []\n\t\tself.getSibling('ddArea').props.value = []\n\t\tself.getSibling('ddState').props.value = []\n\t\tself.getSibling('txtSearch').props.text = ''\n\texcept:\n\t\tpass\n\tq.refreshToken = q.refreshToken + 1" }, "scope": "G", "type": "script" @@ -266,4 +278,4 @@ }, "type": "ia.container.flex" } -} \ No newline at end of file +} diff --git a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Shell/Header/resource.json b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Shell/Header/resource.json index 276dced..373c439 100644 --- a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Shell/Header/resource.json +++ b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Shell/Header/resource.json @@ -4,7 +4,14 @@ "restricted": false, "overridable": true, "files": [ - "view.json" + "view.json", + "thumbnail.png" ], - "attributes": {} -} + "attributes": { + "lastModificationSignature": "ad9c36584854ed67b341f681e7992b6264caf658ed334102fa088a1cc84012fd", + "lastModification": { + "actor": "admin", + "timestamp": "2026-07-16T20:47:54Z" + } + } +} \ No newline at end of file diff --git a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Shell/Header/view.json b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Shell/Header/view.json index 61a7083..1063089 100644 --- a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Shell/Header/view.json +++ b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Shell/Header/view.json @@ -1,8 +1,6 @@ { "custom": {}, - "params": { - "bundle": null - }, + "params": {}, "propConfig": { "params.bundle": { "paramDirection": "input", @@ -11,7 +9,7 @@ }, "props": { "defaultSize": { - "height": 150, + "height": 165, "width": 1600 } }, @@ -19,6 +17,26 @@ "children": [ { "children": [ + { + "meta": { + "name": "imgLogo" + }, + "position": { + "basis": "148px", + "shrink": 0 + }, + "props": { + "altText": "Prime Controls", + "fit": { + "mode": "contain" + }, + "source": "/system/images/PrimeControls/image%20(4).png", + "style": { + "marginRight": "12px" + } + }, + "type": "ia.display.image" + }, { "children": [ { @@ -178,7 +196,6 @@ "propConfig": { "props.value": { "binding": { - "bidirectional": true, "config": { "path": "session.custom.PrimeControls.query.rangePreset" }, @@ -231,6 +248,9 @@ "shrink": 0 }, "props": { + "image": { + "height": 15 + }, "primary": false, "text": "Refresh" }, @@ -256,7 +276,9 @@ "shrink": 0 }, "props": { - "primary": true, + "image": { + "height": 15 + }, "text": "Shift Report" }, "type": "ia.input.button" @@ -281,8 +303,7 @@ "name": "rowTop" }, "position": { - "basis": "52px", - "grow": 0, + "basis": "55px", "shrink": 0 }, "props": { @@ -315,8 +336,10 @@ } }, "props": { - "params": {}, - "path": "PrimeControls/Components/HealthGauge" + "path": "PrimeControls/Components/HealthGauge", + "style": { + "overflow": "hidden" + } }, "type": "ia.display.view" }, @@ -362,7 +385,10 @@ "params": { "label": "Activations" }, - "path": "PrimeControls/Components/KpiCard" + "path": "PrimeControls/Components/KpiCard", + "style": { + "overflow": "hidden" + } }, "type": "ia.display.view" }, @@ -409,7 +435,10 @@ "label": "Rate", "unit": "/hr" }, - "path": "PrimeControls/Components/KpiCard" + "path": "PrimeControls/Components/KpiCard", + "style": { + "overflow": "hidden" + } }, "type": "ia.display.view" }, @@ -455,7 +484,10 @@ "params": { "label": "Active Now" }, - "path": "PrimeControls/Components/KpiCard" + "path": "PrimeControls/Components/KpiCard", + "style": { + "overflow": "hidden" + } }, "type": "ia.display.view" }, @@ -501,7 +533,10 @@ "params": { "label": "Unacked" }, - "path": "PrimeControls/Components/KpiCard" + "path": "PrimeControls/Components/KpiCard", + "style": { + "overflow": "hidden" + } }, "type": "ia.display.view" }, @@ -547,7 +582,10 @@ "params": { "label": "MTTA" }, - "path": "PrimeControls/Components/KpiCard" + "path": "PrimeControls/Components/KpiCard", + "style": { + "overflow": "hidden" + } }, "type": "ia.display.view" }, @@ -593,7 +631,10 @@ "params": { "label": "Flood Time" }, - "path": "PrimeControls/Components/KpiCard" + "path": "PrimeControls/Components/KpiCard", + "style": { + "overflow": "hidden" + } }, "type": "ia.display.view" }, @@ -639,7 +680,10 @@ "params": { "label": "Chattering" }, - "path": "PrimeControls/Components/KpiCard" + "path": "PrimeControls/Components/KpiCard", + "style": { + "overflow": "hidden" + } }, "type": "ia.display.view" } @@ -651,7 +695,6 @@ "grow": 1 }, "props": { - "alignItems": "stretch", "style": { "gap": "10px" } @@ -671,4 +714,4 @@ }, "type": "ia.container.flex" } -} \ No newline at end of file +} diff --git a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Analysis/view.json b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Analysis/view.json index b3fb31d..3519e36 100644 --- a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Analysis/view.json +++ b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Analysis/view.json @@ -2343,6 +2343,11 @@ }, "selection": { "enableRowSelection": false + }, + "cells": { + "style": { + "padding": "5px" + } } }, "type": "ia.display.table" @@ -2455,4 +2460,4 @@ }, "type": "ia.container.flex" } -} \ No newline at end of file +} diff --git a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Tabs/BadActors/resource.json b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Tabs/BadActors/resource.json index 276dced..703a7a1 100644 --- a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Tabs/BadActors/resource.json +++ b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Tabs/BadActors/resource.json @@ -4,7 +4,14 @@ "restricted": false, "overridable": true, "files": [ - "view.json" + "view.json", + "thumbnail.png" ], - "attributes": {} -} + "attributes": { + "lastModificationSignature": "7504c7695ab45f2fc1d0d6dc8071e96746ac068f99e878d3ca1dcf804c9eea76", + "lastModification": { + "actor": "admin", + "timestamp": "2026-07-16T20:38:41Z" + } + } +} \ No newline at end of file diff --git a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Tabs/BadActors/view.json b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Tabs/BadActors/view.json index 50f49dc..b231024 100644 --- a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Tabs/BadActors/view.json +++ b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Tabs/BadActors/view.json @@ -1,8 +1,6 @@ { "custom": {}, - "params": { - "bundle": null - }, + "params": {}, "propConfig": { "params.bundle": { "paramDirection": "input", @@ -11,7 +9,6 @@ }, "props": { "defaultSize": { - "height": 800, "width": 1600 } }, @@ -151,6 +148,11 @@ } }, "props": { + "cells": { + "style": { + "padding": "5px" + } + }, "columns": [ { "field": "source", @@ -206,15 +208,15 @@ "header": { "title": "Share of Total" }, - "render": "number", "number": "progress", "progressBar": { - "min": 0, - "max": 100, "bar": { "color": "#3B7DD8" - } + }, + "max": 100, + "min": 0 }, + "render": "number", "width": 2.4 }, { @@ -228,8 +230,7 @@ } ], "pager": { - "bottom": false, - "top": false + "bottom": false }, "rows": { "height": 34 @@ -275,8 +276,7 @@ }, "position": { "basis": "320px", - "grow": 1, - "shrink": 1 + "grow": 1 }, "props": { "direction": "column", @@ -316,9 +316,16 @@ } }, "props": { - "text": "" + "style": { + "whiteSpace": "nowrap" + } }, - "type": "ia.display.label" + "type": "ia.display.label", + "position": { + "basis": "auto", + "grow": 0, + "shrink": 0 + } }, { "events": { @@ -336,6 +343,7 @@ "name": "icoClearFocus" }, "position": { + "basis": "20px", "shrink": 0 }, "props": { @@ -491,6 +499,11 @@ } }, "props": { + "cells": { + "style": { + "padding": "5px" + } + }, "columns": [ { "field": "source", @@ -552,8 +565,7 @@ } ], "pager": { - "bottom": false, - "top": false + "bottom": false }, "rows": { "height": 34 @@ -695,6 +707,11 @@ } }, "props": { + "cells": { + "style": { + "padding": "5px" + } + }, "columns": [ { "field": "source", @@ -746,8 +763,7 @@ } ], "pager": { - "bottom": false, - "top": false + "bottom": false }, "rows": { "height": 34 @@ -904,6 +920,11 @@ } }, "props": { + "cells": { + "style": { + "padding": "5px" + } + }, "columns": [ { "field": "source", @@ -956,8 +977,7 @@ } ], "pager": { - "bottom": false, - "top": false + "bottom": false }, "rows": { "height": 34 @@ -1020,11 +1040,9 @@ }, "position": { "basis": "280px", - "grow": 1, - "shrink": 1 + "grow": 1 }, "props": { - "alignItems": "stretch", "style": { "gap": "12px" } @@ -1063,7 +1081,6 @@ "type": "ia.container.flex" } ], - "custom": {}, "meta": { "name": "root" }, @@ -1075,4 +1092,4 @@ }, "type": "ia.container.flex" } -} \ No newline at end of file +} diff --git a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Journal/view.json b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Journal/view.json index 821acba..14a4ca2 100644 --- a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Journal/view.json +++ b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Journal/view.json @@ -408,6 +408,11 @@ "pager": { "bottom": false, "top": false + }, + "cells": { + "style": { + "padding": "5px" + } } }, "type": "ia.display.table" @@ -471,4 +476,4 @@ }, "type": "ia.container.flex" } -} \ No newline at end of file +} diff --git a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Overview/view.json b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Overview/view.json index c93abaf..f5d44cd 100644 --- a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Overview/view.json +++ b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Overview/view.json @@ -32,7 +32,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\treturn not (int(value['meta']['event_count']) > 0)\n\texcept:\n\t\treturn True", + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\ttry:\n\t\treturn not (int(value['meta']['event_count']) > 0)\n\texcept:\n\t\treturn True", "type": "script" } ], @@ -67,7 +67,7 @@ }, "transforms": [ { - "code": "\trows = []\n\ttry:\n\t\tfor r in (value['priority']['raw'] or []):\n\t\t\ttry:\n\t\t\t\trows.append({'name': r['name'], 'level': int(r['level']), 'count': int(r['count'])})\n\t\t\texcept:\n\t\t\t\tpass\n\texcept:\n\t\trows = []\n\trows.sort(key=lambda r: -r['level'])\n\ttotal = 0\n\tfor r in rows:\n\t\ttotal = total + r['count']\n\tout = []\n\tfor r in rows:\n\t\tif total > 0:\n\t\t\tp = 100.0 * r['count'] / total\n\t\telse:\n\t\t\tp = 0.0\n\t\tout.append({'priority_name': r['name'], 'count': r['count'], 'pct': p})\n\treturn out", + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\trows = []\n\ttry:\n\t\tfor r in (value['priority']['raw'] or []):\n\t\t\ttry:\n\t\t\t\trows.append({'name': r['name'], 'level': int(r['level']), 'count': int(r['count'])})\n\t\t\texcept:\n\t\t\t\tpass\n\texcept:\n\t\trows = []\n\trows.sort(key=lambda r: -r['level'])\n\ttotal = 0\n\tfor r in rows:\n\t\ttotal = total + r['count']\n\tout = []\n\tfor r in rows:\n\t\tif total > 0:\n\t\t\tp = 100.0 * r['count'] / total\n\t\telse:\n\t\t\tp = 0.0\n\t\tout.append({'priority_name': r['name'], 'count': r['count'], 'pct': p})\n\treturn out", "type": "script" } ], @@ -110,7 +110,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\treturn value['kpis']['active_now']\n\texcept:\n\t\treturn None", + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\ttry:\n\t\treturn value['kpis']['active_now']\n\texcept:\n\t\treturn None", "type": "script" } ], @@ -124,7 +124,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\treturn PrimeControls.fmt.num(value['kpis']['active_now']['value'])\n\texcept:\n\t\treturn '--'", + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\ttry:\n\t\treturn PrimeControls.fmt.num(value['kpis']['active_now']['value'])\n\texcept:\n\t\treturn '--'", "type": "script" } ], @@ -156,7 +156,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\treturn value['kpis']['unacked_now']\n\texcept:\n\t\treturn None", + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\ttry:\n\t\treturn value['kpis']['unacked_now']\n\texcept:\n\t\treturn None", "type": "script" } ], @@ -170,7 +170,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\treturn PrimeControls.fmt.num(value['kpis']['unacked_now']['value'])\n\texcept:\n\t\treturn '--'", + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\ttry:\n\t\treturn PrimeControls.fmt.num(value['kpis']['unacked_now']['value'])\n\texcept:\n\t\treturn '--'", "type": "script" } ], @@ -202,7 +202,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\treturn value['kpis']['shelved_now']\n\texcept:\n\t\treturn None", + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\ttry:\n\t\treturn value['kpis']['shelved_now']\n\texcept:\n\t\treturn None", "type": "script" } ], @@ -216,7 +216,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\treturn PrimeControls.fmt.num(value['kpis']['shelved_now']['value'])\n\texcept:\n\t\treturn '--'", + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\ttry:\n\t\treturn PrimeControls.fmt.num(value['kpis']['shelved_now']['value'])\n\texcept:\n\t\treturn '--'", "type": "script" } ], @@ -248,7 +248,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\treturn value['kpis']['standing_count']\n\texcept:\n\t\treturn None", + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\ttry:\n\t\treturn value['kpis']['standing_count']\n\texcept:\n\t\treturn None", "type": "script" } ], @@ -262,7 +262,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\treturn PrimeControls.fmt.num(value['kpis']['standing_count']['value'])\n\texcept:\n\t\treturn '--'", + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\ttry:\n\t\treturn PrimeControls.fmt.num(value['kpis']['standing_count']['value'])\n\texcept:\n\t\treturn '--'", "type": "script" } ], @@ -361,7 +361,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\treturn len(value['rate']['bins']) > 0\n\texcept:\n\t\treturn False", + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\ttry:\n\t\treturn len(value['rate']['bins']) > 0\n\texcept:\n\t\treturn False", "type": "script" } ], @@ -375,7 +375,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\trate = value['rate']\n\t\tbins = rate['bins'] or []\n\texcept:\n\t\treturn []\n\ttry:\n\t\ttarget = float(rate['target_per_bin'])\n\texcept:\n\t\ttarget = None\n\ttry:\n\t\tuse_day = float(value['meta']['window_hours']) > 24\n\texcept:\n\t\tuse_day = False\n\tout = []\n\tfor b in bins:\n\t\ttry:\n\t\t\tt0 = b['t0']\n\t\t\tc = int(b['count'])\n\t\texcept:\n\t\t\tcontinue\n\t\ttry:\n\t\t\tflood = bool(b['flood'])\n\t\texcept:\n\t\t\tflood = False\n\t\tif use_day:\n\t\t\tlbl = PrimeControls.fmt.day_clock(t0)\n\t\telse:\n\t\t\tlbl = PrimeControls.fmt.clock(t0)\n\t\trow = {'t': lbl, 'count_base': (0 if flood else c), 'count_flood': (c if flood else 0)}\n\t\tif target is not None:\n\t\t\trow['target'] = target\n\t\tout.append(row)\n\treturn out", + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\ttry:\n\t\trate = value['rate']\n\t\tbins = rate['bins'] or []\n\texcept:\n\t\treturn []\n\ttry:\n\t\ttarget = float(rate['target_per_bin'])\n\texcept:\n\t\ttarget = None\n\ttry:\n\t\tuse_day = float(value['meta']['window_hours']) > 24\n\texcept:\n\t\tuse_day = False\n\tout = []\n\tfor b in bins:\n\t\ttry:\n\t\t\tt0 = b['t0']\n\t\t\tc = int(b['count'])\n\t\texcept:\n\t\t\tcontinue\n\t\ttry:\n\t\t\tflood = bool(b['flood'])\n\t\texcept:\n\t\t\tflood = False\n\t\tif use_day:\n\t\t\tlbl = PrimeControls.fmt.day_clock(t0)\n\t\telse:\n\t\t\tlbl = PrimeControls.fmt.clock(t0)\n\t\trow = {'t': lbl, 'count_base': (0 if flood else c), 'count_flood': (c if flood else 0)}\n\t\tif target is not None:\n\t\t\trow['target'] = target\n\t\tout.append(row)\n\treturn out", "type": "script" } ], @@ -426,7 +426,7 @@ "labels": { "font": { "color": "", - "size": 16, + "size": 12, "weight": 400 }, "text": "[bold]{name}[/]" @@ -439,14 +439,14 @@ "topRight": 0 }, "enabled": true, - "height": 20, + "height": 12, "icon": { "color": "", - "height": 20, + "height": 12, "horizontalCenter": "left", "path": "", "verticalCenter": "top", - "width": 20 + "width": 12 }, "mirrorLookOfSeries": true, "stroke": { @@ -454,7 +454,7 @@ "opacity": 0, "width": 1 }, - "width": 20 + "width": 12 }, "position": "bottom" }, @@ -1301,13 +1301,13 @@ { "appearance": { "font": { - "size": "", + "size": 11, "weight": 500 }, "grid": { "color": "", "dashArray": "", - "minDistance": 60, + "minDistance": 140, "opacity": 0, "position": 0.5 }, @@ -1317,7 +1317,7 @@ "horizontalCenter": "middle", "opacity": 1, "rotation": 0, - "verticalCenter": "middle" + "verticalCenter": "top" }, "opposite": false }, @@ -1390,7 +1390,7 @@ { "appearance": { "font": { - "size": "", + "size": 11, "weight": 500 }, "grid": { @@ -1496,7 +1496,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\treturn len(value['rate']['bins']) == 0\n\texcept:\n\t\treturn True", + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\ttry:\n\t\treturn len(value['rate']['bins']) == 0\n\texcept:\n\t\treturn True", "type": "script" } ], @@ -1565,7 +1565,7 @@ }, "transforms": [ { - "code": "\tout = []\n\ttry:\n\t\trows = value['insights'] or []\n\texcept:\n\t\trows = []\n\tfor r in rows:\n\t\ttry:\n\t\t\tout.append({'severity': int(r['severity']), 'icon': r['icon'], 'text': r['text'], 'tab': r['tab']})\n\t\texcept:\n\t\t\tpass\n\treturn out", + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\tout = []\n\ttry:\n\t\trows = value['insights'] or []\n\texcept:\n\t\trows = []\n\tfor r in rows:\n\t\ttry:\n\t\t\tout.append({'severity': int(r['severity']), 'icon': r['icon'], 'text': r['text'], 'tab': r['tab']})\n\t\texcept:\n\t\t\tpass\n\treturn out", "type": "script" } ], @@ -1605,7 +1605,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\treturn len(value['insights']) == 0\n\texcept:\n\t\treturn True", + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\ttry:\n\t\treturn len(value['insights']) == 0\n\texcept:\n\t\treturn True", "type": "script" } ], @@ -1671,7 +1671,7 @@ }, "transforms": [ { - "code": "\tout = []\n\ttry:\n\t\trows = value['top_sources'] or []\n\texcept:\n\t\trows = []\n\tfor r in rows:\n\t\ttry:\n\t\t\tout.append({'rank': int(r['rank']), 'source': r['source'], 'label': r['label'], 'count': int(r['count']), 'pct': float(r['pct'])})\n\t\texcept:\n\t\t\tpass\n\treturn out", + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\tout = []\n\ttry:\n\t\trows = value['top_sources'] or []\n\texcept:\n\t\trows = []\n\tfor r in rows:\n\t\ttry:\n\t\t\tout.append({'rank': int(r['rank']), 'source': r['source'], 'label': r['label'], 'count': int(r['count']), 'pct': float(r['pct'])})\n\t\texcept:\n\t\t\tpass\n\treturn out", "type": "script" } ], @@ -1711,7 +1711,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\treturn len(value['top_sources']) == 0\n\texcept:\n\t\treturn True", + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\ttry:\n\t\treturn len(value['top_sources']) == 0\n\texcept:\n\t\treturn True", "type": "script" } ], @@ -1776,7 +1776,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\treturn int(value['meta']['event_count']) > 0\n\texcept:\n\t\treturn False", + "code": "\ttry:\n\t\tif value['overview']:\n\t\t\tvalue = value['overview']\n\texcept:\n\t\tpass\n\ttry:\n\t\treturn int(value['meta']['event_count']) > 0\n\texcept:\n\t\treturn False", "type": "script" } ], @@ -1804,4 +1804,4 @@ }, "type": "ia.container.flex" } -} \ No newline at end of file +} diff --git a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.vision/client-tags/data.bin b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.vision/client-tags/data.bin new file mode 100644 index 0000000000000000000000000000000000000000..87e842c294fc1515b8a4c4b7374ea967515bccc1 GIT binary patch literal 342 zcmV-c0jd5UiwFP!00000|8-HlPC_vl{VF0C5W&qDhdY_LH02rri5rF(23>f8S_)Qg z>Gif3;{!MvH{<56gNbjUFXZZ1YMiu5^PQgWr>Cb+dk-&zuhH52{pZoiM5k>$dk$QxZi1ta02IB^1tjiHKknt4+U~`(I ztQ(VG(S^*dGIQih0K39ys^*hIIJJLh9;dzFS3mDoYK-duCpx&vVcUvA1Fn^2`ERwR3N^n oLc9mnAO^^Gct6y9OFaso0gW07egKJofS=U-27sOIeBc290FqFjNdN!< literal 0 HcmV?d00001 diff --git a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.vision/client-tags/resource.json b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.vision/client-tags/resource.json new file mode 100644 index 0000000..b083298 --- /dev/null +++ b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.vision/client-tags/resource.json @@ -0,0 +1,16 @@ +{ + "scope": "C", + "version": 1, + "restricted": false, + "overridable": true, + "files": [ + "data.bin" + ], + "attributes": { + "lastModificationSignature": "71cfaca87ea3ea9c779495b13fa308d24deb72d86d5d3423e8cd0d949609a7d3", + "lastModification": { + "actor": "admin", + "timestamp": "2026-07-16T20:02:07Z" + } + } +} \ No newline at end of file diff --git a/ignition/gateway/projects/PrimeBAT/ignition/script-python/PrimeControls/alarms/code.py b/ignition/gateway/projects/PrimeBAT/ignition/script-python/PrimeControls/alarms/code.py index 33c3078..f51522e 100644 --- a/ignition/gateway/projects/PrimeBAT/ignition/script-python/PrimeControls/alarms/code.py +++ b/ignition/gateway/projects/PrimeBAT/ignition/script-python/PrimeControls/alarms/code.py @@ -1,8 +1,4 @@ -# PrimeControls.alarms - gateway adapter (the ONLY impure module). -# Journal access via system.alarm.queryJournal/queryStatus exclusively - no SQL, -# no journal name assumptions, every query bounded by the caller's date range. -# Jython 2.7. Bare except is used on defensive paths deliberately: java exceptions -# do not subclass Python Exception and would bypass 'except Exception'. + from __future__ import division, print_function import sys @@ -113,6 +109,10 @@ def _fetch_journal(start_ms, end_ms, max_events, include_data=False): truncated = False try: kwargs = {"startDate": int(start_ms), "endDate": int(end_ms)} + if include_data: + # Required for ack attribution: without includeData the journal + # omits associated data, so evt.get("ackUserName") is always None. + kwargs["includeData"] = True result = system.alarm.queryJournal(**kwargs) for evt in result: r = _norm_event(evt) @@ -226,6 +226,11 @@ def getDashboardBundle(startMs, endMs, options=None): if err: bundle["meta"]["error"] = err bundle["insights"] = fmt.insights(bundle) + try: + if bundle.get("overview"): + bundle["overview"]["insights"] = fmt.insights(bundle["overview"]) + except: + pass return bundle except: try: @@ -242,7 +247,8 @@ STATE_LABELS = {"active": "Active", "clear": "Cleared", "ack": "Acked"} def _journal_rows(start_ms, end_ms, filters, max_rows): - rows, dropped, truncated, err = _fetch_journal(start_ms, end_ms, max_rows) + rows, dropped, truncated, err = _fetch_journal(start_ms, end_ms, max_rows, + include_data=True) out = [] prios = set(filters.get("priorities") or []) areas = set(filters.get("areas") or []) @@ -307,7 +313,8 @@ def getSourceDetail(source, startMs, endMs): s = int(startMs) e = int(endMs) src = str(source or "") - rows, dropped, truncated, err = _fetch_journal(s, e, int(calc.DEFAULTS["max_events"])) + rows, dropped, truncated, err = _fetch_journal(s, e, int(calc.DEFAULTS["max_events"]), + include_data=True) mine = [r for r in rows if r["source"] == src] instances, _mode = calc.correlate_instances(mine) parsed = calc.parse_source(src, mine[0]["display_path"] if mine else "") diff --git a/ignition/gateway/projects/PrimeBAT/ignition/script-python/PrimeControls/alarms/resource.json b/ignition/gateway/projects/PrimeBAT/ignition/script-python/PrimeControls/alarms/resource.json index e160ebe..d7b2ab7 100644 --- a/ignition/gateway/projects/PrimeBAT/ignition/script-python/PrimeControls/alarms/resource.json +++ b/ignition/gateway/projects/PrimeBAT/ignition/script-python/PrimeControls/alarms/resource.json @@ -7,6 +7,11 @@ "code.py" ], "attributes": { - "hintScope": 2 + "hintScope": 2, + "lastModificationSignature": "1b496e4409991a871c59e69297bc2abbb576020fbf0643f46ecd1890921c4f69", + "lastModification": { + "actor": "admin", + "timestamp": "2026-07-16T21:24:34Z" + } } -} +} \ No newline at end of file diff --git a/ignition/gateway/projects/PrimeBAT/ignition/script-python/PrimeControls/calc/code.py b/ignition/gateway/projects/PrimeBAT/ignition/script-python/PrimeControls/calc/code.py index e8af1f0..4720525 100644 --- a/ignition/gateway/projects/PrimeBAT/ignition/script-python/PrimeControls/calc/code.py +++ b/ignition/gateway/projects/PrimeBAT/ignition/script-python/PrimeControls/calc/code.py @@ -1,5 +1,4 @@ -# PrimeControls.calc - PURE alarm analytics computation (no system.*, no I/O, no clock). -# Runs under Jython 2.7 (Ignition) AND CPython 3 (pytest). No f-strings. + from __future__ import division, print_function DEFAULTS = { @@ -117,7 +116,7 @@ def _new_instance(row): "priority": lvl, "priority_name": name, "active_ms": None, "clear_ms": None, "ack_ms": None, "duration_ms": None, "tta_ms": None, "open": True, - "ack_user": None} + "ack_user": None, "n_events": 0} def _finalize(inst): @@ -152,6 +151,7 @@ def correlate_instances(rows): ident = r break inst = _new_instance(ident or grp[0]) + inst["n_events"] = len(grp) acts = [r["ts"] for r in grp if r["state"] == "active"] clears = [r["ts"] for r in grp if r["state"] == "clear"] acks = [r["ts"] for r in grp if r["state"] == "ack"] @@ -173,12 +173,14 @@ def correlate_instances(rows): instances.append(_finalize(open_by_src.pop(src))) inst = _new_instance(r) inst["active_ms"] = r["ts"] + inst["n_events"] = 1 open_by_src[src] = inst else: inst = open_by_src.get(src) if inst is None: inst = _new_instance(r) open_by_src[src] = inst + inst["n_events"] = inst.get("n_events", 0) + 1 if st == "clear" and inst["clear_ms"] is None: inst["clear_ms"] = r["ts"] elif st == "ack" and inst["ack_ms"] is None: @@ -242,6 +244,37 @@ def apply_filters(instances, filters): return out +def apply_status_filters(status_rows, filters): + """Same filter semantics as apply_filters, but for queryStatus rows + (active-now list): keys source/display_path/priority/unacked.""" + if not filters: + return status_rows + prios = set(filters.get("priorities") or []) + areas = set(filters.get("areas") or []) + states = set(filters.get("states") or []) + search = (filters.get("search") or "").strip().lower() + if not prios and not areas and not states and not search: + return status_rows + out = [] + for s in status_rows or []: + if prios and s.get("priority") not in prios: + continue + parsed = parse_source(s.get("source"), s.get("display_path")) + if areas and parsed["area"] not in areas: + continue + if states: + tags = set(["active"]) + tags.add("unacked" if s.get("unacked") else "acked") + if not (states & tags): + continue + if search: + hay = ((s.get("source") or "") + " " + parsed["label"]).lower() + if search not in hay: + continue + out.append(s) + return out + + # ---------------- rate / floods ---------------- @@ -639,9 +672,50 @@ def build_bundle(rows, active_now, shelved_count, start_ms, end_ms, now_ms, opts start_ms, end_ms, now_ms = 0, 0, 0 filters = (opts or {}).get("filters") if opts else None instances, mode = correlate_instances(rows) + all_instances = instances + all_active_now = active_now instances = apply_filters(instances, filters) - current, prior, orphans = partition_instances(instances, start_ms, end_ms) + active_now = apply_status_filters(active_now, filters) + + # Filter dropdown options enumerate the UNfiltered data, so applying a + # filter never collapses the available choices. + areas = sorted(set([i["area"] for i in all_instances] + + [parse_source(s.get("source"), s.get("display_path"))["area"] + for s in all_active_now or []])) + prios_seen = sorted(set([(i["priority"], i["priority_name"]) + for i in all_instances])) + + bundle = _assemble(instances, active_now, shelved_count, start_ms, end_ms, + now_ms, mode, areas, prios_seen, o) + if filters_active(filters): + # Overview tab stays a plant-wide (unfiltered) summary; every other + # consumer binds the filtered top-level sections. + bundle["overview"] = _assemble(all_instances, all_active_now, + shelved_count, start_ms, end_ms, + now_ms, mode, areas, prios_seen, o) + else: + bundle["overview"] = None + return bundle + + +def filters_active(filters): + if not filters: + return False + try: + return bool(filters.get("priorities") or filters.get("areas") or + filters.get("states") or + (filters.get("search") or "").strip()) + except Exception: + return False + + +def _assemble(instances, active_now, shelved_count, start_ms, end_ms, now_ms, + mode, areas, prios_seen, o): window_h = _safe_div(end_ms - start_ms, 3600000.0) or 0.0 + filtered_events = 0 + for i in instances: + filtered_events += i.get("n_events", 0) + current, prior, orphans = partition_instances(instances, start_ms, end_ms) bins = rate_bins(current, start_ms, end_ms, o) floods = flood_episodes(bins, current, start_ms, end_ms, o) @@ -692,9 +766,6 @@ def build_bundle(rows, active_now, shelved_count, start_ms, end_ms, now_ms, opts if not has_activity and not (active_now or []): health = {"grade": None, "score": None, "subs": health["subs"]} - areas = sorted(set([i["area"] for i in current] + - [r["area"] for r in stand["rows"]])) - prios_seen = sorted(set([(i["priority"], i["priority_name"]) for i in current])) active_now_rows = [] for s in (active_now or [])[:int(o["active_now_cap"])]: parsed = parse_source(s.get("source"), s.get("display_path")) @@ -709,7 +780,7 @@ def build_bundle(rows, active_now, shelved_count, start_ms, end_ms, now_ms, opts return { "meta": {"start_ms": start_ms, "end_ms": end_ms, "prior_start_ms": p_start, "now_ms": now_ms, "window_hours": window_h, - "event_count": len(rows or []), "activation_count": n_act, + "event_count": filtered_events, "activation_count": n_act, "dropped_rows": 0, "truncated": False, "correlation_mode": mode, "standing_mode": stand["mode"], "areas": areas, diff --git a/ignition/gateway/projects/PrimeBAT/ignition/script-python/PrimeControls/calc/resource.json b/ignition/gateway/projects/PrimeBAT/ignition/script-python/PrimeControls/calc/resource.json index e160ebe..e9edd17 100644 --- a/ignition/gateway/projects/PrimeBAT/ignition/script-python/PrimeControls/calc/resource.json +++ b/ignition/gateway/projects/PrimeBAT/ignition/script-python/PrimeControls/calc/resource.json @@ -7,6 +7,11 @@ "code.py" ], "attributes": { - "hintScope": 2 + "hintScope": 2, + "lastModificationSignature": "d6bc71bbf780509f382cf189b57f0700a614594e1af3552d7b1c9b03b8edf36c", + "lastModification": { + "actor": "admin", + "timestamp": "2026-07-16T21:24:39Z" + } } -} +} \ No newline at end of file diff --git a/ignition/gateway/projects/PrimeBAT/ignition/script-python/PrimeControls/fmt/code.py b/ignition/gateway/projects/PrimeBAT/ignition/script-python/PrimeControls/fmt/code.py index c9c2f00..906bd4b 100644 --- a/ignition/gateway/projects/PrimeBAT/ignition/script-python/PrimeControls/fmt/code.py +++ b/ignition/gateway/projects/PrimeBAT/ignition/script-python/PrimeControls/fmt/code.py @@ -1,4 +1,4 @@ -# PrimeControls.fmt - PURE formatting, insights, shift report (py2/3, no f-strings). + from __future__ import division, print_function import time as _time diff --git a/ignition/gateway/projects/PrimeBAT/ignition/script-python/PrimeControls/fmt/resource.json b/ignition/gateway/projects/PrimeBAT/ignition/script-python/PrimeControls/fmt/resource.json index e160ebe..251a908 100644 --- a/ignition/gateway/projects/PrimeBAT/ignition/script-python/PrimeControls/fmt/resource.json +++ b/ignition/gateway/projects/PrimeBAT/ignition/script-python/PrimeControls/fmt/resource.json @@ -7,6 +7,11 @@ "code.py" ], "attributes": { - "hintScope": 2 + "hintScope": 2, + "lastModificationSignature": "81cbca2cf7929734afa020563f85f52707e991de359d97175601faec1c13bdce", + "lastModification": { + "actor": "admin", + "timestamp": "2026-07-16T21:24:42Z" + } } -} +} \ No newline at end of file diff --git a/ignition/gateway/projects/SimHarness/ignition/script-python/probe/code.py b/ignition/gateway/projects/SimHarness/ignition/script-python/probe/code.py index b6b88cb..f4d898e 100644 --- a/ignition/gateway/projects/SimHarness/ignition/script-python/probe/code.py +++ b/ignition/gateway/projects/SimHarness/ignition/script-python/probe/code.py @@ -11,7 +11,91 @@ import json OUT_DIR = "/usr/local/bin/ignition/data/projects/.probe" -PROBE_NAME = "P1" +PROBE_NAME = "P2" + + +def _load_pc(): + """Exec PrimeBAT's real PrimeControls modules (calc/fmt/alarms) into a + package object and return it (shared by _p1/_p2).""" + import imp + base = "/usr/local/bin/ignition/data/projects/PrimeBAT/ignition/script-python/PrimeControls/" + pkg = imp.new_module("PrimeControls") + pkg.__path__ = [base] + sys_modules = __import__("sys").modules + sys_modules["PrimeControls"] = pkg + for name in ("calc", "fmt", "alarms"): + m = imp.new_module("PrimeControls." + name) + m.__dict__["system"] = system + code = open(base + name + "/code.py").read() + compiled = compile(code, name, "exec") + exec compiled in m.__dict__ + sys_modules["PrimeControls." + name] = m + setattr(pkg, name, m) + return pkg + + +def _p2(): + """Ack-user visibility + filtered-bundle ground truth.""" + out = {} + end = system.date.toMillis(system.date.now()) + start = end - 4 * 3600 * 1000 + + def ack_facts(entries): + acks = [] + for e in entries: + try: + if e.getAckData() is None: + continue + except: + continue + f = {} + try: + f["ackUserName"] = repr(e.get("ackUserName"))[:80] + except: + f["ackUserName"] = "ERR" + try: + f["ackUser"] = repr(e.get("ackUser"))[:120] + except: + f["ackUser"] = "ERR" + acks.append(f) + named = [a for a in acks if a["ackUserName"] not in ("None", "ERR", "u''", "''")] + return {"ack_count": len(acks), "named_count": len(named), + "samples": named[:3] or acks[:3]} + + try: + r = system.alarm.queryJournal(startDate=start, endDate=end) + out["no_includeData"] = ack_facts([e for e in r]) + except: + out["no_includeData"] = "ERR" + try: + r = system.alarm.queryJournal(startDate=start, endDate=end, includeData=True) + out["includeData_true"] = ack_facts([e for e in r]) + except: + out["includeData_true"] = "ERR" + + pkg = _load_pc() + al = pkg.alarms + + def pick(b): + return {"event_count": b["meta"]["event_count"], + "activation_count": b["meta"]["activation_count"], + "areas_n": len(b["meta"]["areas"]), + "prios_seen": [p["label"] for p in b["meta"]["priorities_seen"]], + "kpis": dict((k, b["kpis"][k]["value"]) for k in b["kpis"]), + "pareto_top": (b["pareto"]["rows"][0]["label"], b["pareto"]["rows"][0]["count"]) if b["pareto"]["rows"] else None, + "overview_act": (b.get("overview") or {}).get("meta", {}).get("activation_count"), + "overview_insights": len((b.get("overview") or {}).get("insights") or []), + "error": b["meta"]["error"]} + out["bundle_all"] = pick(al.getDashboardBundle(start, end, None)) + out["bundle_medium"] = pick(al.getDashboardBundle(start, end, {"priorities": [2]})) + out["bundle_area_tankfarm"] = pick(al.getDashboardBundle(start, end, {"areas": ["TankFarm"]})) + + d = al.getSourceDetail( + "prov:default:/tag:BuildathonSim/Intake/ConveyorJam_Chatter:/alm:ConveyorJam_Chatter", + start, end) + out["source_detail_top_ack"] = d["stats"]["top_ack_users"] + out["source_detail_error"] = d["error"] + return out def _p1(): @@ -227,6 +311,6 @@ def _p0(): def run(): result = {"probe": PROBE_NAME, "ranAt": str(system.date.now())} - result["payload"] = _safe(_p1) + result["payload"] = _safe(_p2) _write(result) system.util.getLogger("probe").info("probe %s written to %s" % (PROBE_NAME, OUT_DIR)) diff --git a/ignition/gateway/projects/SimHarness/ignition/timer/ProbeTick/resource.json b/ignition/gateway/projects/SimHarness/ignition/timer/ProbeTick/resource.json index 943ccb0..1b34bba 100644 --- a/ignition/gateway/projects/SimHarness/ignition/timer/ProbeTick/resource.json +++ b/ignition/gateway/projects/SimHarness/ignition/timer/ProbeTick/resource.json @@ -12,4 +12,4 @@ "fixedDelay": true, "enabled": false } -} \ No newline at end of file +} diff --git a/test-data/simulation_tags.json b/test-data/simulation_tags.json index b79edb0..0b6985b 100644 --- a/test-data/simulation_tags.json +++ b/test-data/simulation_tags.json @@ -34,7 +34,7 @@ "label": "Intake Pump 2 Fault", "mode": "Equality", "setpointA": 1.0, - "priority": "High" + "priority": "Medium" } ] }, @@ -50,7 +50,7 @@ "label": "Intake Screen Diff Press High", "mode": "Equality", "setpointA": 1.0, - "priority": "Low" + "priority": "Medium" } ] }, @@ -264,7 +264,7 @@ "label": "Fuel Gas Pressure Low", "mode": "Equality", "setpointA": 1.0, - "priority": "Low" + "priority": "Medium" } ] }, @@ -280,7 +280,7 @@ "label": "Economizer Diff Press High", "mode": "Equality", "setpointA": 1.0, - "priority": "Low" + "priority": "Medium" } ] }, @@ -398,7 +398,7 @@ "label": "Conveyor Motor Overload", "mode": "Equality", "setpointA": 1.0, - "priority": "High" + "priority": "Medium" } ] }, @@ -430,7 +430,7 @@ "label": "Date Coder Ink Low", "mode": "Equality", "setpointA": 1.0, - "priority": "Low" + "priority": "Medium" } ] }, @@ -580,7 +580,7 @@ "label": "Glycol Supply Temp High", "mode": "Equality", "setpointA": 1.0, - "priority": "Low" + "priority": "Medium" } ] }, @@ -596,7 +596,7 @@ "label": "Nitrogen Pressure Low", "mode": "Equality", "setpointA": 1.0, - "priority": "Low" + "priority": "Medium" } ] }, @@ -698,7 +698,7 @@ "label": "Tank 103 Temp High", "mode": "Equality", "setpointA": 1.0, - "priority": "Low" + "priority": "Medium" } ] }, @@ -714,7 +714,7 @@ "label": "Transfer Pump Fault", "mode": "Equality", "setpointA": 1.0, - "priority": "High" + "priority": "Medium" } ] }, diff --git a/tests/test_calc.py b/tests/test_calc.py index 60e9329..92b0a2e 100644 --- a/tests/test_calc.py +++ b/tests/test_calc.py @@ -301,6 +301,56 @@ def test_build_bundle_filters(): start, end = T0, T0 + 4 * H b = calc.build_bundle(synth_rows(), [], None, start, end, end, {"filters": {"areas": ["Area2"]}}) - assert all(a == "Area2" for a in b["meta"]["areas"]) # Area2 = 5 baseline rows (i % 4 == 2) + 18 flood rows assert b["meta"]["activation_count"] == 23 + # dropdown options enumerate the UNfiltered data (never collapse) + assert len(b["meta"]["areas"]) >= 4 + # event_count reflects only events of filtered instances + b_all = calc.build_bundle(synth_rows(), [], None, start, end, end) + assert 0 < b["meta"]["event_count"] < b_all["meta"]["event_count"] + + +def test_build_bundle_overview_stays_unfiltered(): + start, end = T0, T0 + 4 * H + b_all = calc.build_bundle(synth_rows(), [], None, start, end, end) + assert b_all["overview"] is None # no filters -> Overview uses main bundle + b = calc.build_bundle(synth_rows(), [], None, start, end, end, + {"filters": {"areas": ["Area2"]}}) + ov = b["overview"] + assert ov is not None + # main sections are filtered; overview mirrors the unfiltered plant view + assert b["meta"]["activation_count"] == 23 + assert ov["meta"]["activation_count"] == b_all["meta"]["activation_count"] + assert ov["meta"]["event_count"] == b_all["meta"]["event_count"] + assert len(ov["pareto"]["rows"]) == len(b_all["pareto"]["rows"]) + assert ov.get("overview", None) is None # no recursion + + +def test_build_bundle_filters_apply_to_status_kpis(): + start, end = T0, T0 + 4 * H + status = [ + {"source": "prov:default:/tag:Plant/Area1/S1:/alm:S1", "display_path": "", + "priority": 3, "priority_name": "High", "active_ms": T0, "unacked": True}, + {"source": "prov:default:/tag:Plant/Area2/S2:/alm:S2", "display_path": "", + "priority": 1, "priority_name": "Low", "active_ms": T0, "unacked": False}, + ] + b = calc.build_bundle(synth_rows(), status, 0, start, end, end, + {"filters": {"areas": ["Area2"]}}) + assert b["kpis"]["active_now"]["value"] == 1 + assert b["kpis"]["unacked_now"]["value"] == 0 + assert all(r["area"] == "Area2" for r in b["active_now"]) + + +def test_apply_status_filters_semantics(): + rows = [ + {"source": "prov:default:/tag:Plant/Area1/Pump:/alm:Pump", "display_path": "", + "priority": 4, "unacked": True}, + {"source": "prov:default:/tag:Plant/Area2/Valve:/alm:Valve", "display_path": "", + "priority": 1, "unacked": False}, + ] + assert calc.apply_status_filters(rows, None) == rows + assert calc.apply_status_filters(rows, {"priorities": [4]}) == [rows[0]] + assert calc.apply_status_filters(rows, {"states": ["unacked"]}) == [rows[0]] + assert calc.apply_status_filters(rows, {"states": ["acked"]}) == [rows[1]] + assert calc.apply_status_filters(rows, {"states": ["active"]}) == rows + assert calc.apply_status_filters(rows, {"search": "valve"}) == [rows[1]] diff --git a/tools/lint_project.py b/tools/lint_project.py index 1a53634..724c011 100644 --- a/tools/lint_project.py +++ b/tools/lint_project.py @@ -201,7 +201,13 @@ def main(): if view is None: continue rv = rel(vfile) + # Params are declared in "params" (nulls) and/or "propConfig" ("params.X"). + # Designer saves strip null-valued "params" entries, so propConfig is the + # durable declaration. declared_params = set((view.get("params") or {}).keys()) + for pc_key in (view.get("propConfig") or {}): + if pc_key.startswith("params."): + declared_params.add(pc_key.split(".")[1]) raw = open(vfile).read() for rx, msg in STRING_BANS: