Phase 0: wipe PrimeBAT template, SimHarness (sim+probe), lint + schema harvest tools

Gate G0 green: scan clean, lint 0 failures, clients 200, probe P0 facts captured,
simulator journaling. Probe locked: uuid row grouping, compound AlarmState strings,
java-exception bare-except gotcha, millis dates OK, ackUserName field.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 12:07:23 -05:00
parent 310b1b3b9e
commit af662f6fd2
78 changed files with 911 additions and 1957 deletions

View File

@@ -1,53 +1,9 @@
{ {
"pages": { "pages": {
"/": { "/": {
"viewPath": "Page/Home" "title": "Alarm Analysis",
}, "viewPath": "PrimeControls/Dashboard"
"/alarms": {
"viewPath": "Page/Alarms"
},
"/charts": {
"viewPath": "Page/Charts"
} }
}, },
"sharedDocks": { "sharedDocks": {}
"cornerPriority": "top-bottom", }
"left": [
{
"anchor": "fixed",
"autoBreakpoint": 768,
"content": "push",
"handle": "hide",
"iconUrl": "",
"id": "menu",
"modal": false,
"resizable": false,
"show": "auto",
"size": 210,
"viewParams": {},
"viewPath": "Docks/Menu"
}
],
"top": [
{
"anchor": "fixed",
"autoBreakpoint": 480,
"content": "push",
"handle": "hide",
"iconUrl": "",
"id": "",
"modal": false,
"resizable": false,
"show": "visible",
"size": 58,
"viewParams": {
"breakpointLarge": "768",
"breakpointMedium": "500",
"params": {},
"path": "Header/Header"
},
"viewPath": "Framework/Breakpoint"
}
]
}
}

View File

@@ -6,11 +6,5 @@
"files": [ "files": [
"config.json" "config.json"
], ],
"attributes": { "attributes": {}
"lastModificationSignature": "1178b10756952abe8fdfa8ebe062253c757d9bf195ebfcd3dea1cc39667f9d48", }
"lastModification": {
"actor": "admin",
"timestamp": "2026-07-16T16:25:30Z"
}
}
}

View File

@@ -1,6 +1,26 @@
{ {
"custom": {}, "custom": {
"PrimeControls": {
"query": {
"startMs": 0,
"endMs": 0,
"rangePreset": "8h",
"priorities": [],
"areas": [],
"states": [],
"search": "",
"refreshToken": 0
},
"ui": {
"selectedTab": 0,
"badActorFocus": ""
}
}
},
"propConfig": { "propConfig": {
"custom.PrimeControls": {
"persistent": true
},
"props.auth": { "props.auth": {
"access": "PRIVATE", "access": "PRIVATE",
"persistent": false "persistent": false
@@ -59,4 +79,4 @@
"theme": "light-cool", "theme": "light-cool",
"timeZoneId": "America/Los_Angeles" "timeZoneId": "America/Los_Angeles"
} }
} }

View File

@@ -6,11 +6,5 @@
"files": [ "files": [
"props.json" "props.json"
], ],
"attributes": { "attributes": {}
"lastModificationSignature": "69e78feccb13d237af1bc1a686c7b63ece6dce7a1a57f3799b1b6b76bfa9f861", }
"lastModification": {
"actor": "admin",
"timestamp": "2026-07-16T16:25:30Z"
}
}
}

View File

@@ -1,16 +0,0 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {
"lastModificationSignature": "6e39cd3d3db12a859cd7b16b710d068fff3fe19768a5e2a81b748d691a316fbf",
"lastModification": {
"actor": "admin",
"timestamp": "2026-07-16T16:25:30Z"
}
}
}

View File

@@ -1,16 +0,0 @@
{
"base": {
"animation": {
"keyframes": {
"0%": {
"color": "#E7F5FF",
"fill": "#E7F5FF"
},
"100%": {
"color": "#FF2E2E",
"fill": "#FF2E2E"
}
}
}
}
}

View File

@@ -1,16 +0,0 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {
"lastModificationSignature": "db4826afdca6732aaf2cd310bc00bedaee3706d5d206d54cc184d3ebe1e05b4c",
"lastModification": {
"actor": "admin",
"timestamp": "2026-07-16T16:25:30Z"
}
}
}

View File

@@ -1,14 +0,0 @@
{
"base": {
"style": {
"backgroundColor": "var(--neutral-20)",
"borderTopLeftRadius": "5px",
"borderTopRightRadius": "5px",
"borderBottomLeftRadius": "5px",
"borderBottomRightRadius": "5px",
"color": "var(--neutral-100)",
"marginBottom": "10px",
"marginTop": "10px"
}
}
}

View File

@@ -1,16 +0,0 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {
"lastModificationSignature": "c28cd10d331bb5be39013910c4f55858498c38e8e3210771ca3a697790f0dfec",
"lastModification": {
"actor": "admin",
"timestamp": "2026-07-16T16:25:30Z"
}
}
}

View File

@@ -1,11 +0,0 @@
{
"base": {
"style": {
"backgroundColor": "#003E69",
"color": "#E7F5FF",
"fontSize": "14px",
"lineHeight": "17px",
"fill": "#FFFFFF"
}
}
}

View File

@@ -1,16 +0,0 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {
"lastModificationSignature": "c5e095bbad7d490925ac24e3f759ea2d74ad726cc09c1cb856d7bd1444f63213",
"lastModification": {
"actor": "admin",
"timestamp": "2026-07-16T16:25:30Z"
}
}
}

View File

@@ -1,9 +0,0 @@
{
"base": {
"style": {
"color": "#FFFFFF",
"cursor": "pointer",
"fill": "#FFFFFF"
}
}
}

View File

@@ -1,16 +0,0 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {
"lastModificationSignature": "607b5d618bc88b56d65682c6c04585b7c154ac7993e2143977382dc4003afa6b",
"lastModification": {
"actor": "admin",
"timestamp": "2026-07-16T16:25:30Z"
}
}
}

View File

@@ -1,12 +0,0 @@
{
"base": {
"style": {
"backgroundColor": "var(--neutral-80)",
"color": "var(--neutral-10)",
"fontSize": "14px",
"fontWeight": "bold",
"lineHeight": "17px",
"textTransform": "none"
}
}
}

View File

@@ -1,16 +0,0 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {
"lastModificationSignature": "a6c5e0b96293d580ef315f10ea2b4a140ce8481b2511c10efd0cf3f24ce6d3ac",
"lastModification": {
"actor": "admin",
"timestamp": "2026-07-16T16:25:30Z"
}
}
}

View File

@@ -1,31 +0,0 @@
{
"base": {
"style": {
"backgroundColor": "var(--neutral-20)",
"borderBottomStyle": "none",
"borderTopColor": "var(--neutral-60)",
"borderTopStyle": "solid",
"borderTopWidth": "1px",
"color": "var(--neutral-80)",
"fontSize": "14px",
"lineHeight": "16px",
"textTransform": "uppercase"
}
},
"variants": [
{
"pseudo": "last-child",
"style": {
"borderBottomColor": "var(--neutral-60)",
"borderBottomStyle": "solid",
"borderBottomWidth": "1px"
}
},
{
"pseudo": "hover",
"style": {
"backgroundColor": "var(--callToActionHighlight)"
}
}
]
}

View File

@@ -1,16 +0,0 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {
"lastModificationSignature": "b36499b99310cacc81571763b37d5744604ca494c16de445fd2ae0cc4dca5e32",
"lastModification": {
"actor": "admin",
"timestamp": "2026-07-16T16:25:30Z"
}
}
}

View File

@@ -1,7 +0,0 @@
{
"base": {
"style": {
"backgroundColor": "var(--neutral-30)"
}
}
}

View File

@@ -1,16 +0,0 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {
"lastModificationSignature": "6176de1d1f29cb0a21052c24356abbfbf6e6d75a7f59e741f0f999a12a6b2f40",
"lastModification": {
"actor": "admin",
"timestamp": "2026-07-16T16:25:30Z"
}
}
}

View File

@@ -1,14 +0,0 @@
{
"base": {
"style": {
"borderBottomStyle": "none",
"borderLeftColor": "var(--neutral-40)",
"borderLeftStyle": "solid",
"borderLeftWidth": "1px",
"borderRightColor": "var(--neutral-40)",
"borderRightStyle": "solid",
"borderRightWidth": "1px",
"borderTopStyle": "none"
}
}
}

View File

@@ -1,16 +0,0 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {
"lastModificationSignature": "f3ab795abf985b237e402d061de7ee0707650690ca199b1ad028cc63f725e35f",
"lastModification": {
"actor": "admin",
"timestamp": "2026-07-16T16:25:30Z"
}
}
}

View File

@@ -1,7 +0,0 @@
{
"base": {
"style": {
"backgroundColor": "var(--neutral-20)"
}
}
}

