Dashboard shell: bundle expr binding + script transform, onStartup range init, hand-rolled tab bar (position.display exprs), CONTRACT.md frozen pending G1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
14 KiB
CONTRACT.md — PrimeBAT build contract (FROZEN after Gate G1)
Every builder/verifier agent works from this file. Do not deviate; changes require the
integrator to edit this file first. Repo: /home/bpeck/git/buildathon. Project root:
ignition/gateway/projects/PrimeBAT/. Reload: python3 tools/provision.py scan-projects.
Lint: python3 tools/lint_project.py (must exit 0). Component prop schemas (ground truth
for every prop name): /tmp/claude-1000/-home-bpeck-git-buildathon/a7f89b44-360c-4f23-ab63-503f3c25b53b/scratchpad/schemas/components/<component-id>.schema.json.
Format exemplar (READ IT before writing any view.json):
/home/bpeck/git/primebench/ignition/gateway/projects/PrimeBench/com.inductiveautomation.perspective/views/ForceRow/view.json.
Hard rules (contest + platform)
- Jython 2.7 everywhere (project scripts, transforms, event scripts): NO f-strings
(
%formatting only), notyping, nostatistics. Scripts in view.json are inline JSON strings, tab-indented (\t), first char of every script/transform line block is a tab. - Java exceptions bypass
except Exceptionin Jython. Gateway-facing defensive code uses bareexcept:+sys.exc_info()(probe-verified). - NO tag bindings in PrimeBAT. NO SQL /
system.db.*. NOjournalName=literals. Banned strings (lint-enforced):[default],BuildathonSim,Buildathon_DB,alarmsim,SELECT, f-string prefixes. - All views/styles/scripts under
PrimeControls/namespaces. Single page/→PrimeControls/Dashboard.sharedDocksstays{}(docked views prohibited). - Every view tolerates
bundle: null/ empty arrays — no binding may throw on empty. Empty-window UX =PrimeControls/Components/EmptyStateembed, not a broken chart. - resource.json boilerplate for every view:
{"scope":"G","version":1,"restricted":false,"overridable":true,"files":["view.json"],"attributes":{}}(style classes:files:["style.json"]; script modules: scope"A",attributes:{"hintScope":2}). - Numbers/durations/timestamps rendered through
PrimeControls.fmthelpers (consistency).
Probe-verified journal API facts (do not re-derive)
system.alarm.queryJournal(startDate=<ms|Date>, endDate=<ms|Date>)— epoch millis accepted directly; omitjournalName(uses the single configured journal); nonexistent name raises javaIllegalArgumentException(catch with bare except).- Result
AlarmQueryResultImpl: iterable,len()works. One entry per transition; entries of one alarm instance sharestr(evt.getId())(uuid). str(evt.getState())is compound:"Active, Unacknowledged","Cleared, Acknowledged", etc. Transition kind per row: activation row state starts"Active"and row'sgetActiveData()non-null; clear row starts"Cleared"(getClearedData()non-null); ack row hasgetAckData()non-null / state contains", Acknowledged". Per-instance:active_ms= min ts of Active rows;clear_ms= min ts of Cleared rows;ack_ms= min ts of rows with ack marker.evt.get("eventTime")→ java Date;.getTime()→ ms.evt.get("eventType")is None (unused).evt.getDisplayPath()=""when unconfigured → fall back to parsinggetSource()(prov:default:/tag:FOLDER/SUB/TAG:/alm:ALARMNAME).evt.getPriority()→ AlarmPriority enum:.ordinal()→ 0..4,str()→ name.- System rows:
source == "evt:System Startup"etc.,evt.get("isSystemEvent")True.includeSystem-style kwargs are ACCEPTED BUT IGNORED — always filter client-side. - Ack user field:
evt.get("ackUserName")(unicode, may beu'').ackUseris None. system.alarm.queryStatus(state=["ActiveUnacked","ActiveAcked"])works; entries havegetActiveData()with eventTime.system.alarm.getShelvedPaths()→ list.
Session props (only these exist; NO agent may add any)
session.custom.PrimeControls:
{"query": {"startMs": 0, "endMs": 0, "rangePreset": "8h", "priorities": [], "areas": [],
"states": [], "search": "", "refreshToken": 0},
"ui": {"selectedTab": 0, "badActorFocus": ""}}
rangePreset∈4h|8h|24h|7d|custom. Header preset dropdown writes startMs/endMs/rangePreset.- Filters (
priorities= int levels 0–4,areas= strings,states∈["active","cleared","acked","unacked"],searchsubstring) are written by FilterBar; Apply button bumpsrefreshToken(filters do NOT auto-refetch). ui.selectedTab: 0 Overview · 1 Analysis · 2 Bad Actors · 3 Journal.ui.badActorFocus: source string; set by Overview top-5 click; BadActors highlights + clears.
Data layer (script package PrimeControls, resource paths ignition/script-python/PrimeControls/<mod>/code.py)
Modules: calc (pure), fmt (pure), alarms (gateway adapter). Pure modules run under
CPython 3 for pytest AND Jython 2.7 (from __future__ import division, print_function).
Perspective entry points (exact signatures)
PrimeControls.alarms.getDashboardBundle(startMs, endMs, options=None) # -> bundle dict (below)
PrimeControls.alarms.getJournalPage(startMs, endMs, filters=None, page=0, pageSize=50)
# filters: {"priorities":[int], "areas":[str], "states":[str], "search":str}
# -> {"rows":[{"id","time_ms","time_label","source","label","area","state","state_label",
# "priority","priority_name","ack_user"}], "total":int, "page":int,
# "page_size":int, "truncated":bool, "error":None|str}
PrimeControls.alarms.journalCsv(startMs, endMs, filters=None, maxRows=10000) # -> CSV string
PrimeControls.alarms.getSourceDetail(source, startMs, endMs)
# -> {"label","area","events":[journal-page rows],"daily":[{"t0","count"}],
# "stats":{"count","avg_tta_ms","avg_active_ms","fleeting_count",
# "top_ack_users":[{"user","count"}]},"error":None|str}
PrimeControls.alarms.getEventData(eventId, aroundMs, source) # -> {"props":[{"name","value"}],"error":None|str}
PrimeControls.alarms.getShiftReportText(shiftHours=8, anchorHour=0, options=None)
# -> {"text":str, "start_ms":int, "end_ms":int, "label":str}
PrimeControls.fmt.dur(ms) / num(v, dec=0) / pct(v, dec=0, signed=False) / clock(ms) / day_clock(ms)
PrimeControls.fmt.delta_chip(d) # d = a kpi delta dict -> {"arrow","text","good"}
Bundle schema (getDashboardBundle return — every key ALWAYS present)
meta: {start_ms, end_ms, prior_start_ms, now_ms, window_hours, event_count,
activation_count, dropped_rows, truncated, correlation_mode,
standing_mode, areas:[str], priorities_seen:[{"value":int,"label":str}],
source_count, error:None|str, opts:{...effective thresholds...}}
kpis: {activations, rate_per_hr, active_now, unacked_now, shelved_now, mtta_ms,
mttr_ms, flood_pct, flood_count, chatter_count, standing_count, fleeting_count}
# each value = {"value", "prior", "delta_pct", "dir": "up|down|flat|new|none", "good": bool|None}
health: {grade:"A".."F"|None, score:float|None,
subs:[{key,label,score:float|None,weight,detail}]} # keys: rate,flood,chatter,standing,priority
insights: [{severity:int(0 crit..3 info), icon:str, text:str, tab:str}]
rate: {bins:[{t0,t1,count,flood:bool}], bin_ms, target_per_bin, flood_threshold, max_count}
floods: {episodes:[{start_ms,end_ms,duration_ms,event_count,peak_bin_count,
top_source_label,top_source_count,top_area}], pct_time_in_flood}
priority: {raw:[{name,level,count}], buckets:{low,medium,high,other},
pct:{low,medium,high}, target:{low:80.0,medium:15.0,high:5.0}, sum_abs_dev}
heatmap: {rows:[[int]*24]*7, row_labels:["Mon".."Sun"], max_count, total}
mtta_mttr: {mtta:{mean_ms,median_ms,count}, mttr:{mean_ms,median_ms,count},
trend:[{t0,mtta_ms|None,mttr_ms|None,ack_n,clear_n}]}
pareto: {total_activations, rows:[{rank,source,label,area,priority_name,count,pct,cum_pct}]}
top_sources: [first 5 pareto rows]
chattering: [{source,label,area,priority_name,count,per_hour,median_gap_s}]
fleeting: {total, sources:[{source,label,area,priority_name,count,median_s}]}
standing: {mode, count, rows:[{source,label,area,priority_name,active_ms,age_ms,age_h,unacked}]}
active_now: [{source,label,area,priority_name,active_ms,age_ms,unacked}] # capped 200
Thresholds (surfaced in meta.opts): bins 10 min; flood >10/10min (end hysteresis 5);
ISA target 6/hr; chatter >10/hr AND median gap ≤120 s (min 5); fleeting <10 s;
standing >24 h; deltas flat band ±5%; caps: 50 000 events, 5000 bins, lists ≤50 rows.
Binding patterns (copy these shapes exactly)
P1 — bundle binding (Dashboard shell ONLY): custom.bundle ← expr binding
{"type": "expr",
"config": {"expression": "{session.custom.PrimeControls.query.refreshToken} + '|' + {session.custom.PrimeControls.query.startMs} + '|' + {session.custom.PrimeControls.query.endMs}"},
"transforms": [{"type": "script", "code": "\tq = self.session.custom.PrimeControls.query\n\treturn PrimeControls.alarms.getDashboardBundle(q.startMs, q.endMs, {'priorities': list(q.priorities), 'areas': list(q.areas), 'states': list(q.states), 'search': q.search})"}]}
P2 — embed a sub-view with bundle + tab visibility:
{"meta": {"name": "tabOverview"}, "position": {"grow": 1},
"propConfig": {
"props.params.bundle": {"binding": {"type": "property", "config": {"path": "view.custom.bundle"}}},
"position.display": {"binding": {"type": "expr", "config": {"expression": "{session.custom.PrimeControls.ui.selectedTab} = 0"}}}},
"props": {"path": "PrimeControls/Tabs/Overview", "params": {}},
"type": "ia.display.view"}
P3 — bidirectional session binding (FilterBar widgets):
{"binding": {"bidirectional": true, "type": "property",
"config": {"path": "session.custom.PrimeControls.query.search"}}}
P4 — event scripts: buttons/dropdowns fire events.component.onActionPerformed;
labels/icons/containers fire events.dom.onClick (Designer-verified shape). Action object
is identical for both: {"config": {"script": "\t..."}, "scope": "G", "type": "script"}.
"events": {"dom": {"onClick": {"config": {"script": "\tsystem.perspective.openPopup('PC_HealthScore', 'PrimeControls/Popups/HealthScore', params = {'health': self.view.params.health}, showCloseIcon = True, draggable = True)"}, "scope": "G", "type": "script"}}}
P5 — flex repeater from bundle slice:
{"type": "ia.display.flex-repeater",
"propConfig": {"props.instances": {"binding": {"type": "property", "config": {"path": "view.params.bundle.top_sources"}}}},
"props": {"path": "PrimeControls/Components/SourceRow", "direction": "column"}}
Repeater instance keys become the child view's params → bundle array element keys == component view param names (snake_case).
View param declaration: "params": {"bundle": null} + propConfig
"params.bundle": {"paramDirection": "input", "persistent": true}.
Popups (IDs are constants)
| ID | View | Params |
|---|---|---|
PC_HealthScore |
PrimeControls/Popups/HealthScore |
{health: bundle.health} |
PC_AlarmDetail |
PrimeControls/Popups/AlarmDetail |
{source, label} |
PC_EventDetail |
PrimeControls/Popups/EventDetail |
{event: <journal row dict>} |
PC_ShiftReport |
PrimeControls/Popups/ShiftReport |
{} |
Close: system.perspective.closePopup('<ID>').
Style classes (foundation-owned; reference as "style": {"classes": "PrimeControls/Card"})
PrimeControls/Priority/{Diagnostic,Low,Medium,High,Critical} (solid chip bg+fg) ·
PrimeControls/PrioritySoft/{...same 5} (tinted bg) ·
PrimeControls/Grade/{A,B,C,D,F,NA} · PrimeControls/Card · PrimeControls/CardTitle ·
PrimeControls/Toolbar · PrimeControls/PageBg · PrimeControls/Chip/{Good,Bad,Flat} ·
PrimeControls/Tab/{Active,Inactive} · PrimeControls/State/{Active,Acked,Cleared} ·
PrimeControls/Text/{Big,Kpi,Muted} · PrimeControls/Empty.
Priority scale: Diagnostic #8A94A6 · Low #5B9BD5 · Medium #E5C453 · High #E8883A ·
Critical #D64550. Accent: #3B7DD8. Agents may ADD classes only under
style-classes/PrimeControls/<TheirArea>/..., never modify shared ones.
Shared components (foundation-owned, under views/PrimeControls/Components/)
| View | Params (all input) |
|---|---|
Components/TabButton |
title:str, index:int (writes ui.selectedTab onClick; active style expr) |
Components/KpiCard |
`label:str, value:str, unit:str, delta:{...kpi delta} |
Components/PriorityCard |
priority_name:str, count:int, pct:float |
Components/InsightRow |
severity:int, icon:str, text:str, tab:str |
Components/SourceRow |
rank:int, source:str, label:str, count:int, pct:float (click → focus+tab 2) |
Components/HeatmapCell |
count:int, max:int (bg alpha-scaled accent) |
Components/ScoreBar |
`key:str, label:str, score:float |
Components/EmptyState |
message:str, icon:str |
Components/HealthGauge |
`health:{grade,score,subs} |
View ownership (Phase 2/3)
A: Dashboard/Header + Dashboard/FilterBar · B: Tabs/Overview · C: Tabs/Analysis ·
D: Tabs/BadActors · E: Tabs/Journal · F: Popups/HealthScore+Popups/ShiftReport ·
G: Popups/AlarmDetail+Popups/EventDetail · H: Components/HealthGauge upgrade.
Integrator-only: Dashboard/view.json, session-props, page-config, shared Components,
root style classes, script-python/*, SimHarness.
Verify loop (every agent, before declaring done)
python3 tools/lint_project.py
python3 tools/provision.py scan-projects
sleep 5; docker logs buildathon-ignition --since 1m 2>&1 | grep -iE "error|exception|traceback"
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8088/data/perspective/client/PrimeBAT
Schema uncertainty → read the harvested schema file; still unsure → minimal probe view in
SimHarness/views/Scratch/Current (coordinate via integrator), never guess deep chart JSON.