devagent-ignition 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. devagent_hmi.py +471 -0
  2. devagent_ignition-0.1.0.dist-info/METADATA +204 -0
  3. devagent_ignition-0.1.0.dist-info/RECORD +133 -0
  4. devagent_ignition-0.1.0.dist-info/WHEEL +4 -0
  5. devagent_ignition-0.1.0.dist-info/entry_points.txt +2 -0
  6. devagent_spatial/__init__.py +1 -0
  7. devagent_spatial/_resources/README.md +187 -0
  8. devagent_spatial/_resources/RELEASE_MANIFEST.json +312 -0
  9. devagent_spatial/_resources/SHA256SUMS.txt +270 -0
  10. devagent_spatial/_resources/SOURCE_PROVENANCE.json +186 -0
  11. devagent_spatial/_resources/assets/agent.js +102 -0
  12. devagent_spatial/_resources/assets/creator.css +247 -0
  13. devagent_spatial/_resources/assets/creator_2d.mjs +16 -0
  14. devagent_spatial/_resources/assets/creator_original_r9.mjs +58 -0
  15. devagent_spatial/_resources/assets/creator_original_v8.mjs +120 -0
  16. devagent_spatial/_resources/assets/creator_r9_diagnostics.mjs +53 -0
  17. devagent_spatial/_resources/assets/creator_scene.mjs +128 -0
  18. devagent_spatial/_resources/assets/creator_ui.mjs +60 -0
  19. devagent_spatial/_resources/assets/customer_documents.js +110 -0
  20. devagent_spatial/_resources/assets/equipment_controls.mjs +27 -0
  21. devagent_spatial/_resources/assets/facility2d.css +24 -0
  22. devagent_spatial/_resources/assets/facility2d_app.mjs +45 -0
  23. devagent_spatial/_resources/assets/facility2d_core.mjs +114 -0
  24. devagent_spatial/_resources/assets/facility2d_simulation.mjs +167 -0
  25. devagent_spatial/_resources/assets/facility2d_viewer.html +7 -0
  26. devagent_spatial/_resources/assets/fallback_scene.js +17 -0
  27. devagent_spatial/_resources/assets/flow_geometry.mjs +218 -0
  28. devagent_spatial/_resources/assets/harness.js +78 -0
  29. devagent_spatial/_resources/assets/live_design_review.js +129 -0
  30. devagent_spatial/_resources/assets/operations_ui.js +305 -0
  31. devagent_spatial/_resources/assets/operator_viewer.html +64 -0
  32. devagent_spatial/_resources/assets/project_package.js +52 -0
  33. devagent_spatial/_resources/assets/r9_transport.mjs +70 -0
  34. devagent_spatial/_resources/assets/runtime_feed.js +30 -0
  35. devagent_spatial/_resources/assets/studio.html +139 -0
  36. devagent_spatial/_resources/assets/style.css +191 -0
  37. devagent_spatial/_resources/assets/test_workspace.html +195 -0
  38. devagent_spatial/_resources/assets/three_scene.js +442 -0
  39. devagent_spatial/_resources/assets/unified_engineer.js +101 -0
  40. devagent_spatial/_resources/assets/v076_fat_ui.js +40 -0
  41. devagent_spatial/_resources/assets/v078_command_ui.js +44 -0
  42. devagent_spatial/_resources/assets/viewer.html +45 -0
  43. devagent_spatial/_resources/assets/viewer.js +112 -0
  44. devagent_spatial/_resources/design-system.json +130 -0
  45. devagent_spatial/_resources/examples/CV101-command-contract-PENDING.json +30 -0
  46. devagent_spatial/_resources/examples/cv101_guided.json +272 -0
  47. devagent_spatial/_resources/examples/make_examples.py +36 -0
  48. devagent_spatial/_resources/examples/single_floor.json +444 -0
  49. devagent_spatial/_resources/examples/v0771_training_wizard.json +11 -0
  50. devagent_spatial/_resources/examples/warehouse-3floor-standard-2d.json +708 -0
  51. devagent_spatial/_resources/examples/warehouse-operations-synthetic.json +1186 -0
  52. devagent_spatial/_resources/examples/warehouse-v07-lab-bindings-PENDING.json +372 -0
  53. devagent_spatial/_resources/examples/warehouse.json +708 -0
  54. devagent_spatial/_resources/examples/water.json +288 -0
  55. devagent_spatial/_resources/reference/r9-golden/REFERENCE_SHA256.json +13 -0
  56. devagent_spatial/_resources/reference/r9-golden/web/app.mjs +745 -0
  57. devagent_spatial/_resources/reference/r9-golden/web/index.html +213 -0
  58. devagent_spatial/_resources/reference/r9-golden/web/lab_panel.mjs +53 -0
  59. devagent_spatial/_resources/reference/r9-golden/web/model.mjs +367 -0
  60. devagent_spatial/_resources/reference/r9-golden/web/state_adapter.mjs +78 -0
  61. devagent_spatial/_resources/reference/r9-golden/web/styles.css +76 -0
  62. devagent_spatial/_resources/reference/r9-original/README_R9.md +27 -0
  63. devagent_spatial/_resources/reference/r9-original/SHA256SUMS.txt +63 -0
  64. devagent_spatial/_resources/reference/r9-original/SNAPSHOT_SHA256SUMS.txt +16 -0
  65. devagent_spatial/_resources/reference/r9-original/flow_routes.json +527 -0
  66. devagent_spatial/_resources/reference/r9-original/flow_sim.py +117 -0
  67. devagent_spatial/_resources/reference/r9-original/lab_control.py +73 -0
  68. devagent_spatial/_resources/reference/r9-original/web/app.mjs +745 -0
  69. devagent_spatial/_resources/reference/r9-original/web/index.html +213 -0
  70. devagent_spatial/_resources/reference/r9-original/web/lab_panel.mjs +53 -0
  71. devagent_spatial/_resources/reference/r9-original/web/model.mjs +367 -0
  72. devagent_spatial/_resources/reference/r9-original/web/state_adapter.mjs +78 -0
  73. devagent_spatial/_resources/reference/r9-original/web/styles.css +76 -0
  74. devagent_spatial/_resources/reference/r9-original/web/vendor/LICENSE-three.txt +21 -0
  75. devagent_spatial/_resources/reference/r9-original/web/vendor/SHA256SUMS.txt +4 -0
  76. devagent_spatial/_resources/reference/r9-original/web/vendor/addons/controls/OrbitControls.js +1860 -0
  77. devagent_spatial/_resources/reference/r9-original/web/vendor/three.core.js +58773 -0
  78. devagent_spatial/_resources/reference/r9-original/web/vendor/three.module.js +18251 -0
  79. devagent_spatial/_resources/reference/v8-original/README.md +36 -0
  80. devagent_spatial/_resources/reference/v8-original/SHA256SUMS.txt +12 -0
  81. devagent_spatial/_resources/reference/v8-original/model.py +124 -0
  82. devagent_spatial/_resources/reference/v8-original/run_demo.sh +4 -0
  83. devagent_spatial/_resources/reference/v8-original/server.py +100 -0
  84. devagent_spatial/_resources/reference/v8-original/tests/browser_smoke.py +50 -0
  85. devagent_spatial/_resources/reference/v8-original/tests/test_v8.py +144 -0
  86. devagent_spatial/_resources/reference/v8-original/topology.json +24 -0
  87. devagent_spatial/_resources/reference/v8-original/web/app.js +114 -0
  88. devagent_spatial/_resources/reference/v8-original/web/index.html +22 -0
  89. devagent_spatial/_resources/reference/v8-original/web/simulator.html +1 -0
  90. devagent_spatial/_resources/reference/v8-original/web/simulator.js +6 -0
  91. devagent_spatial/_resources/reference/v8-original/web/style.css +16 -0
  92. devagent_spatial/_resources/sample_customer_inputs/Plant_Equipment_Register.csv +4 -0
  93. devagent_spatial/_resources/sample_customer_inputs/Plant_Flow_Requirements.txt +5 -0
  94. devagent_spatial/_resources/sample_customer_inputs/Raw_PLC_Tag_Export.csv +4 -0
  95. devagent_spatial/_resources/templates/warehouse_connections.csv +12 -0
  96. devagent_spatial/_resources/templates/warehouse_equipment.csv +21 -0
  97. devagent_spatial/_resources/templates/warehouse_signals.csv +34 -0
  98. devagent_spatial/_resources/templates/water_connections.csv +7 -0
  99. devagent_spatial/_resources/templates/water_equipment.csv +10 -0
  100. devagent_spatial/_resources/templates/water_signals.csv +12 -0
  101. devagent_spatial/agent_harness.py +299 -0
  102. devagent_spatial/agent_runtime.py +314 -0
  103. devagent_spatial/contract_opc_fat.py +216 -0
  104. devagent_spatial/contract_opc_source.py +265 -0
  105. devagent_spatial/control_intents.py +38 -0
  106. devagent_spatial/customer_intake.py +235 -0
  107. devagent_spatial/engineering_expertise.py +114 -0
  108. devagent_spatial/generator.py +201 -0
  109. devagent_spatial/ignition_native.py +234 -0
  110. devagent_spatial/ignition_project.py +144 -0
  111. devagent_spatial/live_lab_binding.py +128 -0
  112. devagent_spatial/model.py +297 -0
  113. devagent_spatial/native_reference.py +69 -0
  114. devagent_spatial/opc_snapshot.py +87 -0
  115. devagent_spatial/operations.py +284 -0
  116. devagent_spatial/operations_capture.py +47 -0
  117. devagent_spatial/operations_lab.py +64 -0
  118. devagent_spatial/original_r9.py +169 -0
  119. devagent_spatial/original_v8.py +81 -0
  120. devagent_spatial/preflight.py +140 -0
  121. devagent_spatial/preview_simulation.py +189 -0
  122. devagent_spatial/project_package.py +101 -0
  123. devagent_spatial/qualification.py +152 -0
  124. devagent_spatial/strict_json.py +29 -0
  125. devagent_spatial/studio.py +894 -0
  126. devagent_spatial/studio_workflow.py +277 -0
  127. devagent_spatial/test_reporting.py +87 -0
  128. devagent_spatial/v076_opc_fat.py +180 -0
  129. devagent_spatial/v078_commands.py +277 -0
  130. devagent_spatial/v078_native_commands.py +161 -0
  131. devagent_spatial/v07_fat.py +110 -0
  132. devagent_spatial/v07_native.py +318 -0
  133. devagent_spatial/v083_native.py +349 -0