View File

@@ -1,16 +0,0 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {
"lastModificationSignature": "e5d6bc9707e89cb45f16bc55800c95a150904d3d2d79736685a3bba97c558159",
"lastModification": {
"actor": "admin",
"timestamp": "2026-07-16T16:25:30Z"
}
}
}

View File

@@ -1,7 +0,0 @@
{
"base": {
"style": {
"fontWeight": "bold"
}
}
}

View File

@@ -1,16 +0,0 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {
"lastModificationSignature": "463547a6e49901b11a23984ecf682e31048b29ab5590c7c0811ed61243b70cd8",
"lastModification": {
"actor": "admin",
"timestamp": "2026-07-16T16:25:30Z"
}
}
}

View File

@@ -1,10 +0,0 @@
{
"base": {
"style": {
"marginBottom": "12px",
"marginLeft": "12px",
"marginRight": "12px",
"marginTop": "12px"
}
}
}

View File

@@ -1,16 +0,0 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {
"lastModificationSignature": "0a679916d3e3b1ec10da44d59350746ad43fd686e6a878fe576f69d42b6ba4a8",
"lastModification": {
"actor": "admin",
"timestamp": "2026-07-16T16:25:30Z"
}
}
}

View File

@@ -1,11 +0,0 @@
{
"base": {
"style": {
"borderLeftColor": "var(--neutral-60)",
"borderLeftStyle": "solid",
"borderLeftWidth": "1px",
"fontSize": "14px",
"lineHeight": "20px"
}
}
}

View File

@@ -1,16 +0,0 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {
"lastModificationSignature": "4b2f894a84a1bf9135356109fa6c79266c0aea3a48e56fdaab1086259a230bdb",
"lastModification": {
"actor": "admin",
"timestamp": "2026-07-16T16:25:30Z"
}
}
}

View File

@@ -1,8 +0,0 @@
{
"base": {
"style": {
"fontSize": "14px",
"lineHeight": "20px"
}
}
}

View File

@@ -1,16 +0,0 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {
"lastModificationSignature": "731b1ec590ef2349558f752f42656f8b47cc6157596ff718b5357be5b326549c",
"lastModification": {
"actor": "admin",
"timestamp": "2026-07-16T16:25:30Z"
}
}
}

View File

@@ -1,7 +0,0 @@
{
"base": {
"style": {
"fill": "var(--neutral-90)"
}
}
}

View File

@@ -1,16 +0,0 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {
"lastModificationSignature": "0f91562405222cdff5374db4a3c993d4ef2713b4088d38ea8eed0ce57081da3c",
"lastModification": {
"actor": "admin",
"timestamp": "2026-07-16T16:25:30Z"
}
}
}

View File

@@ -1,12 +0,0 @@
{
"base": {
"style": {
"color": "var(--neutral-90)",
"fontSize": "16px",
"fontWeight": "bold",
"lineHeight": "19px",
"marginLeft": "6px",
"fill": "var(--neutral-90)"
}
}
}

View File

@@ -1,16 +0,0 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {
"lastModificationSignature": "2abac97a687965df9c5e832e6cfc15eeae3a7a792a512835655542d73a6ecdb5",
"lastModification": {
"actor": "admin",
"timestamp": "2026-07-16T16:25:30Z"
}
}
}

View File

@@ -1,13 +0,0 @@
{
"base": {
"style": {
"backgroundColor": "var(--neutral-30)",
"borderBottomColor": "var(--neutral-60)",
"borderBottomStyle": "solid",
"borderBottomWidth": "1px",
"borderTopColor": "var(--neutral-60)",
"borderTopStyle": "solid",
"borderTopWidth": "1px"
}
}
}

View File

@@ -1,17 +0,0 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"view.json",
"thumbnail.png"
],
"attributes": {
"lastModificationSignature": "75100c2d87f37a3ad631acaa15d029ffa0386632a5025625bc6475391e0d6ec7",
"lastModification": {
"actor": "admin",
"timestamp": "2026-07-16T16:25:30Z"
}
}
}

View File

@@ -1,135 +0,0 @@
{
"custom": {},
"params": {},
"props": {
"defaultSize": {
"width": 260
}
},
"root": {
"children": [
{
"meta": {
"name": "Features"
},
"position": {
"grow": 1,
"shrink": 0
},
"propConfig": {
"props.items[0].style.fontWeight": {
"binding": {
"config": {
"path": "page.props.path"
},
"transforms": [
{
"expression": "if ({value} \u003d {this.props.items[0].target}, \"bold\", \"normal\")",
"type": "expression"
}
],
"type": "property"
}
},
"props.items[1].style.fontWeight": {
"binding": {
"config": {
"path": "page.props.path"
},
"transforms": [
{
"expression": "if ({value} \u003d {this.props.items[1].target}, \"bold\", \"normal\")",
"type": "expression"
}
],
"type": "property"
}
},
"props.items[2].style.fontWeight": {
"binding": {
"config": {
"path": "page.props.path"
},
"transforms": [
{
"expression": "if ({value} \u003d {this.props.items[2].target}, \"bold\", \"normal\")",
"type": "expression"
}
],
"type": "property"
}
}
},
"props": {
"backActionStyle": {
"classes": "Menu/All_Header"
},
"backActionText": "All Features",
"itemStyle": {
"classes": "Menu/Item"
},
"items": [
{
"items": [],
"label": {
"icon": {
"path": "material/home"
},
"text": "Home"
},
"navIcon": {},
"showHeader": false,
"style": {
"classes": ""
},
"target": "/"
},
{
"items": [],
"label": {
"icon": {
"path": "material/show_chart"
},
"text": "Charts"
},
"navIcon": {},
"showHeader": false,
"style": {
"classes": ""
},
"target": "/charts"
},
{
"items": [],
"label": {
"icon": {
"path": "material/alarm"
},
"text": "Alarms"
},
"navIcon": {},
"showHeader": false,
"style": {
"classes": ""
},
"target": "/alarms"
}
],
"style": {
"classes": "Menu/Menu"
}
},
"type": "ia.navigation.menutree"
}
],
"meta": {
"name": "root"
},
"props": {
"style": {
"classes": "Menu/Menu"
}
},
"type": "ia.container.flex"
}
}

View File

@@ -1,17 +0,0 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"view.json",
"thumbnail.png"
],
"attributes": {
"lastModificationSignature": "3ccf49cb05813e379514547dfed3f952393d9c1b05697e08be75eb6fc028b12e",
"lastModification": {
"actor": "admin",
"timestamp": "2026-07-16T16:25:30Z"
}
}
}

View File

@@ -1,107 +0,0 @@
{
"custom": {},
"params": {
"breakpoint": 500,
"params": {},
"path": "Header/Header"
},
"propConfig": {
"params.breakpoint": {
"paramDirection": "input",
"persistent": true
},
"params.params": {
"paramDirection": "input",
"persistent": true
},
"params.path": {
"paramDirection": "input",
"persistent": true
}
},
"props": {
"defaultSize": {
"height": 58,
"width": 512
}
},
"root": {
"children": [
{
"meta": {
"name": "Large"
},
"position": {
"size": "large"
},
"propConfig": {
"props.params.params": {
"binding": {
"config": {
"path": "view.params.params"
},
"type": "property"
}
},
"props.path": {
"binding": {
"config": {
"path": "view.params.path"
},
"type": "property"
}
}
},
"props": {
"params": {
"size": "medium"
}
},
"type": "ia.display.view"
},
{
"meta": {
"name": "Small"
},
"propConfig": {
"props.params.params": {
"binding": {
"config": {
"path": "view.params.params"
},
"type": "property"
}
},
"props.path": {
"binding": {
"config": {
"path": "view.params.path"
},
"type": "property"
}
}
},
"props": {
"params": {
"size": "small"
}
},
"type": "ia.display.view"
}
],
"meta": {
"name": "root"
},
"propConfig": {
"props.breakpoint": {
"binding": {
"config": {
"path": "view.params.breakpoint"
},
"type": "property"
}
}
},
"type": "ia.container.breakpt"
}
}

View File

@@ -1,17 +0,0 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"view.json",
"thumbnail.png"
],
"attributes": {
"lastModificationSignature": "12b9bfe565becb15573e4c764975c880fa3363b0f4b39fd792ed620712bac0d2",
"lastModification": {
"actor": "admin",
"timestamp": "2026-07-16T16:25:30Z"
}
}
}

View File

