Heatmap drill-down to Journal, layout fixes, healthy-plant sim mode (B grade)

Heatmap -> Journal drill-down (PrimeBAT only):
- clicking an Activations-by-hour/day cell jumps to the Journal filtered to
  that recurring day-of-week + hour (new query.filterDow/filterHour session
  props; same local-time bucketing as calc.heatmap)
- Journal shows a "Time filter: Thu 12:00-12:59" chip with an X to clear;
  filter composes with existing filters and is included in CSV export
- getJournalPage clamps out-of-range pages when a filter shrinks the set

Layout:
- filter bar moved below the tab strip so the tabs no longer shift when the
  bar hides on Overview (tab bar y verified pixel-stable across tabs)
- popup titles (AlarmDetail/EventDetail) auto-shrink their font to fit long
  source paths on one line without colliding with the state/priority chips

Alarm health -> B (score ~88):
- alarmsim gains a MODE flag: "healthy" (default) generates ~12 activations/hr
  with an 80/15/5 priority mix and no chatter/standing/fleeting/floods;
  "chaos" restores the original stress profile
- SimHarness gains a SimReset one-shot timer (disabled) that clears all sim
  tags; probe P3 reports health-grade ground truth for the 8h window
- Dashboard onStartup now re-anchors relative range presets (4h/8h/24h/7d)
  to now on session start - fixes stale Designer-baked startMs/endMs pinning
  every new session to an old window

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 09:27:47 -05:00
parent d353c5001e
commit 4b07272556
14 changed files with 377 additions and 158 deletions

View File

@@ -188,7 +188,8 @@ def _fetch_shelved():
def _clean_filters(options):
f = {"priorities": [], "areas": [], "states": [], "search": ""}
f = {"priorities": [], "areas": [], "states": [], "search": "",
"dow": -1, "hour": -1}
if not options:
return f
try:
@@ -202,6 +203,12 @@ def _clean_filters(options):
f["search"] = str(options.get("search") or "")
except:
pass
# Journal-only heatmap drill-down: day-of-week (0=Mon) + hour, -1 = off.
for k in ("dow", "hour"):
try:
f[k] = int(options.get(k) if options.get(k) is not None else -1)
except:
f[k] = -1
return f
@@ -254,9 +261,23 @@ def _journal_rows(start_ms, end_ms, filters, max_rows):
areas = set(filters.get("areas") or [])
states = set(filters.get("states") or [])
search = (filters.get("search") or "").strip().lower()
dow = filters.get("dow", -1)
hour = filters.get("hour", -1)
if dow >= 0 or hour >= 0:
import time
for r in rows:
if r["is_system"]:
continue
# same local-time bucketing as calc.heatmap (tm_wday 0=Mon, tm_hour)
if dow >= 0 or hour >= 0:
try:
lt = time.localtime(r["ts"] / 1000.0)
if dow >= 0 and lt.tm_wday != dow:
continue
if hour >= 0 and lt.tm_hour != hour:
continue
except:
continue
parsed = calc.parse_source(r["source"], r["display_path"])
if prios and r["priority"] not in prios:
continue
@@ -286,6 +307,9 @@ def getJournalPage(startMs, endMs, filters=None, page=0, pageSize=50):
int(calc.DEFAULTS["max_events"]))
page = max(0, int(page or 0))
page_size = max(1, int(pageSize or 50))
last_page = max(0, (len(rows) - 1) // page_size)
if page > last_page:
page = last_page
start = page * page_size
return {"rows": rows[start:start + page_size], "total": len(rows),
"page": page, "page_size": page_size, "truncated": truncated,