forked from b.peck/BAT
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:
@@ -3,13 +3,15 @@
|
||||
"PrimeControls": {
|
||||
"query": {
|
||||
"areas": [],
|
||||
"endMs": 1784233045923,
|
||||
"endMs": 0,
|
||||
"priorities": [],
|
||||
"rangePreset": "8h",
|
||||
"refreshToken": 0,
|
||||
"search": "",
|
||||
"startMs": 1784204245923,
|
||||
"states": []
|
||||
"startMs": 0,
|
||||
"states": [],
|
||||
"filterDow": -1,
|
||||
"filterHour": -1
|
||||
},
|
||||
"ui": {
|
||||
"badActorFocus": "",
|
||||
@@ -92,4 +94,4 @@
|
||||
"theme": "light-cool",
|
||||
"timeZoneId": "America/Chicago"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
"custom": {},
|
||||
"params": {
|
||||
"count": null,
|
||||
"max": null
|
||||
"max": null,
|
||||
"dow": null,
|
||||
"hour": null
|
||||
},
|
||||
"propConfig": {
|
||||
"params.count": {
|
||||
@@ -12,6 +14,14 @@
|
||||
"params.max": {
|
||||
"paramDirection": "input",
|
||||
"persistent": false
|
||||
},
|
||||
"params.dow": {
|
||||
"paramDirection": "input",
|
||||
"persistent": false
|
||||
},
|
||||
"params.hour": {
|
||||
"paramDirection": "input",
|
||||
"persistent": false
|
||||
}
|
||||
},
|
||||
"props": {},
|
||||
@@ -66,6 +76,20 @@
|
||||
],
|
||||
"type": "property"
|
||||
}
|
||||
},
|
||||
"props.style.cursor": {
|
||||
"binding": {
|
||||
"config": {
|
||||
"path": "view.params.dow"
|
||||
},
|
||||
"transforms": [
|
||||
{
|
||||
"code": "\ttry:\n\t\treturn 'pointer' if value is not None else 'default'\n\texcept:\n\t\treturn 'default'",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
"type": "property"
|
||||
}
|
||||
}
|
||||
},
|
||||
"props": {
|
||||
@@ -77,7 +101,18 @@
|
||||
"textAlign": "center"
|
||||
}
|
||||
},
|
||||
"type": "ia.display.label"
|
||||
"type": "ia.display.label",
|
||||
"events": {
|
||||
"dom": {
|
||||
"onClick": {
|
||||
"config": {
|
||||
"script": "\ttry:\n\t\td = self.view.params.dow\n\t\th = self.view.params.hour\n\t\tif d is None or h is None:\n\t\t\treturn\n\t\tq = self.session.custom.PrimeControls.query\n\t\tq.filterDow = int(d)\n\t\tq.filterHour = int(h)\n\t\tself.session.custom.PrimeControls.ui.selectedTab = 3\n\texcept:\n\t\tpass"
|
||||
},
|
||||
"scope": "G",
|
||||
"type": "script"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
@@ -91,4 +126,4 @@
|
||||
},
|
||||
"type": "ia.container.flex"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"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'"
|
||||
"script": "\tq = self.session.custom.PrimeControls.query\n\tspans = {'4h': 4, '8h': 8, '24h': 24, '7d': 168}\n\tpreset = q.rangePreset\n\tnow = system.date.toMillis(system.date.now())\n\tif preset in spans:\n\t\t# relative presets re-anchor to now on every session start\n\t\tq.endMs = now\n\t\tq.startMs = now - spans[preset] * 3600 * 1000\n\telif not q.startMs or q.startMs == 0:\n\t\tq.endMs = now\n\t\tq.startMs = now - 8 * 3600 * 1000\n\t\tq.rangePreset = '8h'"
|
||||
},
|
||||
"scope": "G",
|
||||
"type": "script"
|
||||
@@ -59,40 +59,6 @@
|
||||
},
|
||||
"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": [
|
||||
{
|
||||
@@ -180,6 +146,40 @@
|
||||
},
|
||||
"type": "ia.container.flex"
|
||||
},
|
||||
{
|
||||
"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": [
|
||||
{
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"custom.detail": {
|
||||
"binding": {
|
||||
"config": {
|
||||
"expression": "{view.params.source} + \u0027|\u0027 + {session.custom.PrimeControls.query.startMs} + \u0027|\u0027 + {session.custom.PrimeControls.query.endMs}"
|
||||
"expression": "{view.params.source} + '|' + {session.custom.PrimeControls.query.startMs} + '|' + {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",
|
||||
"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",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
@@ -47,7 +47,21 @@
|
||||
},
|
||||
"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",
|
||||
"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'",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
"type": "property"
|
||||
}
|
||||
},
|
||||
"props.style.fontSize": {
|
||||
"binding": {
|
||||
"config": {
|
||||
"path": "this.props.text"
|
||||
},
|
||||
"transforms": [
|
||||
{
|
||||
"code": "\ttry:\n\t\tn = len(value or '')\n\texcept:\n\t\tn = 0\n\tif n <= 34:\n\t\treturn '28px'\n\tsize = int(28.0 * 34 / n)\n\tif size < 12:\n\t\tsize = 12\n\treturn '%dpx' % size",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
@@ -57,10 +71,17 @@
|
||||
},
|
||||
"props": {
|
||||
"style": {
|
||||
"classes": "PrimeControls/Text/Big"
|
||||
"classes": "PrimeControls/Text/Big",
|
||||
"minWidth": "0px",
|
||||
"whiteSpace": "nowrap"
|
||||
}
|
||||
},
|
||||
"type": "ia.display.label"
|
||||
"type": "ia.display.label",
|
||||
"position": {
|
||||
"basis": "auto",
|
||||
"grow": 1,
|
||||
"shrink": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"meta": {
|
||||
@@ -74,7 +95,7 @@
|
||||
},
|
||||
"transforms": [
|
||||
{
|
||||
"code": "\ttry:\n\t\treturn value[\u0027area\u0027] or \u0027\u0027\n\texcept:\n\t\treturn \u0027\u0027",
|
||||
"code": "\ttry:\n\t\treturn value['area'] or ''\n\texcept:\n\t\treturn ''",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
@@ -121,7 +142,7 @@
|
||||
},
|
||||
"transforms": [
|
||||
{
|
||||
"code": "\ttry:\n\t\treturn PrimeControls.fmt.num(value[\u0027stats\u0027][\u0027count\u0027])\n\texcept:\n\t\treturn \u0027--\u0027",
|
||||
"code": "\ttry:\n\t\treturn PrimeControls.fmt.num(value['stats']['count'])\n\texcept:\n\t\treturn '--'",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
@@ -153,7 +174,7 @@
|
||||
},
|
||||
"transforms": [
|
||||
{
|
||||
"code": "\ttry:\n\t\treturn PrimeControls.fmt.dur(value[\u0027stats\u0027][\u0027avg_tta_ms\u0027])\n\texcept:\n\t\treturn \u0027--\u0027",
|
||||
"code": "\ttry:\n\t\treturn PrimeControls.fmt.dur(value['stats']['avg_tta_ms'])\n\texcept:\n\t\treturn '--'",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
@@ -185,7 +206,7 @@
|
||||
},
|
||||
"transforms": [
|
||||
{
|
||||
"code": "\ttry:\n\t\treturn PrimeControls.fmt.dur(value[\u0027stats\u0027][\u0027avg_active_ms\u0027])\n\texcept:\n\t\treturn \u0027--\u0027",
|
||||
"code": "\ttry:\n\t\treturn PrimeControls.fmt.dur(value['stats']['avg_active_ms'])\n\texcept:\n\t\treturn '--'",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
@@ -217,7 +238,7 @@
|
||||
},
|
||||
"transforms": [
|
||||
{
|
||||
"code": "\ttry:\n\t\treturn PrimeControls.fmt.num(value[\u0027stats\u0027][\u0027fleeting_count\u0027])\n\texcept:\n\t\treturn \u0027--\u0027",
|
||||
"code": "\ttry:\n\t\treturn PrimeControls.fmt.num(value['stats']['fleeting_count'])\n\texcept:\n\t\treturn '--'",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
@@ -281,7 +302,7 @@
|
||||
},
|
||||
"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]",
|
||||
"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]",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
@@ -317,7 +338,7 @@
|
||||
},
|
||||
"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",
|
||||
"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'",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
@@ -382,7 +403,7 @@
|
||||
},
|
||||
"transforms": [
|
||||
{
|
||||
"code": "\ttry:\n\t\treturn len(value[\u0027stats\u0027][\u0027top_ack_users\u0027]) \u003e 0\n\texcept:\n\t\treturn False",
|
||||
"code": "\ttry:\n\t\treturn len(value['stats']['top_ack_users']) > 0\n\texcept:\n\t\treturn False",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
@@ -396,7 +417,7 @@
|
||||
},
|
||||
"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",
|
||||
"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 ''",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
@@ -428,7 +449,7 @@
|
||||
},
|
||||
"transforms": [
|
||||
{
|
||||
"code": "\ttry:\n\t\treturn len(value[\u0027stats\u0027][\u0027top_ack_users\u0027]) \u003e 1\n\texcept:\n\t\treturn False",
|
||||
"code": "\ttry:\n\t\treturn len(value['stats']['top_ack_users']) > 1\n\texcept:\n\t\treturn False",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
@@ -442,7 +463,7 @@
|
||||
},
|
||||
"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",
|
||||
"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 ''",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
@@ -474,7 +495,7 @@
|
||||
},
|
||||
"transforms": [
|
||||
{
|
||||
"code": "\ttry:\n\t\treturn len(value[\u0027stats\u0027][\u0027top_ack_users\u0027]) \u003e 2\n\texcept:\n\t\treturn False",
|
||||
"code": "\ttry:\n\t\treturn len(value['stats']['top_ack_users']) > 2\n\texcept:\n\t\treturn False",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
@@ -488,7 +509,7 @@
|
||||
},
|
||||
"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",
|
||||
"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 ''",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
@@ -520,7 +541,7 @@
|
||||
},
|
||||
"transforms": [
|
||||
{
|
||||
"code": "\ttry:\n\t\treturn len(value[\u0027stats\u0027][\u0027top_ack_users\u0027]) \u003d\u003d 0\n\texcept:\n\t\treturn True",
|
||||
"code": "\ttry:\n\t\treturn len(value['stats']['top_ack_users']) == 0\n\texcept:\n\t\treturn True",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
@@ -600,7 +621,7 @@
|
||||
},
|
||||
"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",
|
||||
"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",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
@@ -686,7 +707,7 @@
|
||||
},
|
||||
"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",
|
||||
"code": "\tif value is None:\n\t\treturn False\n\ttry:\n\t\treturn not value['error']\n\texcept:\n\t\treturn True",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
@@ -717,7 +738,7 @@
|
||||
},
|
||||
"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",
|
||||
"code": "\tif value is None:\n\t\treturn True\n\ttry:\n\t\treturn bool(value['error'])\n\texcept:\n\t\treturn False",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
@@ -741,7 +762,7 @@
|
||||
"component": {
|
||||
"onActionPerformed": {
|
||||
"config": {
|
||||
"script": "\tsystem.perspective.closePopup(\u0027PC_AlarmDetail\u0027)"
|
||||
"script": "\tsystem.perspective.closePopup('PC_AlarmDetail')"
|
||||
},
|
||||
"scope": "G",
|
||||
"type": "script"
|
||||
@@ -787,4 +808,4 @@
|
||||
},
|
||||
"type": "ia.container.flex"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,9 @@
|
||||
"name": "lblTitle"
|
||||
},
|
||||
"position": {
|
||||
"grow": 1
|
||||
"basis": "auto",
|
||||
"grow": 1,
|
||||
"shrink": 1
|
||||
},
|
||||
"propConfig": {
|
||||
"props.text": {
|
||||
@@ -56,11 +58,27 @@
|
||||
],
|
||||
"type": "property"
|
||||
}
|
||||
},
|
||||
"props.style.fontSize": {
|
||||
"binding": {
|
||||
"config": {
|
||||
"path": "this.props.text"
|
||||
},
|
||||
"transforms": [
|
||||
{
|
||||
"code": "\ttry:\n\t\tn = len(value or '')\n\texcept:\n\t\tn = 0\n\tif n <= 24:\n\t\treturn '28px'\n\tsize = int(28.0 * 24 / n)\n\tif size < 12:\n\t\tsize = 12\n\treturn '%dpx' % size",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
"type": "property"
|
||||
}
|
||||
}
|
||||
},
|
||||
"props": {
|
||||
"style": {
|
||||
"classes": "PrimeControls/Text/Big"
|
||||
"classes": "PrimeControls/Text/Big",
|
||||
"minWidth": "0px",
|
||||
"whiteSpace": "nowrap"
|
||||
}
|
||||
},
|
||||
"type": "ia.display.label"
|
||||
|
||||
@@ -108,7 +108,7 @@
|
||||
},
|
||||
"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 []",
|
||||
"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, 'dow': 0, 'hour': i} for i, c in enumerate(hm['rows'][0])][:24]\n\texcept:\n\t\treturn []",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
@@ -174,7 +174,7 @@
|
||||
},
|
||||
"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 []",
|
||||
"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, 'dow': 1, 'hour': i} for i, c in enumerate(hm['rows'][1])][:24]\n\texcept:\n\t\treturn []",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
@@ -240,7 +240,7 @@
|
||||
},
|
||||
"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 []",
|
||||
"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, 'dow': 2, 'hour': i} for i, c in enumerate(hm['rows'][2])][:24]\n\texcept:\n\t\treturn []",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
@@ -306,7 +306,7 @@
|
||||
},
|
||||
"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 []",
|
||||
"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, 'dow': 3, 'hour': i} for i, c in enumerate(hm['rows'][3])][:24]\n\texcept:\n\t\treturn []",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
@@ -372,7 +372,7 @@
|
||||
},
|
||||
"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 []",
|
||||
"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, 'dow': 4, 'hour': i} for i, c in enumerate(hm['rows'][4])][:24]\n\texcept:\n\t\treturn []",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
@@ -438,7 +438,7 @@
|
||||
},
|
||||
"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 []",
|
||||
"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, 'dow': 5, 'hour': i} for i, c in enumerate(hm['rows'][5])][:24]\n\texcept:\n\t\treturn []",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
@@ -504,7 +504,7 @@
|
||||
},
|
||||
"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 []",
|
||||
"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, 'dow': 6, 'hour': i} for i, c in enumerate(hm['rows'][6])][:24]\n\texcept:\n\t\treturn []",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -10,11 +10,11 @@
|
||||
"custom.pageData": {
|
||||
"binding": {
|
||||
"config": {
|
||||
"expression": "{session.custom.PrimeControls.query.refreshToken} + '|' + {session.custom.PrimeControls.query.startMs} + '|' + {session.custom.PrimeControls.query.endMs} + '|' + {view.custom.page}"
|
||||
"expression": "{session.custom.PrimeControls.query.refreshToken} + '|' + {session.custom.PrimeControls.query.startMs} + '|' + {session.custom.PrimeControls.query.endMs} + '|' + {session.custom.PrimeControls.query.filterDow} + '|' + {session.custom.PrimeControls.query.filterHour} + '|' + {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",
|
||||
"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, 'dow': q.filterDow, 'hour': q.filterHour}, page, 50)\n\texcept:\n\t\timport sys\n\t\tsafe['error'] = '%s' % (sys.exc_info()[1],)\n\t\treturn safe",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
@@ -66,6 +66,99 @@
|
||||
},
|
||||
"type": "ia.display.label"
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"meta": {
|
||||
"name": "lblTimeFilter"
|
||||
},
|
||||
"position": {
|
||||
"basis": "auto",
|
||||
"grow": 0,
|
||||
"shrink": 0
|
||||
},
|
||||
"propConfig": {
|
||||
"props.text": {
|
||||
"binding": {
|
||||
"config": {
|
||||
"struct": {
|
||||
"dow": "{session.custom.PrimeControls.query.filterDow}",
|
||||
"hour": "{session.custom.PrimeControls.query.filterHour}"
|
||||
},
|
||||
"waitOnAll": false
|
||||
},
|
||||
"transforms": [
|
||||
{
|
||||
"code": "\tdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']\n\ttry:\n\t\td = int(value['dow'])\n\t\th = int(value['hour'])\n\texcept:\n\t\treturn ''\n\tif d < 0 and h < 0:\n\t\treturn ''\n\tday = days[d] if 0 <= d < 7 else 'Any day'\n\tif h >= 0:\n\t\treturn 'Time filter: %s %02d:00-%02d:59' % (day, h, h)\n\treturn 'Time filter: %s' % day",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
"type": "expr-struct"
|
||||
}
|
||||
}
|
||||
},
|
||||
"props": {
|
||||
"style": {
|
||||
"whiteSpace": "nowrap"
|
||||
}
|
||||
},
|
||||
"type": "ia.display.label"
|
||||
},
|
||||
{
|
||||
"events": {
|
||||
"dom": {
|
||||
"onClick": {
|
||||
"config": {
|
||||
"script": "\tq = self.session.custom.PrimeControls.query\n\tq.filterDow = -1\n\tq.filterHour = -1"
|
||||
},
|
||||
"scope": "G",
|
||||
"type": "script"
|
||||
}
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"name": "icoClearTimeFilter"
|
||||
},
|
||||
"position": {
|
||||
"basis": "20px",
|
||||
"grow": 0,
|
||||
"shrink": 0
|
||||
},
|
||||
"props": {
|
||||
"path": "material/close",
|
||||
"style": {
|
||||
"cursor": "pointer",
|
||||
"fontSize": "16px"
|
||||
}
|
||||
},
|
||||
"type": "ia.display.icon"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"name": "chipTimeFilter"
|
||||
},
|
||||
"position": {
|
||||
"shrink": 0
|
||||
},
|
||||
"propConfig": {
|
||||
"position.display": {
|
||||
"binding": {
|
||||
"config": {
|
||||
"expression": "{session.custom.PrimeControls.query.filterDow} > -1 || {session.custom.PrimeControls.query.filterHour} > -1"
|
||||
},
|
||||
"type": "expr"
|
||||
}
|
||||
}
|
||||
},
|
||||
"props": {
|
||||
"alignItems": "center",
|
||||
"style": {
|
||||
"classes": "PrimeControls/Chip/Flat",
|
||||
"gap": "6px"
|
||||
}
|
||||
},
|
||||
"type": "ia.container.flex"
|
||||
},
|
||||
{
|
||||
"meta": {
|
||||
"name": "lblError"
|
||||
@@ -242,7 +335,7 @@
|
||||
"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"
|
||||
"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, 'dow': q.filterDow, 'hour': q.filterHour})\n\t\tsystem.perspective.download('alarm_journal.csv', csv)\n\texcept:\n\t\tpass"
|
||||
},
|
||||
"scope": "G",
|
||||
"type": "script"
|
||||
|
||||
Binary file not shown.
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"scope": "C",
|
||||
"version": 1,
|
||||
"restricted": false,
|
||||
"overridable": true,
|
||||
"files": [
|
||||
"data.bin"
|
||||
],
|
||||
"attributes": {
|
||||
"lastModificationSignature": "71cfaca87ea3ea9c779495b13fa308d24deb72d86d5d3423e8cd0d949609a7d3",
|
||||
"lastModification": {
|
||||
"actor": "admin",
|
||||
"timestamp": "2026-07-16T20:02:07Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -1,36 +1,21 @@
|
||||
# alarm_simulator.py — Build-a-Thon alarm activity generator (Jython, Ignition 8.3)
|
||||
# alarmsim - Build-a-Thon alarm activity generator (Jython, Ignition 8.3).
|
||||
# Driven by the AlarmSimTick gateway timer (5000 ms).
|
||||
#
|
||||
# Drives the Boolean memory tags in [default]BuildathonSim/ (import
|
||||
# simulation_tags.json first). Designed to be called every 5 seconds:
|
||||
# MODE:
|
||||
# "healthy" - ~12 activations/hr plant-wide, ISA-like 80/15/5 priority mix,
|
||||
# no chatter / standing / fleeting / floods. Keeps the Alarm
|
||||
# Health score in the B range.
|
||||
# "chaos" - original stress profile (floods, chatterers, standing,
|
||||
# fleeting) for demoing bad-actor analytics.
|
||||
#
|
||||
# RECOMMENDED — Gateway timer script (survives Designer close):
|
||||
# 1. Designer > Project Library: create a script named "alarmsim", paste this file.
|
||||
# 2. Project > Gateway Events > Timer: new script, Delay 5000 ms,
|
||||
# Fixed Delay, Dedicated thread. Body: alarmsim.tick()
|
||||
# 3. Save the project. Alarm activity starts immediately and runs forever.
|
||||
#
|
||||
# ALTERNATIVE — Designer Script Console (blocks the console while running):
|
||||
# Paste this whole file, then add at the bottom:
|
||||
# run_console(minutes=30)
|
||||
#
|
||||
# Patterns produced (all timestamps land in the alarm journal):
|
||||
# - Baseline: random alarms across 5 areas / 3 priorities, active 30 s - 5 min,
|
||||
# averaging a few activations per minute
|
||||
# - Chattering: *_Chatter tags re-trigger every 30-60 s (short 5-15 s actives)
|
||||
# - Standing: *_Standing tags go active on the first tick and never clear
|
||||
# - Fleeting: *_Fleeting tags are active < 10 s
|
||||
# - Flood: roughly every 30-60 min (or on demand via force_flood()), one area
|
||||
# bursts 20-40 activations over 10 minutes
|
||||
#
|
||||
# Manual helpers (Script Console, gateway scope via system.util.sendRequest not
|
||||
# needed — just call from a console if running console mode, or temporarily from
|
||||
# the timer script):
|
||||
# alarmsim.force_flood() -> start a flood burst now
|
||||
# alarmsim.reset() -> clear all sim tags and internal state
|
||||
# Helpers: alarmsim.force_flood() (chaos only), alarmsim.reset() clears all
|
||||
# sim tags and internal state.
|
||||
|
||||
import random
|
||||
import time
|
||||
|
||||
MODE = "healthy"
|
||||
|
||||
BASE = "[default]BuildathonSim"
|
||||
|
||||
BASELINE = {
|
||||
@@ -62,12 +47,29 @@ FLEETING = [
|
||||
"TankFarm/PumpSealFlushLow_Fleeting",
|
||||
]
|
||||
|
||||
# Tuning (per 5 s tick)
|
||||
BASELINE_PROB = 0.30 # ~3-4 baseline activations/min across the plant
|
||||
FLEETING_PROB = 0.08 # ~1 fleeting alarm/min
|
||||
FLOOD_START_PROB = 0.0025 # expected flood every ~30-60 min
|
||||
FLOOD_DURATION = 600 # 10 minutes
|
||||
FLOOD_EVENT_PROB = 0.35 # per tick during flood -> ~25 activations / 10 min
|
||||
# Healthy mode: weighted 80/15/5 Low/Medium/High tag pools (priorities as
|
||||
# configured in test-data/simulation_tags.json).
|
||||
HEALTHY_LOW = ["Intake/InletFlowLow", "Intake/SampleTempHigh", "Intake/PowerMonitorAlarm",
|
||||
"BoilerHouse/StackTempHigh", "BoilerHouse/BlowdownConductivityHigh",
|
||||
"Packaging/CapperTorqueLow", "Packaging/FillerLevelDeviation", "Packaging/GuardDoorOpen",
|
||||
"Utilities/CoolingTowerVibration", "Utilities/WaterSoftenerFault",
|
||||
"TankFarm/VaporRecoveryFault", "TankFarm/T104_PressHigh", "TankFarm/ManifoldLeakDetect"]
|
||||
HEALTHY_MED = ["Intake/Pump2_Fault", "Intake/ScreenDiffPressHigh", "BoilerHouse/FuelGasPressLow",
|
||||
"BoilerHouse/EconomizerDPHigh", "Packaging/PrinterInkLow", "Packaging/ConveyorOverload",
|
||||
"Utilities/GlycolTempHigh", "Utilities/N2PressLow", "TankFarm/T103_TempHigh",
|
||||
"TankFarm/TransferPumpFault"]
|
||||
HEALTHY_HIGH = ["Intake/Pump1_Fault", "BoilerHouse/FeedPumpA_Fault", "Packaging/LabelerFault",
|
||||
"Utilities/ChillerTripped", "TankFarm/T101_LevelHigh"]
|
||||
|
||||
# chaos tuning (per 5 s tick)
|
||||
BASELINE_PROB = 0.30
|
||||
FLEETING_PROB = 0.08
|
||||
FLOOD_START_PROB = 0.0025
|
||||
FLOOD_DURATION = 600
|
||||
FLOOD_EVENT_PROB = 0.35
|
||||
|
||||
# healthy tuning: ~12 activations/hr -> 12/720 per 5 s tick
|
||||
HEALTHY_PROB = 12.0 / 720.0
|
||||
|
||||
|
||||
def _path(rel):
|
||||
@@ -78,9 +80,9 @@ def _state():
|
||||
g = system.util.getGlobals()
|
||||
if "buildathon_alarmsim" not in g:
|
||||
g["buildathon_alarmsim"] = {
|
||||
"clears": {}, # tag path -> epoch seconds to write False
|
||||
"chatter_next": {}, # chatter path -> epoch seconds of next re-trigger
|
||||
"flood": None, # {"area": name, "until": epoch} while flooding
|
||||
"clears": {},
|
||||
"chatter_next": {},
|
||||
"flood": None,
|
||||
"standing_set": False,
|
||||
}
|
||||
return g["buildathon_alarmsim"]
|
||||
@@ -92,30 +94,52 @@ def _write(pairs):
|
||||
|
||||
|
||||
def tick():
|
||||
if MODE == "healthy":
|
||||
_tick_healthy()
|
||||
else:
|
||||
_tick_chaos()
|
||||
|
||||
|
||||
def _tick_healthy():
|
||||
st = _state()
|
||||
now = time.time()
|
||||
writes = []
|
||||
|
||||
# 1. Standing alarms: activate once, never clear
|
||||
if not st["standing_set"]:
|
||||
writes += [(_path(p), True) for p in STANDING]
|
||||
st["standing_set"] = True
|
||||
|
||||
# 2. Process scheduled clears
|
||||
for path in list(st["clears"].keys()):
|
||||
if now >= st["clears"][path]:
|
||||
writes.append((path, False))
|
||||
del st["clears"][path]
|
||||
if random.random() < HEALTHY_PROB:
|
||||
r = random.random()
|
||||
if r < 0.80:
|
||||
rel = random.choice(HEALTHY_LOW)
|
||||
elif r < 0.95:
|
||||
rel = random.choice(HEALTHY_MED)
|
||||
else:
|
||||
rel = random.choice(HEALTHY_HIGH)
|
||||
path = _path(rel)
|
||||
if path not in st["clears"]:
|
||||
writes.append((path, True))
|
||||
st["clears"][path] = now + random.uniform(45, 360)
|
||||
_write(writes)
|
||||
|
||||
# 3. Baseline activity: random tag, active 30 s - 5 min
|
||||
|
||||
def _tick_chaos():
|
||||
st = _state()
|
||||
now = time.time()
|
||||
writes = []
|
||||
if not st["standing_set"]:
|
||||
writes += [(_path(p), True) for p in STANDING]
|
||||
st["standing_set"] = True
|
||||
for path in list(st["clears"].keys()):
|
||||
if now >= st["clears"][path]:
|
||||
writes.append((path, False))
|
||||
del st["clears"][path]
|
||||
if random.random() < BASELINE_PROB:
|
||||
area = random.choice(list(BASELINE.keys()))
|
||||
path = _path("%s/%s" % (area, random.choice(BASELINE[area])))
|
||||
if path not in st["clears"]:
|
||||
writes.append((path, True))
|
||||
st["clears"][path] = now + random.uniform(30, 300)
|
||||
|
||||
# 4. Chattering: re-trigger every 30-60 s, active 5-15 s each time
|
||||
for rel in CHATTER:
|
||||
path = _path(rel)
|
||||
nxt = st["chatter_next"].get(path, 0)
|
||||
@@ -123,15 +147,11 @@ def tick():
|
||||
writes.append((path, True))
|
||||
st["clears"][path] = now + random.uniform(5, 15)
|
||||
st["chatter_next"][path] = now + random.uniform(30, 60)
|
||||
|
||||
# 5. Fleeting: active 2-8 s
|
||||
if random.random() < FLEETING_PROB:
|
||||
path = _path(random.choice(FLEETING))
|
||||
if path not in st["clears"]:
|
||||
writes.append((path, True))
|
||||
st["clears"][path] = now + random.uniform(2, 8)
|
||||
|
||||
# 6. Flood burst: one area, 15+ activations inside 10 minutes
|
||||
if st["flood"] is None:
|
||||
if random.random() < FLOOD_START_PROB:
|
||||
_start_flood(st, now)
|
||||
@@ -141,11 +161,8 @@ def tick():
|
||||
if random.random() < FLOOD_EVENT_PROB:
|
||||
area = st["flood"]["area"]
|
||||
path = _path("%s/%s" % (area, random.choice(BASELINE[area])))
|
||||
# During a flood, alarms clear fast and may re-trigger, inflating the
|
||||
# event rate the way a real upset does. Overwrite any pending clear.
|
||||
writes.append((path, True))
|
||||
st["clears"][path] = now + random.uniform(10, 45)
|
||||
|
||||
_write(writes)
|
||||
|
||||
|
||||
@@ -156,7 +173,7 @@ def _start_flood(st, now):
|
||||
|
||||
|
||||
def force_flood():
|
||||
"""Start a flood burst immediately (handy for demo/testing)."""
|
||||
"""Start a flood burst immediately (chaos mode only)."""
|
||||
_start_flood(_state(), time.time())
|
||||
|
||||
|
||||
@@ -169,11 +186,3 @@ def reset():
|
||||
paths += [_path("%s/%s" % (area, n)) for n in names]
|
||||
paths += [_path(p) for p in CHATTER + STANDING + FLEETING]
|
||||
system.tag.writeBlocking(paths, [False] * len(paths))
|
||||
|
||||
|
||||
def run_console(minutes=30):
|
||||
"""Script Console runner: calls tick() every 5 s for `minutes`. Blocks the console."""
|
||||
end = time.time() + minutes * 60
|
||||
while time.time() < end:
|
||||
tick()
|
||||
time.sleep(5)
|
||||
|
||||
@@ -98,6 +98,22 @@ def _p2():
|
||||
return out
|
||||
|
||||
|
||||
def _p3():
|
||||
"""Health-grade ground truth for the default 8h window."""
|
||||
pkg = _load_pc()
|
||||
end = system.date.toMillis(system.date.now())
|
||||
start = end - 8 * 3600 * 1000
|
||||
b = pkg.alarms.getDashboardBundle(start, end, None)
|
||||
return {"grade": b["health"]["grade"], "score": b["health"]["score"],
|
||||
"subs": [(s["key"], s["score"], s["detail"]) for s in b["health"]["subs"]],
|
||||
"activations": b["meta"]["activation_count"],
|
||||
"kpis": {"rate": b["kpis"]["rate_per_hr"]["value"],
|
||||
"flood_pct": b["kpis"]["flood_pct"]["value"],
|
||||
"chatter": b["kpis"]["chatter_count"]["value"],
|
||||
"standing": b["kpis"]["standing_count"]["value"],
|
||||
"active_now": b["kpis"]["active_now"]["value"]}}
|
||||
|
||||
|
||||
def _p1():
|
||||
"""Exec PrimeBAT's real PrimeControls modules (calc/fmt/alarms) in this scope
|
||||
and run getDashboardBundle against the live journal - end-to-end data-layer
|
||||
@@ -311,6 +327,6 @@ def _p0():
|
||||
def run():
|
||||
result = {"probe": PROBE_NAME,
|
||||
"ranAt": str(system.date.now())}
|
||||
result["payload"] = _safe(_p2)
|
||||
result["payload"] = _safe(_p3)
|
||||
_write(result)
|
||||
system.util.getLogger("probe").info("probe %s written to %s" % (PROBE_NAME, OUT_DIR))
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
def handleTimerEvent():
|
||||
alarmsim.reset()
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"scope": "G",
|
||||
"version": 1,
|
||||
"restricted": false,
|
||||
"overridable": true,
|
||||
"files": [
|
||||
"handleTimerEvent.py"
|
||||
],
|
||||
"attributes": {
|
||||
"sharedThread": false,
|
||||
"delay": 10000,
|
||||
"fixedDelay": true,
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user