@@ -1,118 +0,0 @@
{
"custom": {},
"params": {
"breakpointLarge": 768,
"breakpointMedium": 500,
"params": {},
"path": "Header/Header"
},
"propConfig": {
"params.breakpointLarge": {
"paramDirection": "input",
"persistent": true
},
"params.breakpointMedium": {
"paramDirection": "input",
"persistent": true
},
"params.params": {
"paramDirection": "input",
"persistent": true
},
"params.path": {
"paramDirection": "input",
"persistent": true
}
},
"props": {
"defaultSize": {
"height": 58,
"width": 818
}
},
"root": {
"children": [
{
"meta": {
"name": "Large"
},
"position": {
"size": "large"
},
"propConfig": {
"props.params.params": {
"binding": {
"config": {
"path": "view.params.params"
},
"type": "property"
}
},
"props.path": {
"binding": {
"config": {
"path": "view.params.path"
},
"type": "property"
}
}
},
"props": {
"params": {
"size": "large"
}
},
"type": "ia.display.view"
},
{
"meta": {
"name": "Small"
},
"propConfig": {
"props.params.breakpoint": {
"binding": {
"config": {
"path": "view.params.breakpointMedium"
},
"type": "property"
}
},
"props.params.params": {
"binding": {
"config": {
"path": "view.params.params"
},
"type": "property"
}
},
"props.params.path": {
"binding": {
"config": {
"path": "view.params.path"
},
"type": "property"
}
}
},
"props": {
"path": "Framework/Breakpoint Embedded"
},
"type": "ia.display.view"
}
],
"meta": {
"name": "root"
},
"propConfig": {
"props.breakpoint": {
"binding": {
"config": {
"path": "view.params.breakpointLarge"
},
"type": "property"
}
}
},
"type": "ia.container.breakpt"
}
}

View File

@@ -1,17 +0,0 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"view.json",
"thumbnail.png"
],
"attributes": {
"lastModificationSignature": "9e00070a2bf48325f01052c849970bd7d5919423207b466df4fd442ee24a800d",
"lastModification": {
"actor": "admin",
"timestamp": "2026-07-16T16:25:30Z"
}
}
}

View File