@@ -0,0 +1,78 @@
1
+ /* Explicit, bounded BYOM workflow; never send project information without a click. */
2
+ (()=>{'use strict';
3
+ const $=id=>document.getElementById(id);
4
+ const fields=['name','industry','layout','palette','description','equipment_csv','connections_csv','signals_csv','requirements'];
5
+ const snapshot=()=>Object.fromEntries(fields.map(f=>[f,$(f).value]));
6
+ const reviewFingerprint=()=>JSON.stringify({wizard:snapshot(),aiRequest:$('ai-request').value.trim()});
7
+ let reviewedInput=null;
8
+ const canBuild=()=>window.DevAgentAIFlow?.isConnected()&&reviewedInput===reviewFingerprint();
9
+ window.DevAgentHarness={canBuild};
10
+ function resetReview(){reviewedInput=null;$('build').disabled=true;}
11
+ for(const field of [...fields,'ai-request']){$(field).addEventListener('input',resetReview);$(field).addEventListener('change',resetReview);}
12
+ document.addEventListener('devagent:ai-connection',resetReview);
13
+ document.addEventListener('devagent:project-reset',()=>{
14
+ resetReview();proposed=null;before=null;approval.classList.add('hidden');result.classList.add('hidden');
15
+ });
16
+
17
+ const result=$('harness-results');const approval=$('harness-approval');
18
+ let proposed=null, before=null;
19
+ const show=t=>{result.classList.remove('hidden');result.textContent=t;};
20
+ const currentConn=()=>{const model=$('ai-model').value.trim();if(!model)return null;
21
+ return {provider:$('ai-provider').value,model,api_key:$('ai-key').value,endpoint:$('ai-endpoint').value.trim()};};
22
+ const render=(data)=>{
23
+ const inspection=data.engineering||{};
24
+ const warning=(inspection.findings||[]).slice(0,9).map(f=>{
25
+ const label=f.asset_id?` · ${f.asset_id}`:'';
26
+ return `• ${f.category}${label}${f.roles?.length?` — ${f.roles.join(', ')}`:''}\n ${f.next_action||''}`;
27
+ });
28
+ const tests=(data.attempts||[]).map(x=>`Attempt ${x.attempt}: ${x.result} · ${x.rank?.score??x.failure_code??x.reason??'review'}`);
29
+ const summary=[`Engineering: ${inspection.status||'NOT_ASSESSED'} · ${inspection.equipment_count??0} equipment · ${inspection.signal_count??0} signals`,
30
+ `AI: ${data.mode==='NO_AI_MODEL_CONNECTED'?'Not called — independent engine only':'Model proposed / deterministic checks only'}`,
31
+ `OPC / Ignition / FAT: NOT VERIFIED · Production: HOLD`,
32
+ ...(warning.length?['\nEngineering findings:',...warning]:['\nNo catalog gaps returned; this does not certify mapping.']),
33
+ ...(tests.length?['\nBounded harness:',...tests]:[]),
34
+ data.winner?'\nPreferred draft available for YOUR review. Engineering accuracy and visual quality are not certified.':''];
35
+ show(summary.join('\n'));
36
+ };
37
+ async function run(withAI){
38
+ if(!window.DevAgentAIFlow?.requireConnection())return;
39
+ resetReview();
40
+ for(const id of ['harness-review','harness-optimize'])$(id).disabled=true;
41
+ proposed=null;approval.classList.add('hidden');
42
+ before=snapshot();
43
+ const connection=withAI?currentConn():null;
44
+ if(withAI&&!connection){show('MODEL NOT CONNECTED. Click “Connect model ⚙” to set up your own AI provider. No data sent.');
45
+ $('harness-configure').focus();for(const id of ['harness-review','harness-optimize'])$(id).disabled=false;return;}
46
+ show(withAI?'Asking your model for up to 2 proposals and independently validating them…':'Reviewing supplied equipment, mapping and requirements locally…');
47
+ const controller=new AbortController();const timeout=setTimeout(()=>controller.abort(),135000);
48
+ try{
49
+ const rsp=await fetch('/api/agent/harness',{method:'POST',headers:{'Content-Type':'application/json','X-DevAgent-Request':'agent-harness-v1'},
50
+ body:JSON.stringify({wizard:before,connection,request:$('ai-request').value.trim(),max_attempts:withAI?2:1}),signal:controller.signal});
51
+ const obj=await rsp.json();if(!rsp.ok){show(`HOLD ${obj.reason||obj.status}. No project changed.`);return;}
52
+ render(obj);
53
+ if(withAI && obj.winner?.fields && obj.status==='DRAFT_REQUIRES_HUMAN_REVIEW'){
54
+ // A proposed draft must be explicitly reviewed/applied before generation.
55
+ show(result.textContent+'\n\nReview and apply the selected draft to unlock the final validation/build step.');
56
+ }
57
+ if(obj.winner?.fields&&obj.status==='DRAFT_REQUIRES_HUMAN_REVIEW'){
58
+ proposed=obj.winner.fields;approval.classList.remove('hidden');}
59
+ }catch(e){show('HOLD: Agent timed out or could not reach the local server. No project was changed.');}
60
+ finally{clearTimeout(timeout);for(const id of ['harness-review','harness-optimize'])$(id).disabled=false;}
61
+ }
62
+ $('harness-review').addEventListener('click',()=>run(false));
63
+ $('harness-optimize').addEventListener('click',()=>run(true));
64
+ $('harness-configure').addEventListener('click',()=>window.DevAgentAIFlow.openConnection());
65
+ $('start-ai-review').addEventListener('click',()=>{ $('harness-optimize').click();$('harness-results').scrollIntoView({behavior:'smooth',block:'center'}); });
66
+ $('harness-apply').addEventListener('click',()=>{
67
+ if(!proposed)return;
68
+ if(JSON.stringify(snapshot())!==JSON.stringify(before)){
69
+ approval.classList.add('hidden');proposed=null;
70
+ show('HOLD: Project inputs changed since review. Run the harness again before applying a draft.');return;
71
+ }
72
+ for(const k of fields){if(typeof proposed[k]==='string'){
73
+ $(k).value=proposed[k];$(k).dispatchEvent(new Event('input',{bubbles:true}));}}
74
+ proposed=null;approval.classList.add('hidden');
75
+ reviewedInput=reviewFingerprint();$('build').disabled=false;
76
+ show('Reviewed candidate applied. You may now run VALIDATE, then GENERATE PREVIEW. Any input edit requires re-review. PLC writes and deployment remain blocked.');
77
+ });
78
+ })();
@@ -0,0 +1,129 @@
1
+ /* V0.7.9 — bounded, review-only conversational visual refinement.
2
+ * This never calls the PLC, OPC UA, Gateway, or modifies the verified project.
3
+ * No arbitrary CSS/HTML/JS supplied by the user is evaluated or interpolated.
4
+ * Open-ended AI layout revisions still require a separate verified generation pass.
5
+ */
6
+ (()=>{'use strict';
7
+ const el=id=>document.getElementById(id);
8
+ const panel=el('design-review'), frame=el('live-hmi-frame'), stage=el('live-hmi-panel');
9
+ if(!panel||!frame||!stage)return;
10
+ const log=el('review-chat-log'), input=el('review-chat-input');
11
+ const BASE=Object.freeze({text:'normal',map:'normal',inspector:'normal',kpi:'normal',contrast:'normal',inspectorSide:'right'});
12
+ let state={...BASE}, history=[], projectId=null, reviewEvents=[];
13
+ const full=el('full-hmi-review'), fullSlot=el('full-hmi-viewer-slot'), chatSlot=el('full-hmi-editor-slot');
14
+ const homeStage=stage.parentNode, homeChat=panel.parentNode;
15
+ let expanded=false, previousFocus=null;
16
+ const cameraResized=()=>{try{frame.contentWindow?.dispatchEvent(new Event('resize'));}catch(_e){}};
17
+ function expand(){
18
+ if(!projectId||expanded)return;
19
+ previousFocus=document.activeElement;fullSlot.append(stage);chatSlot.append(panel);
20
+ full.classList.remove('hidden');document.body.classList.add('full-hmi-review-open');expanded=true;
21
+ el('full-review-new-tab').href=frame.src;el('full-review-close').focus();
22
+ setTimeout(cameraResized,100);
23
+ }
24
+ function collapse(){
25
+ if(!expanded)return;homeStage.append(stage);homeChat.append(panel);
26
+ full.classList.add('hidden');document.body.classList.remove('full-hmi-review-open');expanded=false;
27
+ if(previousFocus?.isConnected)previousFocus.focus();setTimeout(cameraResized,100);
28
+ }
29
+
30
+ const reply=(body,who='assistant')=>{const div=document.createElement('div');div.className='review-msg '+who;div.textContent=body;log.append(div);if(log.children.length>15)log.firstElementChild.remove();log.scrollTop=log.scrollHeight;};
31
+ const norm=s=>s.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/đ/g,'d').trim();
32
+ function parse(prompt){
33
+ const s=norm(prompt);let next={...state};let matched=false;
34
+ // A request containing both styling and engineering changes is NOT partially applied.
35
+ // Never interpret an unsafe mixed request as a harmless CSS edit.
36
+ if(/\b(plc|opc|nodeid|tags?|safety|interlocks?|permissives?|emergency|alarms?|faults?|motors?|belts?|speeds?|start|stop|commands?|sensors?|topology|wiring|control logic|sorters?|routing|write|setpoint)\b/.test(s))return null;
37
+ // These commands change presentation only. They never create PLC values or alter signal semantics.
38
+ if(/\b(reset design|reset style|default layout|reset all|mac dinh|khoi phuc giao dien)\b/.test(s))return {...BASE};
39
+ if(/(text|font|chu|typograph)/.test(s)){
40
+ if(/(larger|bigger|increase|too small|to hon|lon hon|tang|phong to)/.test(s)){next.text='large';matched=true;}
41
+ else if(/(smaller|decrease|nho hon|giam)/.test(s)){next.text='small';matched=true;}
42
+ else if(/(normal|default|mac dinh)/.test(s)){next.text='normal';matched=true;}
43
+ }
44
+ if(/(map|overview|viewer|scene|canvas|so do|ban do|3d)/.test(s)){
45
+ if(/(larger|bigger|more space|increase|to hon|rong hon|lon hon|phong to)/.test(s)){next.map='large';matched=true;}
46
+ else if(/(smaller|less space|nho hon|giam)/.test(s)){next.map='compact';matched=true;}
47
+ else if(/(normal|default|mac dinh)/.test(s)){next.map='normal';matched=true;}
48
+ }
49
+ if(/(inspector|detail panel|parameter panel|thong so|bang chi tiet|panel)/.test(s)){
50
+ if(/(wider|larger|bigger|more space|rong hon|to hon)/.test(s)){next.inspector='wide';matched=true;}
51
+ else if(/(narrow|smaller|compact|gon hon|hep hon)/.test(s)){next.inspector='narrow';matched=true;}
52
+ else if(/(normal|default|mac dinh)/.test(s)){next.inspector='normal';matched=true;}
53
+ }
54
+ if(/(inspector|detail panel|parameter panel|bang chi tiet)/.test(s) && /(left|right|ben trai|ben phai)/.test(s)){next.inspectorSide=/(left|ben trai)/.test(s)?'left':'right';matched=true;}
55
+ if(/(kpi|metrics|dashboard card|the thong ke)/.test(s)){
56
+ if(/(compact|smaller|less space|gon hon|thu gon)/.test(s)){next.kpi='compact';matched=true;}
57
+ else if(/(larger|more space|roomy|rong hon|to hon)/.test(s)){next.kpi='spacious';matched=true;}
58
+ else if(/(normal|default|mac dinh)/.test(s)){next.kpi='normal';matched=true;}
59
+ }
60
+ if(/(contrast|readability|de doc|tuong phan)/.test(s)){
61
+ if(/(high|more|increase|higher|tang|cao hon)/.test(s)){next.contrast='high';matched=true;}
62
+ else if(/(normal|default|mac dinh)/.test(s)){next.contrast='normal';matched=true;}
63
+ }
64
+ return matched ? next : null;
65
+ }
66
+ function cssFor(s){
67
+ let out='/* Review-only presentation, not a production control change. */\n';
68
+ if(s.text==='large')out+='body{zoom:1.09!important}\n';
69
+ if(s.text==='small')out+='body{zoom:.94!important}\n';
70
+ if(s.map==='large')out+='.stage,.map-canvas{height:clamp(650px,74vh,920px)!important;min-height:650px!important}\n';
71
+ if(s.map==='compact')out+='.stage,.map-canvas{height:420px!important;min-height:420px!important}\n';
72
+ if(s.inspector==='wide')out+='.workspace{grid-template-columns:minmax(0,1fr) 380px!important}.layout{grid-template-columns:minmax(0,1fr) 390px!important}\n';
73
+ if(s.inspector==='narrow')out+='.workspace{grid-template-columns:minmax(0,1fr) 244px!important}.layout{grid-template-columns:minmax(0,1fr) 245px!important}\n';
74
+ if(s.inspectorSide==='left')out+='.workspace>.inspector{grid-column:1!important;grid-row:1!important}.workspace>.viewer-panel{grid-column:2!important;grid-row:1!important}.workspace{grid-template-columns:330px minmax(0,1fr)!important}.layout>.inspector{order:-1!important}.layout{grid-template-columns:330px minmax(0,1fr)!important}\n';
75
+ if(s.kpi==='compact')out+='.metrics,.ops-metrics{gap:5px!important}.metric,.ops-metric{min-height:84px!important;padding:9px!important}\n';
76
+ if(s.kpi==='spacious')out+='.metrics,.ops-metrics{gap:12px!important}.metric,.ops-metric{min-height:120px!important;padding:18px!important}\n';
77
+ if(s.contrast==='high')out+=':root{--muted:#bed7e7!important;--edge:#578199!important;--border:#578199!important}.panel,.inspector,.metric,.ops-metric{border-color:#578199!important}\n';
78
+ return out;
79
+ }
80
+ function apply(){
81
+ const doc=frame.contentDocument;if(!doc || !doc.head)return false;
82
+ let st=doc.getElementById('devagent-review-visual-only');
83
+ if(!st){st=doc.createElement('style');st.id='devagent-review-visual-only';doc.head.appendChild(st);}
84
+ st.textContent=cssFor(state);cameraResized();
85
+ return true;
86
+ }
87
+ function updateUndo(){el('review-undo').disabled=history.length===0;}
88
+ function update(next,prompt,kind='visual'){history.push({...state});state=next;updateUndo();apply();reviewEvents.push({request:prompt,kind,design_tokens:{...state}});reply('Preview updated. This is a visual draft only; the generated HMI remains unchanged.');}
89
+ function activate(pid){
90
+ if(!/^[a-z0-9-]{1,100}$/.test(pid))return;
91
+ projectId=pid;state={...BASE};history=[];reviewEvents=[];updateUndo();
92
+ frame.src='/projects/'+pid+'/viewer/index.html';
93
+ panel.classList.remove('hidden');stage.classList.remove('hidden');
94
+ frame.addEventListener('load',()=>{if(frame.contentWindow && frame.contentDocument)apply();}, {once:true});
95
+ el('stage-eyebrow').textContent='GENERATED HMI / COMPLETE SCREEN';
96
+ el('stage-heading').textContent='Full HMI review';
97
+ el('stage-mode').textContent='CONCEPT';
98
+ reply('Your complete generated HMI is open. Select equipment and review the full screen, or request a visual adjustment.');
99
+ expand();
100
+ }
101
+ document.addEventListener('devagent:project-built',ev=>activate(ev.detail?.project_id||''));
102
+ document.addEventListener('devagent:project-reset',()=>{
103
+ collapse();projectId=null;state={...BASE};history=[];reviewEvents=[];updateUndo();
104
+ frame.removeAttribute('src');stage.classList.add('hidden');panel.classList.add('hidden');
105
+ log.replaceChildren();reply('Generate an AI-reviewed HMI to begin visual review.');
106
+ });
107
+ el('review-chat-form').addEventListener('submit',e=>{
108
+ e.preventDefault();if(!projectId){reply('Generate an AI-reviewed HMI first.');return;}
109
+ const text=input.value.trim();if(!text)return;
110
+ reply(text,'user');input.value='';
111
+ const next=parse(text);
112
+ if(!next){reply('I can preview visual edits such as: “larger text”, “bigger map”, “wider inspector”, “inspector left”, “compact KPI”, or “higher contrast”. Changes to PLC tags, alarms, sorter logic, machine geometry or commands require engineering review and a new validated generation.');return;}
113
+ update(next,text);
114
+ });
115
+ el('open-full-hmi-review').addEventListener('click',expand);
116
+ el('full-review-close').addEventListener('click',collapse);
117
+ document.addEventListener('keydown',e=>{if(e.key==='Escape'&&expanded)collapse();});
118
+ el('review-undo').addEventListener('click',()=>{if(!history.length)return;state=history.pop();updateUndo();apply();reply('Previous visual setting restored.');});
119
+ el('review-reset').addEventListener('click',()=>{history.push({...state});state={...BASE};updateUndo();apply();reply('Original preview appearance restored.');});
120
+ el('review-export').addEventListener('click',()=>{
121
+ if(!projectId){reply('No project ready to export.');return;}
122
+ const recipe={schema:'devagent.review.visual_recipe.v1',project_id:projectId,scope:'CONCEPT_PREVIEW_ONLY',does_not_modify:['PLC','OPC_UA','CONTROL','ALARM_SEMANTICS','IGNITION_PRODUCTION'],design_tokens:state,requests:reviewEvents};
123
+ const blob=new Blob([JSON.stringify(recipe,null,2)+'\n'],{type:'application/json'}),url=URL.createObjectURL(blob);
124
+ const a=document.createElement('a');a.href=url;a.download='DevAgent-'+projectId+'-VISUAL-REVIEW-ONLY.json';document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),5000);
125
+ reply('Downloaded review notes. To persist these changes in Ignition, regenerate and run the separate engineering, visual and Gateway FAT gates.');
126
+ });
127
+ // Test hooks are read-only and never expose credentials or raw customer data.
128
+ window.DevAgentDesignReview={parse:(s)=>parse(s),getState:()=>({...state}),preview:()=>Boolean(projectId),open:expand,close:collapse,isExpanded:()=>expanded};
129
+ })();
@@ -0,0 +1,305 @@
1
+ /* DevAgent V0.5.6: safe read-only LAB FAT replay of externally produced receipt.
2
+ Never labels replay data as verified production OPC UA or Ignition telemetry.
3
+ Dynamic KPI values are only rendered after contract-digest matching. */
4
+ export function installOperations({model,selectAsset,scene,policy}) {
5
+ const $=id=>document.getElementById(id);
6
+ const ids={equipment_running:'running',active_faults:'faults',photoeyes_blocked:'blocked',
7
+ no_reads:'noread',total_scans:'scans',
8
+ parcels_in_transit:'transit', delivered:'delivered',last_scan_result:'scanresult',last_barcode:'barcode',
9
+ signal_health:'quality'};
10
+ let receipt=null,frameNumber=0,labLive=false,syntheticMode=false,virtualPE=new Map(),simAcknowledged=false,simFaultAsset=null;
11
+ for(const [key,definition] of Object.entries(policy.metrics)){
12
+ if(!definition.supported && ids[key]){
13
+ const kpi=$('kpi-'+ids[key]);if(kpi?.closest)kpi.closest('.ops-metric').hidden=true;
14
+ }
15
+ }
16
+ const alertDialog=$('alerts-dialog');
17
+ const selectEquipment=id=>{
18
+ if(!model.equipment.some(e=>e.id===id))return;
19
+ selectAsset(id);
20
+ if(alertDialog.open)alertDialog.close();
21
+ };
22
+ const button=$('alerts-toggle');
23
+ const open=()=>{renderAlerts();alertDialog.showModal();};
24
+ button.addEventListener('click',open);
25
+ $('alerts-close').addEventListener('click',()=>alertDialog.close());
26
+ alertDialog.addEventListener('click',e=>{if(e.target===alertDialog)alertDialog.close();});
27
+ const set=(id,value)=>{const node=$(id);if(node)node.textContent=String(value);};
28
+ function renderMetric(key,metric) {
29
+ const suffix=ids[key];
30
+ const good=metric && (metric.status==='LAB_REPLAY');
31
+ const number=good && (typeof metric.value==='number'||typeof metric.value==='string') ? metric.value : null;
32
+ set('kpi-'+suffix,number===null?'N/A':number);
33
+ let explain='NOT CONFIGURED';
34
+ if(metric?.status==='SOURCE_UNAVAILABLE')explain=`${metric.good??0}/${metric.sources} good sources`;
35
+ if(metric?.status==='LAB_REPLAY')explain=`${metric.sources} sources · ${labLive?(syntheticMode?'SYNTHETIC DEMO':'STUDIO LAB'):'SIMULATED'}`;
36
+ set('kpi-'+suffix+'-note',explain);
37
+ const card=$('kpi-'+suffix).closest('.ops-metric');
38
+ card.classList.toggle('critical',key==='active_faults'&&typeof number==='number'&&number>0);
39
+ }
40
+ function current(){return receipt?.frames?.[labLive?receipt.frames.length-1:frameNumber]??null;}
41
+ // Visual bell states reflect validated replay only; unknown is NOT zero faults.
42
+ function updateBell(state,count=0){
43
+ const badge=$('alert-count');
44
+ const labels={unknown:'Notifications — source not verified; alarm state unknown',
45
+ alarm:`Notifications — ${count} ${labLive?'Studio lab':'simulated'} alarms; not production`,
46
+ warning:'Notifications — lab data quality warnings; not production',
47
+ 'replay-clear':'Notifications — no alarms in lab source; not production'};
48
+ button.classList.remove('is-unknown','has-alarm','has-warning','is-replay-clear');
49
+ badge.classList.remove('is-unknown','has-alarm','has-warning');
50
+ button.classList.add(state==='alarm'?'has-alarm':state==='warning'?'has-warning':state==='unknown'?'is-unknown':'is-replay-clear');
51
+ if(state==='replay-clear'){
52
+ badge.textContent='';badge.hidden=true;
53
+ } else {
54
+ badge.hidden=false;
55
+ badge.textContent=state==='unknown'?'?':state==='warning'?'!':String(count);
56
+ badge.classList.add(state==='alarm'?'has-alarm':state==='warning'?'has-warning':'is-unknown');
57
+ }
58
+ button.setAttribute('aria-label',labels[state]);
59
+ button.title=labels[state];
60
+ }
61
+ function render() {
62
+ const frame=current();
63
+ if(!frame){
64
+ set('ops-mode','DATA UNAVAILABLE');
65
+ updateBell('unknown');
66
+ return;
67
+ }
68
+ for(const key of Object.keys(ids)){
69
+ if(key==='signal_health'){
70
+ const good=frame.metrics.signal_health;
71
+ set('kpi-quality',`${good.value}/${good.total}`);
72
+ set('kpi-quality-note',labLive?'GOOD SIGNALS · SIMULATION':'GOOD & FRESH IN REPLAY');
73
+ }else renderMetric(key,frame.metrics[key]);
74
+ }
75
+ const active=frame.active_alarms.length;
76
+ scene()?.alarms?.(frame.active_alarms);
77
+ scene()?.operatingStates?.(frame.signals);
78
+ set('ops-mode',syntheticMode?'SYNTHETIC DIGITAL TWIN · NOT OPC':labLive?'SIMULATION · LAB SOURCE':'SIMULATED FAT REPLAY · NOT LIVE');
79
+ set('source-indicator',syntheticMode?'SYNTHETIC DATA · NOT OPC':labLive?'SIMULATION · LAB SOURCE':'SOURCE UNAVAILABLE');
80
+ const unavailable=frame.quality_issues.length;
81
+ updateBell(active>0?'alarm':unavailable>0?'warning':'replay-clear',active);
82
+ set('ops-import-status',labLive?'External Studio lab feed ACTIVE (read only, not Ignition or physical PLC verified)':`Loaded ${receipt.origin}: frame ${frameNumber+1}/${receipt.frames.length} at ${frame.at} — NOT live OPC UA or actual Ignition FAT`);
83
+ if($('ops-previous'))$('ops-previous').disabled=labLive||frameNumber===0;
84
+ if($('ops-next'))$('ops-next').disabled=labLive||frameNumber===receipt.frames.length-1;
85
+ const selected=$('selected-id').textContent;
86
+ const states=frame.signals.filter(x=>x.asset_id===selected);
87
+ if(states.length){
88
+ const quality=states.every(x=>x.valid)?(syntheticMode?'GOOD · SYNTHETIC':'GOOD · STUDIO LAB'):(syntheticMode?'UNKNOWN · SYNTHETIC':'UNKNOWN / BAD · STUDIO LAB');
89
+ const row=[...$('asset-meta').children].find(x=>x.firstElementChild?.textContent==='DATA QUALITY');
90
+ if(row)row.lastElementChild.textContent=quality;
91
+ const statusRow=[...$('asset-meta').children].find(x=>x.firstElementChild?.textContent==='STATUS');
92
+ if(statusRow){
93
+ let status='UNKNOWN';
94
+ if(states.every(x=>x.valid)){
95
+ const fault=states.find(x=>x.role==='fault');
96
+ const run=states.find(x=>x.role==='run_feedback');
97
+ const photoeye=states.find(x=>x.role==='photoeye_blocked');
98
+ if(fault?.active===true)status='FAULT';
99
+ else if(run)status=run.value===true?'RUNNING':'STOPPED';
100
+ else if(photoeye)status=photoeye.active===true?'BLOCKED':'CLEAR';
101
+ else if(states.find(x=>x.role==='valve_open'))status=states.find(x=>x.role==='valve_open').value===true?'OPEN':'CLOSED';
102
+ else if(states.find(x=>x.role==='status'))status=String(states.find(x=>x.role==='status').value).slice(0,40);
103
+ }
104
+ statusRow.lastElementChild.textContent=status;
105
+ }
106
+ if($('selected-signals'))for(const node of $('selected-signals').children){
107
+ const signalRole=node.textContent.split(' (')[0];
108
+ const state=states.find(x=>x.role===signalRole);
109
+ const small=node.querySelector('small');
110
+ if(small&&state)small.textContent=state.valid?`${syntheticMode?'SYNTHETIC DEMO':labLive?'STUDIO LAB':'SIMULATED REPLAY'}: ${String(state.value).slice(0,80)}`:`${state.quality} · NOT VERIFIED`;
111
+ }
112
+ }
113
+ // Parameter rows are customer-mapped values only. Never infer PE IN/OUT or
114
+ // derive belt velocity from VFD Hz without approved mechanical parameters.
115
+ const asset=model.equipment.find(e=>e.id===selected),params=$('selected-parameters');
116
+ if(asset&&params){
117
+ const find=role=>states.find(x=>x.role===role);
118
+ const val=(role,renderValue)=>{const x=find(role);return x?.valid ? renderValue(x) : 'UNKNOWN / N/A';};
119
+ const rows=[];const row=(name,value)=>{const el=document.createElement('div');el.className='detail-row';
120
+ const k=document.createElement('label');k.textContent=name;const v=document.createElement('strong');v.textContent=String(value);el.append(k,v);rows.push(el);};
121
+ if(['conveyor','sorter','spiral'].includes(asset.kind)){
122
+ row('RUN FEEDBACK',val('run_feedback',x=>x.value===true?'RUNNING':'STOPPED'));
123
+ row('MOTOR FAULT',val('fault',x=>x.active===true?'FAULT':'CLEAR'));
124
+ const ports=syntheticMode?virtualPE.get(asset.id):null;
125
+ row(syntheticMode?'PE IN · SIM':'PE IN',ports?(ports.pe_in?'BLOCKED':'CLEAR'):'NOT MAPPED');
126
+ row(syntheticMode?'PE OUT · SIM':'PE OUT',ports?(ports.pe_out?'BLOCKED':'CLEAR'):'NOT MAPPED');
127
+ row('BELT SPEED (m/s)','NOT MAPPED');
128
+ }else if(asset.kind==='photoeye'){
129
+ row('PE STATE',val('photoeye_blocked',x=>x.active===true?'BLOCKED':'CLEAR'));
130
+ row('PE COUNT',val('photoeye_count',x=>Number.isFinite(x.value)?x.value:'N/A'));
131
+ }else if(asset.kind==='vfd'){
132
+ row('FREQUENCY (Hz)',val('vfd_frequency',x=>typeof x.value==='number'?x.value.toFixed(1)+' Hz':'N/A'));
133
+ row('BELT SPEED','NOT MAPPED');
134
+ }else if(asset.kind==='scanner'){
135
+ row('LAST SCAN',val('scanner_result',x=>String(x.value).slice(0,32)));
136
+ row('NO_READ COUNT',val('no_read_count',x=>Number.isFinite(x.value)?x.value:'N/A'));
137
+ }
138
+ // Industrial diagnostic catalog: all customer-declared signals must be
139
+ // visible on the selected machine, including pumps, tanks, motors,
140
+ // valves and robots. Never invent source values, scaling or sensor links.
141
+ const roleLabels={run_feedback:'RUN FEEDBACK',run_command:'RUN COMMAND',fault:'MOTOR FAULT',
142
+ fault_code:'FAULT CODE',photoeye_blocked:'PHOTOEYE STATE',photoeye_count:'PE COUNT',
143
+ scanner_result:'LAST SCAN',scanner_barcode:'BARCODE',scan_count:'SCANS',
144
+ delivered_count:'DELIVERED',in_transit_count:'IN TRANSIT',no_read_count:'NO_READ COUNT',
145
+ motor_speed:'ACTUAL SPEED',vfd_frequency:'VFD FREQUENCY',tank_level:'TANK LEVEL',
146
+ pressure:'PRESSURE',flow:'FLOW',valve_open:'VALVE POSITION',mode:'OPERATING MODE',status:'PROCESS STATUS'};
147
+ const covered=new Set();
148
+ for(const role of ['run_feedback','fault','photoeye_blocked','photoeye_count','vfd_frequency','scanner_result','no_read_count']){
149
+ if(rows.some(r=>r.querySelector('label')?.textContent===({run_feedback:'RUN FEEDBACK',fault:'MOTOR FAULT',photoeye_blocked:'PE STATE',photoeye_count:'PE COUNT',vfd_frequency:'FREQUENCY (Hz)',scanner_result:'LAST SCAN',no_read_count:'NO_READ COUNT'}[role])))covered.add(role);
150
+ }
151
+ const mapping=new Map(model.signals.filter(s=>s.asset_id===asset.id).map(s=>[s.role,s]));
152
+ const order=['fault','fault_code','run_feedback','status','mode','valve_open','tank_level',
153
+ 'pressure','flow','motor_speed','vfd_frequency','photoeye_blocked','photoeye_count',
154
+ 'scanner_result','scanner_barcode','scan_count','no_read_count','in_transit_count',
155
+ 'delivered_count','run_command'];
156
+ for(const role of order){
157
+ const def=mapping.get(role);if(!def||covered.has(role))continue;
158
+ const obs=states.find(s=>s.role===role);
159
+ let value='UNKNOWN / N/A';
160
+ if(role==='run_command')value='READ ONLY · NO COMMAND AUTHORITY';
161
+ else if(obs?.valid){
162
+ if(role==='fault')value=obs.active===true?'FAULT':'CLEAR';
163
+ else if(role==='run_feedback')value=obs.value===true?'RUNNING':'STOPPED';
164
+ else if(role==='photoeye_blocked')value=obs.active===true?'BLOCKED':'CLEAR';
165
+ else if(role==='valve_open')value=obs.value===true?'OPEN':'CLOSED';
166
+ else if(typeof obs.value==='number'&&Number.isFinite(obs.value))value=obs.value.toLocaleString(undefined,{maximumFractionDigits:2})+(def.unit?' '+def.unit:'');
167
+ else value=String(obs.value).slice(0,60);
168
+ }
169
+ row(roleLabels[role]||role.toUpperCase(),value);
170
+ }
171
+ params.replaceChildren(...rows);
172
+ }
173
+ const note=$('selected-mapping-note');
174
+ if(note&&['conveyor','sorter','spiral'].includes(asset?.kind))
175
+ note.textContent=syntheticMode?'PE IN/OUT are VIRTUAL SIMULATOR PORTS, not customer-mapped sensors.':'PE IN/OUT need approved sensor-to-conveyor mappings.';
176
+ renderAlerts();
177
+ }
178
+ function line(className,text) {const div=document.createElement('div');div.className=className;div.textContent=text;return div;}
179
+ function renderAlerts(){
180
+ const frame=current(),list=$('alerts-items');list.replaceChildren();
181
+ if(!frame){
182
+ set('alerts-active','N/A active alarms');
183
+ set('alerts-source','OPC UA NOT CONNECTED · machine health UNKNOWN');
184
+ set('alerts-evidence','Event history unavailable');
185
+ list.append(line('empty-alerts','No alarm observations are available. Do not interpret this as zero active faults.'));
186
+ return;
187
+ }
188
+ const knownAlarms=frame.signals.filter(s=>['fault','photoeye_blocked','scanner_result'].includes(s.role)).every(s=>s.valid);
189
+ set('alerts-active',knownAlarms?`${frame.active_alarms.length} ACTIVE (${syntheticMode?'SYNTHETIC':'STUDIO LAB'})`:'N/A ACTIVE · QUALITY UNKNOWN');
190
+ set('alerts-source',labLive?'SIMULATION · NOT LIVE OPERATIONS':'REPLAY · NOT LIVE OPERATIONS');
191
+ set('alerts-evidence',`${receipt.events.length} ${labLive?'simulated':'replayed'} events`+(syntheticMode&&simAcknowledged&&simFaultAsset?' · '+simFaultAsset+' ACKNOWLEDGED IN SIMULATION':''));
192
+ const events=receipt.events.filter(x=>x.at<=frame.at).slice(-80).reverse();
193
+ if(!events.length){list.append(line('empty-alerts','No replay transitions in this frame window. This is not evidence of a healthy live facility.'));return;}
194
+ for(const event of events){
195
+ const row=document.createElement('article');row.className='alert-item '+event.severity.toLowerCase();
196
+ const header=document.createElement('div');header.className='event-header';
197
+ header.append(line('event-title',`${event.severity} · ${event.kind} · ${event.asset_id}`));
198
+ const go=document.createElement('button');go.type='button';go.textContent='INSPECT ASSET';
199
+ go.addEventListener('click',()=>selectEquipment(event.asset_id));header.append(go);
200
+ row.append(header,line('event-message',event.message),line('event-time',`${event.at} · ${event.signal_id??'QUALITY GROUP'} · ${syntheticMode?'SYNTHETIC DEMO':labLive?'ISOLATED STUDIO LAB':event.source}`));
201
+ list.append(row);
202
+ }
203
+ }
204
+ function validateReceipt(x){
205
+ if(!x||x.schema!=='devagent.operations.replay.v1'||x.status!=='LAB_REPLAY_ONLY_NOT_VERIFIED')throw Error('REPLAY_SCHEMA_OR_STATUS_HOLD');
206
+ if(!['SYNTHETIC_OFFLINE_FIXTURE','ISOLATED_LAB_OPC_READ_ONLY'].includes(x.origin))throw Error('UNKNOWN_RECEIPT_ORIGIN');
207
+ if(x.mapping_digest!==policy.mapping_digest||x.project_name!==model.name)throw Error('CUSTOMER_MAPPING_DIGEST_MISMATCH');
208
+ if(!x.evidence||x.evidence.production!=='HOLD'||x.evidence.ignition_perspective!=='NOT_EXECUTED'||x.evidence.plc_writes!=='DISABLED')throw Error('UNSAFE_EVIDENCE_CLAIM');
209
+ if(!Array.isArray(x.frames)||x.frames.length<1||x.frames.length>120||!Array.isArray(x.events)||x.events.length>2000)throw Error('UNSUPPORTED_REPLAY_SIZE');
210
+ const signalIds=new Set(model.signals.map(s=>s.id)),assetIds=new Set(model.equipment.map(e=>e.id));
211
+ let previousTime='';
212
+ for(const f of x.frames){
213
+ if(typeof f.at!=='string'||!Number.isFinite(Date.parse(f.at))||f.at<=previousTime)throw Error('FRAME_TIME_INVALID');
214
+ previousTime=f.at;
215
+ if(!f.metrics||!Array.isArray(f.signals)||!Array.isArray(f.quality_issues)||!Array.isArray(f.active_alarms))throw Error('FRAME_SCHEMA_HOLD');
216
+ if(f.signals.length!==model.signals.length)throw Error('SIGNAL_COUNT_MISMATCH');
217
+ for(const s of f.signals)if(!signalIds.has(s.signal_id)||!assetIds.has(s.asset_id))throw Error('UNDECLARED_SIGNAL_IN_REPLAY');
218
+ for(const alarm of f.active_alarms)if(!assetIds.has(alarm.asset_id)||!signalIds.has(alarm.signal_id))throw Error('UNDECLARED_ALARM_IN_REPLAY');
219
+ }
220
+ for(const event of x.events)if(!assetIds.has(event.asset_id)||!['CRITICAL','WARNING','INFO'].includes(event.severity)||event.source!=='LAB_REPLAY')throw Error('INVALID_EVENT_IN_REPLAY');
221
+ }
222
+ $('ops-upload')?.addEventListener('change',async e=>{
223
+ const file=e.target.files?.[0];if(!file)return;
224
+ if(file.size>2_000_000){set('ops-import-status','HOLD: Replay file exceeds 2 MB');return;}
225
+ try {
226
+ const candidate=JSON.parse(await file.text());validateReceipt(candidate);
227
+ if(labLive)throw Error('LAB_SOURCE_ACTIVE_RECEIPT_OVERRIDE_DENIED');
228
+ receipt=candidate;labLive=false;frameNumber=0;render();
229
+ } catch(err){set('ops-import-status',`HOLD: ${String(err.message??'INVALID_REPLAY').slice(0,100)}. Previous validated replay unchanged.`);}
230
+ e.target.value='';
231
+ });
232
+ $('ops-previous')?.addEventListener('click',()=>{if(receipt&&frameNumber>0){frameNumber--;render();}});
233
+ $('ops-next')?.addEventListener('click',()=>{if(receipt&&frameNumber<receipt.frames.length-1){frameNumber++;render();}});
234
+ const observer=new MutationObserver(()=>{if(receipt)render();});
235
+ observer.observe($('selected-id'),{childList:true,characterData:true,subtree:true});
236
+ const policies=Object.entries(policy.metrics).map(([key,val])=>`${key}: ${val.sources} mappings`).join(' · ');
237
+ set('ops-contract-summary',`STATIC CONTRACT ONLY · ${policies} · MAPPING ${policy.mapping_digest.slice(0,14)}…`);
238
+ updateBell('unknown');
239
+ renderAlerts();
240
+ // Source authority remains in the external Engineering Studio session.
241
+ // A lab readback never proves native Perspective or production PLC mapping.
242
+ return {
243
+ applyLabReading:result=>{
244
+ if(result?.status!=='PASS_STUDIO_LAB_DISPLAY_ONLY'||result?.source!=='ISOLATED_STUDIO_OPC_UA_READONLY'||
245
+ result?.production!=='HOLD'||result?.ignition!=='NOT_VERIFIED'||
246
+ !['CREATOR_LAB_SESSION_READONLY','UNBOUND_VERIFIED_MANIFEST_READONLY'].includes(result?.authority)){
247
+ throw Error('UNAPPROVED_LAB_FEED');
248
+ }
249
+ validateReceipt(result.receipt);
250
+ if(result.receipt.origin!=='ISOLATED_LAB_OPC_READ_ONLY')throw Error('LAB_RECEIPT_ORIGIN_HOLD');
251
+ receipt=result.receipt;labLive=true;syntheticMode=false;virtualPE.clear();simAcknowledged=false;simFaultAsset=null;frameNumber=receipt.frames.length-1;scene()?.parcels?.([]);render();
252
+ if($('ops-upload'))$('ops-upload').disabled=true;
253
+ },
254
+ applySyntheticReading:result=>{
255
+ if(result?.status!=='PASS_SYNTHETIC_PREVIEW_ONLY'||result?.source!=='IN_MEMORY_SYNTHETIC_NOT_OPC'||
256
+ result?.authority!=='PROJECT_SCOPED_DEMO_ONLY'||result?.opc_verified!==false||
257
+ result?.opc_writes!==false||result?.production!=='HOLD'||result?.ignition!=='NOT_VERIFIED'){
258
+ throw Error('UNAUTHORIZED_SYNTHETIC_FEED');
259
+ }
260
+ validateReceipt(result.receipt);
261
+ if(result.receipt.origin!=='SYNTHETIC_OFFLINE_FIXTURE')throw Error('NOT_SYNTHETIC_RECEIPT');
262
+ if(!Array.isArray(result.parcels)||result.parcels.length>4)throw Error('PARCELS_INVALID');
263
+ if(result.virtual_pe_source!=='CONCEPTUAL_SYNTHETIC_PORTS_NOT_MAPPED'||!Array.isArray(result.virtual_pe))throw Error('VIRTUAL_PORT_SOURCE_INVALID');
264
+ const transport=new Set(model.equipment.filter(e=>['conveyor','sorter','spiral','junction'].includes(e.kind)).map(e=>e.id));
265
+ if(result.virtual_pe.length>transport.size||new Set(result.virtual_pe.map(x=>x.asset_id)).size!==result.virtual_pe.length||
266
+ result.virtual_pe.some(x=>!transport.has(x.asset_id)||typeof x.pe_in!=='boolean'||typeof x.pe_out!=='boolean'))throw Error('VIRTUAL_PE_UNDECLARED_ASSET');
267
+ virtualPE=new Map(result.virtual_pe.map(x=>[x.asset_id,x]));
268
+ simAcknowledged=result.acknowledged===true;simFaultAsset=typeof result.fault_asset==='string'?result.fault_asset:null;
269
+ receipt=result.receipt;labLive=true;syntheticMode=true;frameNumber=receipt.frames.length-1;
270
+ scene()?.parcels?.(result.parcels);render();
271
+ },
272
+ sourceUnavailable:()=>{
273
+ receipt=null;labLive=false;syntheticMode=false;virtualPE.clear();simAcknowledged=false;simFaultAsset=null;frameNumber=0;
274
+ scene()?.parcels?.([]);
275
+ if($('ops-upload'))$('ops-upload').disabled=true;
276
+ scene()?.alarms?.([]);scene()?.operatingStates?.([]);
277
+ for(const key of Object.keys(ids)){
278
+ set('kpi-'+ids[key],'N/A');set('kpi-'+ids[key]+'-note','SOURCE NOT VERIFIED / UNAVAILABLE');
279
+ $('kpi-'+ids[key]).closest('.ops-metric').classList.remove('critical');
280
+ }
281
+ set('ops-mode','DATA UNAVAILABLE');set('source-indicator','SOURCE UNAVAILABLE');
282
+ const q=[...$('asset-meta').children].find(x=>x.firstElementChild?.textContent==='DATA QUALITY');
283
+ if(q)q.lastElementChild.textContent='UNKNOWN';
284
+ const st=[...$('asset-meta').children].find(x=>x.firstElementChild?.textContent==='STATUS');
285
+ if(st)st.lastElementChild.textContent='UNKNOWN';
286
+ if($('selected-signals'))for(const row of $('selected-signals').children){const small=row.querySelector('small');if(small)small.textContent='SOURCE UNAVAILABLE';}
287
+ // Fail closed on every operator-facing parameter, never leave simulated
288
+ // PE BLOCKED/CLEAR or RUN status visible when the source disappears.
289
+ const selectedAsset=model.equipment.find(e=>e.id===$('selected-id').textContent);
290
+ if(selectedAsset){
291
+ const params=$('selected-parameters');
292
+ if(params)for(const child of params.children){
293
+ const label=child.firstElementChild?.textContent||'';
294
+ child.lastElementChild.textContent=/^PE IN|^PE OUT|^BELT SPEED/.test(label)?'NOT MAPPED':'UNKNOWN / N/A';
295
+ if(label.startsWith('PE IN'))child.firstElementChild.textContent='PE IN';
296
+ if(label.startsWith('PE OUT'))child.firstElementChild.textContent='PE OUT';
297
+ }
298
+ const note=$('selected-mapping-note');
299
+ if(note&&['conveyor','sorter','spiral'].includes(selectedAsset.kind))note.textContent='PE IN/OUT need approved sensor-to-conveyor mappings.';
300
+ }
301
+ updateBell('unknown');renderAlerts();
302
+ }
303
+ };
304
+ }
305
+
@@ -0,0 +1,64 @@
1
+ <!DOCTYPE html>
2
+
3
+ <html lang="en">
4
+ <head><meta charset="utf-8"/><meta content="width=device-width,initial-scale=1" name="viewport"/><meta content="dark" name="color-scheme"/><title>DevAgent — Industrial Operations</title><link href="./style.css" rel="stylesheet"/></head>
5
+ <body>
6
+ <div id="root">
7
+ <header class="top"><div><div class="eyebrow">DEVAGENT / INDUSTRIAL OPERATIONS</div><h1 id="project-name">Industrial Facility</h1></div><div class="top-right"><span class="tag warn" id="source-indicator" title="Source unavailable until verified; never assume healthy states">SOURCE UNAVAILABLE</span><button aria-controls="alerts-dialog" aria-haspopup="dialog" aria-label="Notifications — source not verified" class="alert-bell is-unknown" id="alerts-toggle" title="Notifications — source not verified" type="button"><svg aria-hidden="true" class="bell-icon" focusable="false" viewbox="0 0 24 24"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 8.5-3 9.5h18c0-1-3-2.5-3-9.5ZM10 21h4"></path></svg><span aria-hidden="true" class="bell-badge is-unknown" id="alert-count">?</span></button></div></header>
8
+ <main>
9
+
10
+ <section aria-label="Industrial operations KPIs" class="ops-board">
11
+ <div class="ops-heading"><div><div class="eyebrow">OPERATIONAL INTELLIGENCE / SOURCE-QUALIFIED</div><b>System performance &amp; alarms</b></div><div class="ops-mode" id="ops-mode">NO LIVE SOURCE · KPI UNAVAILABLE</div></div>
12
+ <div class="ops-metrics">
13
+ <div class="ops-metric"><small>EQUIPMENT RUNNING</small><strong id="kpi-running">N/A</strong><span id="kpi-running-note">Awaiting verified data</span></div>
14
+ <div class="ops-metric"><small>ACTIVE FAULTS</small><strong id="kpi-faults">N/A</strong><span id="kpi-faults-note">Unknown ≠ no faults</span></div>
15
+ <div class="ops-metric"><small>PHOTOEYES BLOCKED</small><strong id="kpi-blocked">N/A</strong><span id="kpi-blocked-note">No live source</span></div>
16
+ <div class="ops-metric"><small>NO_READ EVENTS</small><strong id="kpi-noread">N/A</strong><span id="kpi-noread-note">Source not verified</span></div>
17
+ <div class="ops-metric"><small>TOTAL SCANS</small><strong id="kpi-scans">N/A</strong><span id="kpi-scans-note">Source not configured</span></div>
18
+ <div class="ops-metric"><small>PARCELS IN TRANSIT</small><strong id="kpi-transit">N/A</strong><span id="kpi-transit-note">Source not configured</span></div>
19
+ <div class="ops-metric"><small>DELIVERED PARCELS</small><strong id="kpi-delivered">N/A</strong><span id="kpi-delivered-note">Source not configured</span></div>
20
+ <div class="ops-metric"><small>LAST SCAN RESULT</small><strong id="kpi-scanresult">N/A</strong><span id="kpi-scanresult-note">Source not connected</span></div>
21
+ <div class="ops-metric"><small>LAST BARCODE</small><strong id="kpi-barcode">N/A</strong><span id="kpi-barcode-note">Source not configured</span></div>
22
+ <div class="ops-metric"><small>SIGNAL QUALITY</small><strong id="kpi-quality">N/A</strong><span id="kpi-quality-note">0 verified mappings</span></div>
23
+ </div>
24
+ </section>
25
+
26
+ <!-- Standard industrial operator controls. Presentation-only until engineering-
27
+ approved command nodes, role permissions and write interlocks exist.
28
+ NEVER confuse STOP with a safety-rated emergency stop. -->
29
+ <section class="operator-command-bar" aria-label="Industrial equipment controls">
30
+ <div class="operator-command-identity">
31
+ <strong>Facility controls</strong>
32
+ <span>Operator commands</span>
33
+ </div>
34
+ <div class="operator-command-actions" role="group" aria-label="Facility commands (simulation only when linked)">
35
+ <button type="button" class="operator-command-button cmd-start" id="op-cmd-start" disabled aria-disabled="true" title="START unavailable: approved PLC command mapping, permission and safety interlocks required">START</button>
36
+ <button type="button" class="operator-command-button cmd-stop" id="op-cmd-stop" disabled aria-disabled="true" title="STOP unavailable: approved PLC command mapping, permission and safety interlocks required; not an emergency stop">STOP</button>
37
+ <button type="button" class="operator-command-button cmd-reset" id="op-cmd-reset" disabled aria-disabled="true" title="RESET unavailable: PLC reset command and authorization not configured">RESET</button>
38
+ <button type="button" class="operator-command-button cmd-ack" id="op-cmd-ack" disabled aria-disabled="true" title="ACK unavailable: authenticated alarm acknowledgment and durable audit trail not configured">ACK</button>
39
+ </div>
40
+ <div class="operator-control-lock" role="status" id="operator-authority" title="No approved PLC write authority; alarm acknowledgment is not configured"><span aria-hidden="true">&#128274;</span> CONTROL LOCKED</div>
41
+ </section>
42
+
43
+ <div class="workspace">
44
+ <section class="viewer-panel"><div class="panel-head"><div><div class="eyebrow">01 / SPATIAL FACILITY MAP</div><h2 id="viewer-title">Spatial material flow</h2><p id="viewer-description">Live equipment overview</p></div></div>
45
+ <div class="viewer-toolbar"><div class="seg"><button class="active" id="view-fit">3D OVERVIEW</button><button id="view-top">TOP</button><button id="view-side">SIDE</button><button id="view-reset">RESET CAMERA</button></div><div class="seg"><label for="floor">FLOOR</label><select id="floor"><option value="all">ALL LEVELS</option></select></div></div>
46
+ <div aria-label="Interactive industrial scene" class="stage" id="view-stage" tabindex="0"><div id="scene-layer"></div><div class="stage-caption"><div class="eyebrow">FACILITY OVERVIEW</div><strong id="scene-label">Spatial layout</strong><small>Drag to orbit · Scroll to zoom · Select belt, label or Equipment list</small></div><div aria-label="Facility floor navigation" class="layer-index" id="layer-index"><div class="eyebrow">3D LAYER INDEX</div><div id="layer-buttons"></div></div><div class="engine-tag" id="scene-engine" role="status">Loading renderer…</div></div>
47
+ </section>
48
+ <aside class="inspector"><div class="panel-head inspector-head"><div><div class="eyebrow">02 / EQUIPMENT INSPECTOR</div><h2>Asset intelligence</h2><p>Select a machine in the spatial viewer</p></div></div><div class="inspector-body"><label for="equipment-jump" class="equipment-jump-label">FIND EQUIPMENT</label><select id="equipment-jump" class="equipment-jump" aria-label="Select equipment by ID"><option value="">Choose equipment…</option></select><div class="eyebrow" id="selected-type">SELECTED EQUIPMENT</div><h2 id="selected-id">—</h2><p id="selected-name">Select a machine for status and signals.</p><div class="small-title">DEVICE COMMANDS</div><div id="device-controls" aria-label="Device control intents (locked)"></div><div class="detail-grid" id="asset-meta"></div><div class="small-title">OPERATING PARAMETERS</div><div class="detail-grid" id="selected-parameters"></div></div></aside>
49
+ </div>
50
+ <dialog aria-labelledby="alerts-title" class="alerts-dialog" id="alerts-dialog">
51
+ <div class="alerts-title"><div><div class="eyebrow">DEVAGENT / OPERATOR AWARENESS</div><h2 id="alerts-title">Alarm &amp; Notification Center</h2><p id="alerts-source">NO LIVE SOURCE — current equipment alarm states unknown</p></div><button id="alerts-close" type="button">CLOSE ×</button></div>
52
+ <div class="alerts-summary"><b id="alerts-active">N/A active alarms</b><span id="alerts-evidence">No recent events</span></div>
53
+ <div aria-live="polite" class="alerts-items" id="alerts-items"><p class="empty-alerts">No alarm history available. This does not mean the equipment is healthy.</p></div>
54
+ <p class="alerts-disclaimer">Alarm acknowledgement and equipment reset are not available from this read-only display.</p>
55
+ </dialog>
56
+
57
+ <footer>DEVAGENT · INDUSTRIAL OPERATIONS</footer>
58
+ </main>
59
+ </div>
60
+ <script type="application/json" id="project-data">__EMBEDDED_PROJECT__</script>
61
+ <script type="application/json" id="operations-policy">__EMBEDDED_OPERATIONS_POLICY__</script><script type="application/json" id="control-plan">__EMBEDDED_CONTROL_PLAN__</script>
62
+ <script src="./viewer.js" type="module"></script>
63
+ </body></html>
64
+