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:
@@ -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 "")
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
"code.py"
|
||||
],
|
||||
"attributes": {
|
||||
"hintScope": 2
|
||||
"hintScope": 2,
|
||||
"lastModificationSignature": "1b496e4409991a871c59e69297bc2abbb576020fbf0643f46ecd1890921c4f69",
|
||||
"lastModification": {
|
||||
"actor": "admin",
|
||||
"timestamp": "2026-07-16T21:24:34Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
"code.py"
|
||||
],
|
||||
"attributes": {
|
||||
"hintScope": 2
|
||||
"hintScope": 2,
|
||||
"lastModificationSignature": "d6bc71bbf780509f382cf189b57f0700a614594e1af3552d7b1c9b03b8edf36c",
|
||||
"lastModification": {
|
||||
"actor": "admin",
|
||||
"timestamp": "2026-07-16T21:24:39Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
"code.py"
|
||||
],
|
||||
"attributes": {
|
||||
"hintScope": 2
|
||||
"hintScope": 2,
|
||||
"lastModificationSignature": "81cbca2cf7929734afa020563f85f52707e991de359d97175601faec1c13bdce",
|
||||
"lastModification": {
|
||||
"actor": "admin",
|
||||
"timestamp": "2026-07-16T21:24:42Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user