@@ -1,643 +0,0 @@
{
"custom": {},
"params": {
"params": {},
"size": "medium"
},
"propConfig": {
"params.params": {
"paramDirection": "input",
"persistent": true
},
"params.size": {
"paramDirection": "input"
}
},
"props": {
"defaultSize": {
"height": 58
}
},
"root": {
"children": [
{
"meta": {
"hasDelegate": true,
"name": "Spacer Start"
},
"position": {
"basis": "16px",
"shrink": 0
},
"props": {
"text": " "
},
"type": "ia.display.label"
},
{
"events": {
"dom": {
"onClick": {
"config": {
"id": "menu",
"type": "toggle"
},
"scope": "C",
"type": "dock"
}
}
},
"meta": {
"name": "Menu Dock"
},
"position": {
"basis": "24px",
"shrink": 0
},
"propConfig": {
"position.display": {
"binding": {
"config": {
"expression": "{view.params.size} !\u003d \"large\""
},
"type": "expr"
}
}
},
"props": {
"path": "material/menu",
"style": {
"classes": "Header/Icon",
"marginRight": "10px"
}
},
"type": "ia.display.icon"
},
{
"children": [
{
"children": [
{
"children": [
{
"meta": {
"name": "path2"
},
"props": {
"fill": {
"opacity": "1"
},
"path": "m 37.344391,71.227685 v 25.297 h -3.817 v -25.297 z",
"style": {
"classes": "Header/Icon"
}
},
"type": "ia.shapes.path"
},
{
"meta": {
"name": "path4"
},
"props": {
"fill": {
"opacity": "1"
},
"path": "m 56.308391,96.148685 c 0,0.754 -0.029,1.417 -0.075,1.991 -0.049,0.574 -0.118,1.077 -0.205,1.513 -0.262,1.201995 -0.775,2.235995 -1.542,3.097995 -1.438,1.664 -3.425,2.491 -5.945,2.491 -2.13,0 -3.884,-0.575 -5.26,-1.728 -1.421,-1.186 -2.24,-2.821 -2.458,-4.907995 h 3.702 c 0.144,0.787 0.378,1.393998 0.706,1.818995 0.764,0.993 1.879,1.49 3.342,1.49 2.698,0 4.047,-1.646 4.047,-4.945995 v -2.228 c -1.464,1.496 -3.152,2.244 -5.063,2.244 -2.172,0 -3.953,-0.788 -5.341,-2.359 -1.396,-1.595 -2.096,-3.589 -2.096,-5.98 0,-2.327 0.649,-4.303 1.949,-5.93 1.398,-1.729 3.244,-2.59 5.538,-2.59 2.011,0 3.681,0.746 5.014,2.245 v -1.803 h 3.688 v 15.581 z m -3.54,-7.561 c 0,-1.549 -0.417,-2.789 -1.245,-3.713 -0.839,-0.949 -1.915,-1.424 -3.227,-1.424 -1.4,0 -2.503,0.518 -3.312,1.553 -0.73,0.927 -1.098,2.129 -1.098,3.599 0,1.451 0.367,2.639 1.098,3.566 0.797,1.016 1.902,1.523 3.312,1.523 1.408,0 2.522,-0.513 3.342,-1.539 0.751,-0.927 1.13,-2.114 1.13,-3.565 z",
"style": {
"classes": "Header/Icon"
}
},
"type": "ia.shapes.path"
},
{
"meta": {
"name": "path6"
},
"props": {
"fill": {
"opacity": "1"
},
"path": "m 59.950391,80.567685 h 3.705 v 1.474 c 1.29,-1.278 2.743,-1.916 4.36,-1.916 1.857,0 3.305,0.585 4.342,1.753 0.895,0.994 1.344,2.615 1.344,4.865 v 9.782 h -3.703 v -8.902 c 0,-1.569 -0.221,-2.656 -0.657,-3.257 -0.425,-0.609 -1.202,-0.916 -2.326,-0.916 -1.224,0 -2.093,0.403 -2.606,1.21 -0.502,0.797 -0.754,2.189 -0.754,4.173 v 7.692 h -3.705 z",
"style": {
"classes": "Header/Icon"
}
},
"type": "ia.shapes.path"
},
{
"meta": {
"name": "path8"
},
"props": {
"fill": {
"opacity": "1"
},
"path": "m 76.790391,73.930685 c 0,-0.644 0.233,-1.202 0.702,-1.67 0.473,-0.47 1.033,-0.703 1.691,-0.703 0.663,0 1.234,0.233 1.701,0.703 0.473,0.457 0.708,1.023 0.708,1.688 0,0.667 -0.235,1.234 -0.708,1.704 -0.456,0.468 -1.02,0.705 -1.685,0.705 -0.669,0 -1.234,-0.236 -1.707,-0.705 -0.469,-0.471 -0.702,-1.045 -0.702,-1.722 z m 4.241,6.637 v 15.958 h -3.687 v -15.958 z",
"style": {
"classes": "Header/Icon"
}
},
"type": "ia.shapes.path"
},
{
"meta": {
"name": "path10"
},
"props": {
"fill": {
"opacity": "1"
},
"path": "m 89.170391,84.006685 v 12.519 h -3.689 v -12.519 h -1.574 v -3.439 h 1.574 v -5.849 h 3.689 v 5.849 h 2.865 v 3.439 z",
"style": {
"classes": "Header/Icon"
}
},
"type": "ia.shapes.path"
},
{
"meta": {
"name": "path12"
},
"props": {
"fill": {
"opacity": "1"
},
"path": "m 93.649391,73.930685 c 0,-0.644 0.233,-1.202 0.705,-1.67 0.468,-0.47 1.031,-0.703 1.688,-0.703 0.667,0 1.233,0.233 1.704,0.703 0.468,0.457 0.705,1.023 0.705,1.688 0,0.667 -0.236,1.234 -0.705,1.704 -0.459,0.468 -1.021,0.705 -1.688,0.705 -0.666,0 -1.235,-0.236 -1.704,-0.705 -0.472,-0.471 -0.705,-1.045 -0.705,-1.722 z m 4.243,6.637 v 15.958 h -3.687 v -15.958 z",
"style": {
"classes": "Header/Icon"
}
},
"type": "ia.shapes.path"
},
{
"meta": {
"name": "path14"
},
"props": {
"fill": {
"opacity": "1"
},
"path": "m 100.80139,88.432685 c 0,-2.305 0.826,-4.266 2.476,-5.881 1.651,-1.619 3.66,-2.426 6.03,-2.426 2.381,0 4.4,0.813 6.062,2.442 1.636,1.627 2.459,3.628 2.459,5.998 0,2.389 -0.83,4.395 -2.476,6.011 -1.662,1.604 -3.698,2.408 -6.111,2.408 -2.393,0 -4.398,-0.816 -6.014,-2.458 -1.619,-1.616 -2.426,-3.646 -2.426,-6.094 z m 3.77,0.067 c 0,1.59 0.425,2.856 1.278,3.783 0.873,0.938 2.025,1.41 3.457,1.41 1.44,0 2.596,-0.463 3.457,-1.394 0.861,-0.927 1.294,-2.167 1.294,-3.721 0,-1.55 -0.433,-2.791 -1.294,-3.717 -0.873,-0.94 -2.028,-1.41 -3.457,-1.41 -1.411,0 -2.552,0.47 -3.425,1.41 -0.874,0.938 -1.31,2.151 -1.31,3.639 z",
"style": {
"classes": "Header/Icon"
}
},
"type": "ia.shapes.path"
},
{
"meta": {
"name": "path16"
},
"props": {
"fill": {
"opacity": "1"
},
"path": "m 132.36239,82.585685 c -0.544,-0.74 -1.026,-1.406 -1.483,-2.036 -0.715,-0.279 -1.523,-0.425 -2.434,-0.425 -1.619,0 -3.07,0.638 -4.36,1.916 v -1.474 h -3.703 v 15.958 h 3.703 v -7.692 c 0,-1.983 0.253,-3.375 0.755,-4.173 0.513,-0.807 1.384,-1.21 2.606,-1.21 1.125,0 1.898,0.307 2.326,0.916 0.434,0.601 0.652,1.688 0.652,3.257 v 8.902 h 3.705 v -9.782 c 0,-0.719 -0.05,-1.37 -0.143,-1.963 -0.587,-0.792 -1.142,-1.54 -1.624,-2.194 z",
"style": {
"classes": "Header/Icon"
}
},
"type": "ia.shapes.path"
},
{
"meta": {
"name": "path18"
},
"props": {
"fill": {
"paint": "#f7901e"
},
"path": "m 170.41439,49.972685 c -0.428,-0.949 -1.103,-1.685 -2.024,-2.208 -0.927,-0.521 -1.903,-0.723 -2.933,-0.598 -1.034,0.12 -1.923,0.836 -2.664,2.151 -0.14,0.241 -0.494,0.984 -1.069,2.232 -0.578,1.245 -1.313,2.821 -2.2,4.728 -0.885,1.903 -1.892,4.048 -3.013,6.43 -1.126,2.381 -2.287,4.887 -3.484,7.513 -1.202,2.627 -2.412,5.277 -3.638,7.956 -1.201,2.625 -2.346,5.116 -3.426,7.47 0.518,0.222 1.007,0.508 1.473,0.854 2.222,-3.257 4.599,-6.724 7.125,-10.401 2.557,-3.722 4.931,-7.207 7.126,-10.46 2.196,-3.25 4.064,-6.014 5.606,-8.284 1.544,-2.271 2.425,-3.602 2.644,-3.991 0.745,-1.316 0.905,-2.443 0.477,-3.392 z"
},
"type": "ia.shapes.path"
},
{
"meta": {
"name": "path20"
},
"props": {
"fill": {
"paint": "#f7901e"
},
"path": "m 139.79939,85.710685 c -0.701,-2.8 -1.275,-5.053 -1.715,-6.762 -0.449,-1.726 -0.823,-3.093 -1.114,-4.1 -0.29,-1.003 -0.521,-1.742 -0.69,-2.21 -0.171,-0.47 -0.335,-0.871 -0.493,-1.2 -0.377,-0.79 -0.934,-1.306 -1.666,-1.547 -0.734,-0.241 -1.518,-0.161 -2.351,0.237 -0.834,0.398 -1.389,0.96 -1.66,1.681 -0.276,0.724 -0.223,1.48 0.15,2.27 0.159,0.329 0.367,0.708 0.626,1.136 0.258,0.427 0.686,1.072 1.287,1.931 0.598,0.858 1.429,2.002 2.488,3.435 1.058,1.432 2.469,3.31 4.23,5.643 0.296,-0.193 0.599,-0.369 0.908,-0.514 z"
},
"type": "ia.shapes.path"
},
{
"children": [
{
"meta": {
"name": "path64"
},
"props": {
"fill": {
"paint": "#f7901e"
},
"path": "m 336.294,345.251 c 0,-0.837 0.668,-1.505 1.513,-1.505 0.846,0 1.512,0.668 1.512,1.505 0,0.838 -0.666,1.504 -1.512,1.504 -0.845,0 -1.513,-0.666 -1.513,-1.504 z m 2.794,0 c 0,-0.709 -0.591,-1.274 -1.281,-1.274 -0.692,0 -1.28,0.565 -1.28,1.274 0,0.709 0.588,1.273 1.28,1.273 0.69,0 1.281,-0.564 1.281,-1.273 z m -0.564,0.814 h -0.279 l -0.494,-0.76 h -0.274 v 0.76 h -0.234 v -1.647 h 0.72 c 0.297,0 0.585,0.081 0.585,0.435 0,0.324 -0.226,0.444 -0.52,0.453 z m -0.773,-0.992 c 0.206,0 0.565,0.04 0.565,-0.223 0,-0.17 -0.226,-0.2 -0.402,-0.2 h -0.438 v 0.423 z"
},
"type": "ia.shapes.path"
}
],
"meta": {
"name": "g66"
},
"props": {
"transform": "translate(-185.14061,-250.00331)"
},
"type": "ia.shapes.group"
},
{
"meta": {
"name": "circle68"
},
"props": {
"cx": "142.94638",
"cy": "92.620682",
"fill": {
"paint": "#f7901e"
},
"r": "4.652"
},
"type": "ia.shapes.circle"
}
],
"meta": {
"name": "g196"
},
"props": {
"transform": "translate(0,1.2700031)"
},
"type": "ia.shapes.group"
}
],
"meta": {
"name": "g111"
},
"props": {
"transform": "translate(-33.527391,-48.400683)"
},
"type": "ia.shapes.group"
}
],
"events": {
"dom": {
"onClick": {
"config": {
"page": "/"
},
"scope": "C",
"type": "nav"
}
}
},
"meta": {
"name": "Ignition Logo"
},
"position": {
"basis": "100px",
"shrink": 0
},
"props": {
"style": {
"cursor": "pointer"
},
"viewBox": "0 0 137.121 58.111001"
},
"type": "ia.container.drawing"
},
{
"meta": {
"hasDelegate": true,
"name": "Filler"
},
"position": {
"grow": 1
},
"props": {
"text": " "
},
"type": "ia.display.label"
},
{
"meta": {
"name": "Dropdown"
},
"position": {
"basis": "124px",
"shrink": 0
},
"propConfig": {
"props.value": {
"binding": {
"config": {
"bidirectional": true,
"path": "session.props.theme"
},
"type": "property"
}
}
},
"props": {
"options": [
{
"label": "light",
"value": "light"
},
{
"label": "light-cool",
"value": "light-cool"
},
{
"label": "light-warm",
"value": "light-warm"
},
{
"label": "dark",
"value": "dark"
},
{
"label": "dark-cool",
"value": "dark-cool"
},
{
"label": "dark-warm",
"value": "dark-warm"
}
],
"search": {
"enabled": false
},
"style": {
"classes": "Header/DropDown",
"textAlign": "left"
}
},
"type": "ia.input.dropdown"
},
{
"meta": {
"hasDelegate": true,
"name": "Spacer 1"
},
"position": {
"basis": "10px"
},
"props": {
"text": " "
},
"type": "ia.display.label"
},
{
"events": {
"dom": {
"onClick": {
"config": {
"script": "\tif self.session.props.auth.authenticated:\n\t\tsystem.perspective.logout()\n\telse:\n\t\tsystem.perspective.login()"
},
"scope": "G",
"type": "script"
}
}
},
"meta": {
"name": "User"
},
"position": {
"basis": "30px",
"shrink": 0
},
"props": {
"path": "material/person",
"style": {
"classes": "Header/Icon"
}
},
"type": "ia.display.icon"
},
{
"events": {
"dom": {
"onClick": {
"config": {
"script": "\tif self.session.props.auth.authenticated:\n\t\tsystem.perspective.logout()\n\telse:\n\t\tsystem.perspective.login()"
},
"scope": "G",
"type": "script"
}
}
},
"meta": {
"hasDelegate": true,
"name": "Sign In"
},
"position": {
"shrink": 0
},
"propConfig": {
"position.display": {
"binding": {
"config": {
"expression": "{view.params.size} !\u003d \"small\""
},
"type": "expr"
}
},
"props.text": {
"binding": {
"config": {
"path": "session.props.auth.user.userName"
},
"transforms": [
{
"expression": "if({value} \u003d null || len({value}) \u003d 0, \"Sign In\", {value})",
"type": "expression"
}
],
"type": "property"
}
}
},
"props": {
"style": {
"classes": "Header/Icon",
"cursor": "pointer",
"marginLeft": "4px"
}
},
"type": "ia.display.label"
},
{
"meta": {
"hasDelegate": true,
"name": "Spacer 2"
},
"position": {
"basis": "10px",
"shrink": 0
},
"props": {
"text": " "
},
"type": "ia.display.label"
},
{
"events": {
"dom": {
"onClick": {
"config": {
"page": "/alarms"
},
"scope": "C",
"type": "nav"
}
}
},
"meta": {
"name": "Alarm Icon"
},
"position": {
"basis": "26px",
"shrink": 0
},
"propConfig": {
"props.style.classes": {
"binding": {
"config": {
"path": "../Alarms.custom.numAlarms"
},
"transforms": [
{
"expression": "if({value} \u003e 0, \"Header/Alarm_Active\", \"Header/Icon\")",
"type": "expression"
}
],
"type": "property"
}
}
},
"props": {
"path": "material/alarm"
},
"type": "ia.display.icon"
},
{
"events": {
"dom": {
"onClick": {
"config": {
"page": "/alarms"
},
"scope": "C",
"type": "nav"
}
}
},
"meta": {
"hasDelegate": true,
"name": "Alarms"
},
"position": {
"shrink": 0
},
"propConfig": {
"custom.numAlarms": {
"binding": {
"config": {
"expression": "now(2000)"
},
"transforms": [
{
"code": "\treturn len(system.alarm.queryStatus(state\u003d[\"ActiveUnacked\"]))",
"type": "script"
}
],
"type": "expr"
}
},
"position.display": {
"binding": {
"config": {
"expression": "{view.params.size} !\u003d \"small\""
},
"type": "expr"
}
},
"props.style.classes": {
"binding": {
"config": {
"path": "../Alarms.custom.numAlarms"
},
"transforms": [
{
"expression": "if({value} \u003e 0, \"Header/Alarm_Active\", \"\")",
"type": "expression"
}
],
"type": "property"
}
},
"props.text": {
"binding": {
"config": {
"path": "this.custom.numAlarms"
},
"transforms": [
{
"expression": "numberFormat({value}, \"#,##0\") + \" alarm\" + if({value} \u003e 1 || {value} \u003d 0, \"s\", \"\")",
"type": "expression"
}
],
"type": "property"
}
}
},
"props": {
"style": {
"cursor": "pointer",
"marginLeft": "6px"
}
},
"type": "ia.display.label"
},
{
"meta": {
"hasDelegate": true,
"name": "Spacer End"
},
"position": {
"basis": "16px",
"shrink": 0
},
"props": {
"text": " "
},
"type": "ia.display.label"
}
],
"meta": {
"name": "root"
},
"propConfig": {
"props.style.backgroundColor": {
"binding": {
"config": {
"path": "session.props.theme"
},
"transforms": [
{
"code": "\tif \"dark\" in value:\n\t\treturn \"var(--neutral-10)\"\n\telse:\n\t\treturn \"\"",
"type": "script"
}
],
"type": "property"
}
}
},
"props": {
"style": {
"classes": "Header/Header"
}
},
"type": "ia.container.flex"
}
}

