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 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 16:27:48 -05:00
parent b70b4d6991
commit d353c5001e
165 changed files with 13988 additions and 525 deletions

View File

@@ -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 "")

View File

@@ -7,6 +7,11 @@
"code.py"
],
"attributes": {
"hintScope": 2
"hintScope": 2,
"lastModificationSignature": "1b496e4409991a871c59e69297bc2abbb576020fbf0643f46ecd1890921c4f69",
"lastModification": {
"actor": "admin",
"timestamp": "2026-07-16T21:24:34Z"
}
}
}
}

View File

@@ -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,

View File

@@ -7,6 +7,11 @@
"code.py"
],
"attributes": {
"hintScope": 2
"hintScope": 2,
"lastModificationSignature": "d6bc71bbf780509f382cf189b57f0700a614594e1af3552d7b1c9b03b8edf36c",
"lastModification": {
"actor": "admin",
"timestamp": "2026-07-16T21:24:39Z"
}
}
}
}

View File

@@ -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

View File

@@ -7,6 +7,11 @@
"code.py"
],
"attributes": {
"hintScope": 2
"hintScope": 2,
"lastModificationSignature": "81cbca2cf7929734afa020563f85f52707e991de359d97175601faec1c13bdce",
"lastModification": {
"actor": "admin",
"timestamp": "2026-07-16T21:24:42Z"
}
}
}
}