View File

@@ -1,17 +0,0 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"view.json",
"thumbnail.png"
],
"attributes": {
"lastModificationSignature": "295c4883382d0243d3e8270d5a208fec97cf3bd84a43573e894c552707c0480c",
"lastModification": {
"actor": "admin",
"timestamp": "2026-07-16T16:25:30Z"
}
}
}

View File

@@ -1,66 +0,0 @@
{
"custom": {},
"params": {},
"propConfig": {
"params.feature": {
"paramDirection": "input"
},
"params.key": {
"paramDirection": "input"
},
"params.tier1": {
"paramDirection": "input"
},
"params.tier2": {
"paramDirection": "input"
}
},
"props": {},
"root": {
"children": [
{
"meta": {
"name": "Title"
},
"position": {
"basis": "56px",
"shrink": 0
},
"props": {
"params": {
"icon": "alarm",
"text": "Alarms"
},
"path": "Page/Embedded/Title"
},
"type": "ia.display.view"
},
{
"meta": {
"name": "AlarmStatusTable"
},
"position": {
"basis": "400px",
"grow": 1
},
"props": {
"style": {
"classes": "Page/Alarm/Alarm"
}
},
"type": "ia.display.alarmstatustable"
}
],
"meta": {
"name": "root"
},
"props": {
"alignContent": "flex-start",
"direction": "column",
"style": {
"classes": "Page/Alarm/Page Page/Page"
}
},
"type": "ia.container.flex"
}
}

View File

@@ -1,17 +0,0 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"view.json",
"thumbnail.png"
],
"attributes": {
"lastModificationSignature": "5023f2710f4d702828d7fc9f6d63dc16ce483727872ce0ee0c71af7446129630",
"lastModification": {
"actor": "admin",
"timestamp": "2026-07-16T16:25:30Z"
}
}
}

View File

@@ -1,60 +0,0 @@
{
"custom": {},
"params": {},
"propConfig": {
"params.feature": {
"paramDirection": "input"
},
"params.key": {
"paramDirection": "input"
},
"params.tier1": {
"paramDirection": "input"
},
"params.tier2": {
"paramDirection": "input"
}
},
"props": {},
"root": {
"children": [
{
"meta": {
"name": "Title"
},
"position": {
"basis": "56px",
"shrink": 0
},
"props": {
"params": {
"icon": "show_chart",
"text": "Charts"
},
"path": "Page/Embedded/Title"
},
"type": "ia.display.view"
},
{
"meta": {
"name": "PowerChart"
},
"position": {
"basis": "600px",
"grow": 1
},
"type": "ia.chart.powerchart"
}
],
"meta": {
"name": "root"
},
"props": {
"direction": "column",
"style": {
"classes": "Page/Page"
}
},
"type": "ia.container.flex"
}
}

View File

@@ -1,17 +0,0 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"view.json",
"thumbnail.png"
],
"attributes": {
"lastModificationSignature": "4faccd858e984072a9442439dc3f60d3c0332ce5d8c8f116e14cb6037916b80e",
"lastModification": {
"actor": "admin",
"timestamp": "2026-07-16T16:25:30Z"
}
}
}

View File

@@ -1,89 +0,0 @@
{
"custom": {},
"params": {
"icon": "home",
"text": "Home"
},
"propConfig": {
"params.icon": {
"paramDirection": "input",
"persistent": true
},
"params.text": {
"paramDirection": "input",
"persistent": true
}
},
"props": {
"defaultSize": {
"height": 56,
"width": 600
}
},
"root": {
"children": [
{
"meta": {
"name": "Icon"
},
"position": {
"basis": "24px",
"shrink": 0
},
"propConfig": {
"props.path": {
"binding": {
"config": {
"expression": "\"material/\" + {view.params.icon}"
},
"type": "expr"
},
"persistent": false
}
},
"props": {
"style": {
"classes": "Title/Icon",
"marginLeft": "10px"
}
},
"type": "ia.display.icon"
},
{
"meta": {
"hasDelegate": true,
"name": "Label"
},
"position": {
"grow": 1
},
"propConfig": {
"props.text": {
"binding": {
"config": {
"path": "view.params.text"
},
"type": "property"
},
"persistent": false
}
},
"props": {
"style": {
"classes": "Title/Text"
}
},
"type": "ia.display.label"
}
],
"meta": {
"name": "root"
},
"props": {
"style": {
"classes": "Title/Title"
}
},
"type": "ia.container.flex"
}
}

View File

@@ -1,17 +0,0 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"view.json",
"thumbnail.png"
],
"attributes": {
"lastModificationSignature": "c74f714316b78db6782c355f8ab653ae63b8e66be4b7125ac0bd4ed52fc906ac",
"lastModification": {
"actor": "admin",
"timestamp": "2026-07-16T16:25:30Z"
}
}
}

View File

@@ -1,92 +0,0 @@
{
"custom": {},
"params": {},
"propConfig": {
"params.feature": {
"paramDirection": "input"
},
"params.key": {
"paramDirection": "input"
},
"params.tier1": {
"paramDirection": "input"
},
"params.tier2": {
"paramDirection": "input"
}
},
"props": {},
"root": {
"children": [
{
"meta": {
"name": "Title"
},
"position": {
"basis": "56px",
"shrink": 0
},
"props": {
"params": {
"icon": "home",
"text": "Home"
},
"path": "Page/Embedded/Title"
},
"type": "ia.display.view"
},
{
"meta": {
"hasDelegate": true,
"name": "Welcome"
},
"position": {
"basis": "20px",
"shrink": 0
},
"props": {
"style": {
"classes": "Page/Bold Page/Margins Page/Text"
},
"text": "Welcome to Perspective!"
},
"type": "ia.display.label"
},
{
"meta": {
"hasDelegate": true,
"name": "Description"
},
"position": {
"shrink": 0
},
"props": {
"style": {
"classes": "Page/Text Page/Margins"
},
"text": "The skeleton project is intended to help you get started with Perspective. The project is designed to be mobile-responsive and fully utilizes styles. Feel free to modify as desired."
},
"type": "ia.display.label"
},
{
"meta": {
"name": "CoordinateContainer"
},
"position": {
"grow": 1
},
"type": "ia.container.coord"
}
],
"meta": {
"name": "root"
},
"props": {
"direction": "column",
"style": {
"classes": "Page/Page"
}
},
"type": "ia.container.flex"
}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"view.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,40 @@
{
"custom": {},
"params": {},
"propConfig": {},
"props": {
"defaultSize": {
"height": 900,
"width": 1600
}
},
"root": {
"children": [
{
"meta": {
"name": "lblPlaceholder"
},
"position": {
"grow": 0
},
"props": {
"text": "PrimeControls Dashboard - under construction (Phase 0 placeholder)",
"textStyle": {
"fontSize": "1.2em"
}
},
"type": "ia.display.label"
}
],
"custom": {},
"meta": {
"name": "root"
},
"props": {
"alignItems": "center",
"direction": "column",
"justify": "center"
},
"type": "ia.container.flex"
}
}

View File

@@ -0,0 +1,9 @@
{
"pages": {
"/scratch": {
"title": "Scratch",
"viewPath": "Scratch/Current"
}
},
"sharedDocks": {}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"config.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"view.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,27 @@
{
"custom": {},
"params": {},
"propConfig": {},
"props": {},
"root": {
"children": [
{
"meta": {
"name": "lblScratch"
},
"props": {
"text": "SimHarness scratch view - overwrite me to isolate a component under test."
},
"type": "ia.display.label"
}
],
"custom": {},
"meta": {
"name": "root"
},
"props": {
"direction": "column"
},
"type": "ia.container.flex"
}
}

View File

@@ -0,0 +1,179 @@
# alarm_simulator.py — Build-a-Thon alarm activity generator (Jython, Ignition 8.3)
#
# Drives the Boolean memory tags in [default]BuildathonSim/ (import
# simulation_tags.json first). Designed to be called every 5 seconds:
#
# 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
import random
import time
BASE = "[default]BuildathonSim"
BASELINE = {
"Intake": ["Pump1_Fault", "Pump2_Fault", "ScreenDiffPressHigh", "InletFlowLow",
"ValveV101_FailToOpen", "WetWellLevelHigh", "SampleTempHigh", "PowerMonitorAlarm"],
"BoilerHouse": ["FeedPumpA_Fault", "FeedPumpB_Fault", "SteamPressHigh", "DrumLevelLow",
"StackTempHigh", "FuelGasPressLow", "EconomizerDPHigh", "BlowdownConductivityHigh"],
"Packaging": ["LabelerFault", "CapperTorqueLow", "FillerLevelDeviation", "ConveyorOverload",
"CaseSealerJam", "PrinterInkLow", "RejectRateHigh", "GuardDoorOpen"],
"Utilities": ["AirCompA_Fault", "AirHeaderPressLow", "ChillerTripped", "CoolingTowerVibration",
"GlycolTempHigh", "N2PressLow", "UPSOnBattery", "WaterSoftenerFault"],
"TankFarm": ["T101_LevelHigh", "T102_LevelHigh", "T103_TempHigh", "TransferPumpFault",
"ContainmentSumpHigh", "VaporRecoveryFault", "T104_PressHigh", "ManifoldLeakDetect"],
}
CHATTER = [
"Intake/ConveyorJam_Chatter",
"Packaging/ShrinkTunnelTemp_Chatter",
"Utilities/AirDryerDewpoint_Chatter",
]
STANDING = [
"BoilerHouse/DrumLevelXmtrFail_Standing",
"TankFarm/T105_LevelXmtrFail_Standing",
]
FLEETING = [
"Intake/CommsGlitch_Fleeting",
"BoilerHouse/FlameScannerFlicker_Fleeting",
"Packaging/EStopPulse_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
def _path(rel):
return "%s/%s" % (BASE, rel)
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
"standing_set": False,
}
return g["buildathon_alarmsim"]
def _write(pairs):
if pairs:
system.tag.writeBlocking([p for p, v in pairs], [v for p, v in pairs])
def tick():
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]
# 3. Baseline activity: random tag, active 30 s - 5 min
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)
if now >= nxt and path not in st["clears"]:
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)
elif now >= st["flood"]["until"]:
st["flood"] = None
else:
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)
def _start_flood(st, now):
area = random.choice(list(BASELINE.keys()))
st["flood"] = {"area": area, "until": now + FLOOD_DURATION}
system.util.getLogger("alarmsim").info("Flood burst started in area: %s" % area)
def force_flood():
"""Start a flood burst immediately (handy for demo/testing)."""
_start_flood(_state(), time.time())
def reset():
"""Clear all simulation tags and internal state."""
g = system.util.getGlobals()
g.pop("buildathon_alarmsim", None)
paths = []
for area, names in BASELINE.items():
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)

View File

@@ -0,0 +1,12 @@
{
"scope": "A",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"code.py"
],
"attributes": {
"hintScope": 2
}
}

View File

@@ -0,0 +1,179 @@
# probe - gateway-side API ground truth (SimHarness, dev-only, Jython 2.7).
#
# Runs inside the gateway via the ProbeTick timer (enable it, scan, wait <=20s,
# read the output, disable it). Writes JSON to the bind-mounted repo path:
# container: /usr/local/bin/ignition/data/projects/.probe/out.json
# host repo: ignition/gateway/projects/.probe/out.json
#
# Edit PROBE_NAME / add sections as needed; every section is try-wrapped so a
# single API surprise never kills the whole probe.
import json
OUT_DIR = "/usr/local/bin/ignition/data/projects/.probe"
PROBE_NAME = "P0"
def _safe(fn):
# Bare except is deliberate: in Jython, JAVA exceptions (e.g. bad kwargs
# into system.* calls) do NOT subclass Python's Exception and would fly
# straight past 'except Exception'.
try:
return {"ok": True, "value": fn()}
except:
import sys
et, ev = sys.exc_info()[0], sys.exc_info()[1]
return {"ok": False, "error": "%s: %s" % (getattr(et, "__name__", et), ev)}
def _write(result):
from java.io import File
File(OUT_DIR).mkdirs()
f = open(OUT_DIR + "/out.json", "w")
try:
f.write(json.dumps(result, indent=1, default=lambda o: repr(o)))
finally:
f.close()
def _prop(evt, names):
out = {}
for n in names:
try:
v = evt.get(n)
out[n] = {"cls": v is not None and v.__class__.__name__ or None,
"repr": repr(v)[:200]}
except Exception, e:
out[n] = {"error": "%s" % e}
return out
def _entry_facts(evt):
d = {}
d["cls"] = evt.__class__.__name__
try:
d["id"] = str(evt.getId())
except Exception, e:
d["id_error"] = "%s" % e
try:
s = evt.getState()
d["state_str"] = str(s)
d["state_cls"] = s is not None and s.__class__.__name__ or None
except Exception, e:
d["state_error"] = "%s" % e
try:
d["source"] = str(evt.getSource())
except Exception, e:
d["source_error"] = "%s" % e
try:
d["displayPath"] = repr(evt.getDisplayPath())
except Exception, e:
d["displayPath_error"] = "%s" % e
try:
p = evt.getPriority()
d["priority_cls"] = p is not None and p.__class__.__name__ or None
d["priority_str"] = str(p)
try:
d["priority_int"] = int(p.ordinal())
except Exception:
try:
d["priority_int"] = int(p)
except Exception:
d["priority_int"] = None
except Exception, e:
d["priority_error"] = "%s" % e
d["props"] = _prop(evt, ["eventTime", "eventType", "eventtype", "ackUser",
"ackUserName", "isSystemEvent", "systemEvent",
"eventValue", "name", "label"])
for getter in ("getActiveData", "getClearedData", "getAckData"):
try:
v = getattr(evt, getter)()
d[getter] = v is not None and repr(v)[:200] or None
except Exception, e:
d[getter] = "ERR %s" % e
try:
from java.util import Date
t = evt.get("eventTime")
if t is not None and isinstance(t, Date):
d["eventTime_ms"] = t.getTime()
except Exception:
pass
return d
def _p0():
result = {}
end = system.date.now()
start = system.date.addHours(end, -8)
def q_basic():
r = system.alarm.queryJournal(startDate=start, endDate=end)
entries = [e for e in r]
by_id = {}
for e in entries:
try:
k = str(e.getId())
except Exception:
k = "?"
by_id.setdefault(k, []).append(e)
multi = {}
n = 0
for k, evts in by_id.items():
if len(evts) > 1 and n < 5:
states = []
for e in evts:
try:
states.append(str(e.getState()))
except Exception:
states.append("?")
multi[k] = states
n += 1
return {
"result_cls": r.__class__.__name__,
"len_works": _safe(lambda: len(r)),
"dataset_attr": hasattr(r, "getDataset"),
"entry_count": len(entries),
"distinct_ids": len(by_id),
"multi_state_id_samples": multi,
"first_entries": [_entry_facts(e) for e in entries[:6]],
"system_rows": [_entry_facts(e) for e in entries
if str(_safe(lambda: str(e.getSource()))["value"]).startswith("evt:")][:3],
}
result["basic"] = _safe(q_basic)
result["no_kwargs_at_all"] = _safe(
lambda: len([e for e in system.alarm.queryJournal(
startDate=start, endDate=end)]))
result["with_journal_name"] = _safe(
lambda: len([e for e in system.alarm.queryJournal(
journalName="Journal", startDate=start, endDate=end)]))
result["bad_journal_name"] = _safe(
lambda: len([e for e in system.alarm.queryJournal(
journalName="NoSuchJournal", startDate=start, endDate=end)]))
result["long_millis_dates"] = _safe(
lambda: len([e for e in system.alarm.queryJournal(
startDate=system.date.toMillis(start),
endDate=system.date.toMillis(end))]))
result["include_system_false"] = _safe(
lambda: len([e for e in system.alarm.queryJournal(
startDate=start, endDate=end, includeSystemEvents=False)]))
result["include_system_false_alt"] = _safe(
lambda: len([e for e in system.alarm.queryJournal(
startDate=start, endDate=end, isSystem=False)]))
def q_status():
r = system.alarm.queryStatus(state=["ActiveUnacked", "ActiveAcked"])
entries = [e for e in r]
return {"count": len(entries),
"first_entries": [_entry_facts(e) for e in entries[:5]]}
result["status"] = _safe(q_status)
result["shelved"] = _safe(lambda: repr(system.alarm.getShelvedPaths()))
return result
def run():
result = {"probe": PROBE_NAME,
"ranAt": str(system.date.now())}
result["payload"] = _safe(_p0)
_write(result)
system.util.getLogger("probe").info("probe %s written to %s" % (PROBE_NAME, OUT_DIR))

View File

@@ -0,0 +1,12 @@
{
"scope": "A",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"code.py"
],
"attributes": {
"hintScope": 2
}
}

View File

@@ -0,0 +1,2 @@
def handleTimerEvent():
alarmsim.tick()

View File

@@ -0,0 +1,15 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"handleTimerEvent.py"
],
"attributes": {
"sharedThread": false,
"delay": 5000,
"fixedDelay": true,
"enabled": true
}
}

View File

@@ -0,0 +1,2 @@
def handleTimerEvent():
probe.run()

View File

@@ -0,0 +1,15 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"handleTimerEvent.py"
],
"attributes": {
"sharedThread": false,
"delay": 15000,
"fixedDelay": true,
"enabled": false
}
}

View File

@@ -0,0 +1,7 @@
{
"title": "SimHarness",
"description": "Dev-only: alarm simulator + gateway probes + scratch views. NOT part of the Build-a-Thon submission.",
"enabled": true,
"inheritable": false,
"parent": ""
}

297
tools/lint_project.py Normal file
View File

@@ -0,0 +1,297 @@
#!/usr/bin/env python3
"""lint_project.py — deterministic contest/structure linter for the PrimeBAT project.
Checks (stdlib only, exit non-zero on any FAIL):
structure every .json parses; resource.json integrity (declared files exist,
no undeclared payloads, scope correct); no .resources authored
refs embedded view paths + flex-repeater paths resolve to real views
under PrimeControls/; style-class references resolve
bindings binding types from the known set; property-binding paths well-formed;
session.custom.* references resolve against session-props schema;
view.params.* references resolve against the view's declared params;
script transforms/events are tab-indented
contest page-config: exactly one page "/" -> PrimeControls/Dashboard, empty
sharedDocks; all view/style resources under PrimeControls/;
NO tag bindings anywhere in PrimeBAT; string bans (hardcoded local
paths/datasource/simulator/SQL/f-strings/journalName literal);
queryJournal calls bounded by start+end
--strict (Phase 4) additionally: no ignition/* resources besides
script-python/PrimeControls/* and global-props; session custom keys
only PrimeControls; dead view params reported as failures
Usage: python3 tools/lint_project.py [--strict] [--project PrimeBAT]
"""
import argparse
import json
import os
import re
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PERSPECTIVE = "com.inductiveautomation.perspective"
KNOWN_BINDING_TYPES = {"property", "expr", "expression", "tag", "query", "expr-struct", "http"}
EMBED_TYPES = {"ia.display.view", "ia.display.flex-repeater"}
# (regex, message) — scanned over view.json raw text and PrimeControls *.py
STRING_BANS = [
(re.compile(r"\[default\]"), "hardcoded tag provider path '[default]'"),
(re.compile(r"BuildathonSim"), "hardcoded simulator tag path 'BuildathonSim'"),
(re.compile(r"Buildathon_DB"), "hardcoded local datasource 'Buildathon_DB'"),
(re.compile(r"\balarmsim\b"), "reference to dev simulator 'alarmsim'"),
(re.compile(r"journalName\s*="), "hardcoded journalName= (judges' journal name unknown)"),
(re.compile(r"\bsystem\.db\."), "system.db.* SQL access (journal API only)"),
(re.compile(r"\bSELECT\s+", re.IGNORECASE), "raw SQL SELECT"),
(re.compile(r"""(^|[^A-Za-z0-9_])[fF]["']"""), "f-string (Jython 2.7 cannot parse)"),
(re.compile(r"system\.util\.sendRequest\("), "sendRequest dev bridge leftover"),
]
FAILS = []
WARNS = []
def fail(path, msg):
FAILS.append("FAIL %s: %s" % (path, msg))
def warn(path, msg):
WARNS.append("warn %s: %s" % (path, msg))
def rel(p):
return os.path.relpath(p, ROOT)
def load_json(path):
try:
with open(path) as f:
return json.load(f)
except Exception as e:
fail(rel(path), "invalid JSON: %s" % e)
return None
def walk_components(node, visit, path="root"):
"""Depth-first over a view component tree; visit(node, path)."""
if not isinstance(node, dict):
return
visit(node, path)
for i, child in enumerate(node.get("children") or []):
name = "?"
if isinstance(child, dict):
name = (child.get("meta") or {}).get("name", "?")
walk_components(child, visit, "%s/%s[%d]" % (path, name, i))
def iter_prop_configs(view):
"""Yield (owner_path, prop_key, config_dict) for view-level and component-level propConfig."""
for k, v in (view.get("propConfig") or {}).items():
yield ("view", k, v)
collected = []
def visit(node, path):
for k, v in (node.get("propConfig") or {}).items():
collected.append((path, k, v))
walk_components(view.get("root") or {}, visit)
for item in collected:
yield item
def resolve_in_schema(schema, dotted):
"""Walk a dict tree by dotted path; list-typed nodes accept any index; return True if resolvable."""
cur = schema
for part in dotted.split("."):
part = part.split("[")[0]
if isinstance(cur, list):
return True # element access into arrays: shape unknown, accept
if not isinstance(cur, dict) or part not in cur:
return False
cur = cur[part]
return True
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--strict", action="store_true")
ap.add_argument("--project", default="PrimeBAT")
args = ap.parse_args()
proj = os.path.join(ROOT, "ignition", "gateway", "projects", args.project)
if not os.path.isdir(proj):
print("project dir not found: %s" % proj)
sys.exit(2)
views_root = os.path.join(proj, PERSPECTIVE, "views")
styles_root = os.path.join(proj, PERSPECTIVE, "style-classes")
# ---------- collect resource inventory ----------
view_paths = set() # e.g. "PrimeControls/Dashboard"
style_paths = set() # e.g. "PrimeControls/Card"
for base, target, payload in ((views_root, view_paths, "view.json"),
(styles_root, style_paths, "style.json")):
if not os.path.isdir(base):
continue
for dirpath, _dirs, files in os.walk(base):
if payload in files:
target.add(os.path.relpath(dirpath, base).replace(os.sep, "/"))
# ---------- structure: parse every json + resource.json integrity ----------
session_schema = {}
for dirpath, dirs, files in os.walk(proj):
if ".resources" in dirs:
fail(rel(dirpath), "authored .resources directory (gateway-owned)")
dirs.remove(".resources")
for fn in files:
if fn.endswith(".json"):
load_json(os.path.join(dirpath, fn))
if "resource.json" in files:
res = load_json(os.path.join(dirpath, "resource.json"))
if res is None:
continue
declared = res.get("files") or []
payloads = [f for f in files if f != "resource.json"]
for f in declared:
if f not in payloads:
fail(rel(dirpath), "resource.json declares missing file %r" % f)
for f in payloads:
if f not in declared:
fail(rel(dirpath), "undeclared payload file %r" % f)
rp = rel(dirpath)
want_a = ("script-python" in rp) or ("global-props" in rp)
want_scope = "A" if want_a else "G"
if res.get("scope") != want_scope:
fail(rp, "scope %r, expected %r" % (res.get("scope"), want_scope))
# ---------- session props schema ----------
sp_path = os.path.join(proj, PERSPECTIVE, "session-props", "props.json")
sp = load_json(sp_path) if os.path.exists(sp_path) else None
if sp:
session_schema = sp.get("custom") or {}
if args.strict:
for key in session_schema:
if key != "PrimeControls":
fail(rel(sp_path), "session custom key %r outside PrimeControls" % key)
# ---------- page-config contest rules ----------
pc_path = os.path.join(proj, PERSPECTIVE, "page-config", "config.json")
pc = load_json(pc_path) if os.path.exists(pc_path) else None
if pc is not None:
pages = pc.get("pages") or {}
if list(pages.keys()) != ["/"]:
fail(rel(pc_path), "pages must be exactly ['/'], got %s" % list(pages.keys()))
elif (pages["/"] or {}).get("viewPath") != "PrimeControls/Dashboard":
fail(rel(pc_path), "page '/' must map to PrimeControls/Dashboard")
if pc.get("sharedDocks"):
fail(rel(pc_path), "sharedDocks must be empty (docked views prohibited)")
# ---------- namespace rule: everything under PrimeControls/ ----------
for p in sorted(view_paths):
if not p.startswith("PrimeControls/"):
fail("views/%s" % p, "view outside PrimeControls/ folder")
for p in sorted(style_paths):
if not p.startswith("PrimeControls/"):
fail("style-classes/%s" % p, "style class outside PrimeControls/ folder")
# ---------- per-view checks ----------
for vp in sorted(view_paths):
vfile = os.path.join(views_root, vp.replace("/", os.sep), "view.json")
view = load_json(vfile)
if view is None:
continue
rv = rel(vfile)
declared_params = set((view.get("params") or {}).keys())
raw = open(vfile).read()
for rx, msg in STRING_BANS:
if rx.search(raw):
fail(rv, msg)
def visit(node, cpath):
t = node.get("type")
if t in EMBED_TYPES:
path_prop = (node.get("props") or {}).get("path")
bound = "props.path" in (node.get("propConfig") or {})
if isinstance(path_prop, str) and path_prop:
if path_prop not in view_paths:
fail(rv, "%s embeds missing view %r" % (cpath, path_prop))
elif not path_prop.startswith("PrimeControls/"):
fail(rv, "%s embeds non-PrimeControls view %r" % (cpath, path_prop))
elif not bound:
warn(rv, "%s embed with no static path and no binding" % cpath)
classes = ((node.get("props") or {}).get("style") or {}).get("classes")
if isinstance(classes, str):
for cls in classes.split():
if cls not in style_paths:
fail(rv, "%s references missing style class %r" % (cpath, cls))
walk_components(view.get("root") or {}, visit)
for owner, key, cfg in iter_prop_configs(view):
binding = (cfg or {}).get("binding")
if not binding:
continue
btype = binding.get("type")
if btype not in KNOWN_BINDING_TYPES:
fail(rv, "%s %s: unknown binding type %r" % (owner, key, btype))
if btype == "tag":
fail(rv, "%s %s: tag binding (prohibited in PrimeBAT — journal API only)" % (owner, key))
text_blobs = [json.dumps(binding.get("config") or {})]
for tr in binding.get("transforms") or []:
code = tr.get("code", "")
text_blobs.append(code)
if tr.get("type") == "script" and code and not code.startswith(("\t", "\n")):
fail(rv, "%s %s: script transform not tab-indented" % (owner, key))
blob = " ".join(text_blobs)
for m in re.finditer(r"session\.custom\.([A-Za-z0-9_.\[\]]+)", blob):
if not resolve_in_schema(session_schema, m.group(1)):
fail(rv, "%s %s: unresolvable session.custom.%s" % (owner, key, m.group(1)))
for m in re.finditer(r"view\.params\.([A-Za-z0-9_]+)", blob):
if m.group(1) not in declared_params:
fail(rv, "%s %s: undeclared view param %r" % (owner, key, m.group(1)))
if args.strict:
for p in declared_params:
if not re.search(r"(view\.params\.|params\.)%s" % re.escape(p), raw.replace('"%s"' % p, "", 1)):
warn(rv, "param %r possibly unused" % p)
# ---------- scripts: string bans + bounded queries ----------
sp_root = os.path.join(proj, "ignition", "script-python")
if os.path.isdir(sp_root):
for dirpath, _dirs, files in os.walk(sp_root):
for fn in files:
if not fn.endswith(".py"):
continue
fpath = os.path.join(dirpath, fn)
src = open(fpath).read()
for rx, msg in STRING_BANS:
if rx.search(src):
fail(rel(fpath), msg)
for m in re.finditer(r"queryJournal\s*\(", src):
fstart = src.rfind("\ndef ", 0, m.start())
fend = src.find("\ndef ", m.start())
body = src[max(fstart, 0):fend if fend != -1 else len(src)]
if not (re.search(r"start", body) and re.search(r"end", body)):
fail(rel(fpath), "queryJournal call not visibly bounded by start/end")
# ---------- strict: no stray ignition resources ----------
if args.strict:
ig_root = os.path.join(proj, "ignition")
for entry in sorted(os.listdir(ig_root)) if os.path.isdir(ig_root) else []:
if entry not in ("script-python", "global-props"):
fail("ignition/%s" % entry, "unexpected project resource type for submission")
if os.path.isdir(sp_root):
for entry in sorted(os.listdir(sp_root)):
if entry != "PrimeControls":
fail("script-python/%s" % entry, "script package outside PrimeControls")
for w in WARNS:
print(w)
for f in FAILS:
print(f)
print("lint: %d view(s), %d style class(es), %d failure(s), %d warning(s)%s"
% (len(view_paths), len(style_paths), len(FAILS), len(WARNS),
" [strict]" if args.strict else ""))
sys.exit(1 if FAILS else 0)
if __name__ == "__main__":
main()

55
tools/schema_harvest.sh Executable file
View File

@@ -0,0 +1,55 @@
#!/usr/bin/env bash
# schema_harvest.sh — extract Perspective component prop schemas from the running
# gateway's Perspective module into per-component JSON Schema files.
#
# Sources (verified on 8.3.7): perspective-common-<ver>.jar inside
# Perspective-module.modl carries ia.components.json (70 core components),
# perspective-amcharts.components.json (ia.chart.xy/pie/gauge/simple-gauge),
# perspective-timeseries.components.json, plus schemas/*.json (binding,
# transform, style, view-props, session-props schemas).
#
# Output layout:
# <out>/components/<component-id>.schema.json (the component's props JSON Schema)
# <out>/schemas/<name>.json (binding/transform/etc. schemas)
#
# Usage: tools/schema_harvest.sh [output_dir] (default: .schemas/)
set -euo pipefail
OUT="${1:-.schemas}"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
docker cp buildathon-ignition:"/usr/local/bin/ignition/user-lib/modules/Perspective-module.modl" "$TMP/perspective.modl"
python3 - "$TMP" "$OUT" <<'EOF'
import json, os, sys, zipfile
tmp, out = sys.argv[1], sys.argv[2]
comp_dir = os.path.join(out, "components")
misc_dir = os.path.join(out, "schemas")
os.makedirs(comp_dir, exist_ok=True)
os.makedirs(misc_dir, exist_ok=True)
modl = zipfile.ZipFile(os.path.join(tmp, "perspective.modl"))
total = 0
for jar in [n for n in modl.namelist() if n.endswith(".jar")]:
jp = os.path.join(tmp, os.path.basename(jar))
open(jp, "wb").write(modl.read(jar))
try:
z = zipfile.ZipFile(jp)
except zipfile.BadZipFile:
continue
for entry in z.namelist():
if entry.endswith(".components.json") or entry == "ia.components.json":
data = json.loads(z.read(entry))
for comp in data.get("components", []):
cid = comp.get("id")
if not cid:
continue
with open(os.path.join(comp_dir, cid + ".schema.json"), "w") as f:
json.dump(comp.get("schema", {}), f, indent=1)
total += 1
elif entry.startswith("schemas/") and entry.endswith(".json"):
with open(os.path.join(misc_dir, os.path.basename(entry)), "wb") as f:
f.write(z.read(entry))
print("extracted %d component schemas -> %s" % (total, comp_dir))
EOF
ls "$OUT/components" | wc -l