Superboss1976 commited on
Commit
b4d155d
·
verified ·
1 Parent(s): 0cf7e5c

Add Advanced ARFF build and local training node preview

Browse files
Files changed (6) hide show
  1. README.md +2 -0
  2. academy.css +3 -0
  3. academy.html +1 -0
  4. academy.js +7 -4
  5. course-catalog.js +17 -0
  6. index.html +1 -0
README.md CHANGED
@@ -25,6 +25,8 @@ A working academy demonstration built around AM Risk's supplied brand artwork an
25
  - Official reference links for ICAO, NFPA, EASA, SACAA and UK CAA.
26
  - Clearly marked future-pathway outlines for marshalling, aerodrome familiarisation and wildlife awareness.
27
  - Read-only course register containing 25 official SACAA Opspec scope entries, with separate scope-verification and digital-release statuses.
 
 
28
 
29
  ## Scope
30
 
 
25
  - Official reference links for ICAO, NFPA, EASA, SACAA and UK CAA.
26
  - Clearly marked future-pathway outlines for marshalling, aerodrome familiarisation and wildlife awareness.
27
  - Read-only course register containing 25 official SACAA Opspec scope entries, with separate scope-verification and digital-release statuses.
28
+ - Advanced ARFF controlled build view with six modules, assessment and practical pathways, and seven human release gates.
29
+ - Local Training Node architecture view for the client-site appliance, content drive, Ollama model and MCP services.
30
 
31
  ## Scope
32
 
academy.css CHANGED
@@ -11,3 +11,6 @@
11
  .register-hero{display:grid;grid-template-columns:1.2fr 1fr 1fr;gap:12px;margin:22px 0}.register-hero>div{min-height:125px;padding:20px;border:1px solid #254b65;border-radius:8px;background:linear-gradient(145deg,#102b42,#081827);box-shadow:inset 0 1px #ffffff0a}.register-hero span,.register-search span{display:block;color:#6595b7;font-size:8px;letter-spacing:1.6px}.register-hero strong{display:block;margin:10px 0 6px;color:#f3f8fb;font-family:'Barlow Condensed',sans-serif;font-size:35px;line-height:1}.register-hero small{color:#7394aa;font-size:9px;line-height:1.6}.register-truth{margin-bottom:18px;padding:15px 18px;border:1px solid #28735d;border-radius:8px;background:linear-gradient(90deg,#0c302d,#0a2029);color:#a9cec5}.register-truth>div{display:flex;gap:12px;align-items:center;font-size:10px;line-height:1.6}.register-truth svg{flex:none;width:25px;color:#4bdbab}.register-truth b{display:block;color:#d8fff1;font-size:11px}.register-truth>small{display:block;margin:9px 0 0 37px;color:#739c94;font-size:8px}.register-panel{padding:22px}.register-toolbar{display:flex;align-items:end;justify-content:space-between;gap:20px;margin-bottom:18px}.register-toolbar h2{margin-bottom:4px}.register-toolbar p{font-size:9px}.register-search{width:min(360px,45%)}.register-search input{width:100%;margin-top:7px;padding:11px 13px;border:1px solid #315973;border-radius:5px;background:#071726;color:#e9f6ff;font:11px 'Montserrat',sans-serif;outline:none}.register-search input:focus{border-color:#26aef3;box-shadow:0 0 0 3px #159edf22}.register-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:9px}.register-card{display:grid;grid-template-columns:44px minmax(0,1fr);gap:13px;min-height:116px;padding:15px;border:1px solid #233f53;border-radius:7px;background:#0a1b2a}.register-number{display:grid;place-items:center;width:44px;height:44px;border:1px solid #2c6689;border-radius:50%;color:#7cccf3;font-family:'Barlow Condensed',sans-serif;font-size:18px}.register-main h3{margin:1px 0 6px;font-size:13px;line-height:1.35}.register-main>p{font-size:8px;line-height:1.5}.register-statuses{display:flex;flex-wrap:wrap;gap:6px;margin-top:12px}.register-statuses span{padding:5px 7px;border-radius:3px;font-size:7px;letter-spacing:.45px;text-transform:uppercase}.scope-ok{border:1px solid #2f8068;background:#11372f;color:#84e2c1}.digital-draft{border:1px solid #776533;background:#302b18;color:#e0c874}.register-grid>.empty-state{grid-column:1/-1}
12
  @media(max-width:900px){.register-grid{grid-template-columns:1fr}}
13
  @media(max-width:750px){.register-hero{grid-template-columns:1fr}.register-hero>div{min-height:0}.register-toolbar{align-items:stretch;flex-direction:column}.register-search{width:100%}.register-card{grid-template-columns:38px minmax(0,1fr);padding:13px}.register-number{width:38px;height:38px}.register-truth>small{margin-left:0}}
 
 
 
 
11
  .register-hero{display:grid;grid-template-columns:1.2fr 1fr 1fr;gap:12px;margin:22px 0}.register-hero>div{min-height:125px;padding:20px;border:1px solid #254b65;border-radius:8px;background:linear-gradient(145deg,#102b42,#081827);box-shadow:inset 0 1px #ffffff0a}.register-hero span,.register-search span{display:block;color:#6595b7;font-size:8px;letter-spacing:1.6px}.register-hero strong{display:block;margin:10px 0 6px;color:#f3f8fb;font-family:'Barlow Condensed',sans-serif;font-size:35px;line-height:1}.register-hero small{color:#7394aa;font-size:9px;line-height:1.6}.register-truth{margin-bottom:18px;padding:15px 18px;border:1px solid #28735d;border-radius:8px;background:linear-gradient(90deg,#0c302d,#0a2029);color:#a9cec5}.register-truth>div{display:flex;gap:12px;align-items:center;font-size:10px;line-height:1.6}.register-truth svg{flex:none;width:25px;color:#4bdbab}.register-truth b{display:block;color:#d8fff1;font-size:11px}.register-truth>small{display:block;margin:9px 0 0 37px;color:#739c94;font-size:8px}.register-panel{padding:22px}.register-toolbar{display:flex;align-items:end;justify-content:space-between;gap:20px;margin-bottom:18px}.register-toolbar h2{margin-bottom:4px}.register-toolbar p{font-size:9px}.register-search{width:min(360px,45%)}.register-search input{width:100%;margin-top:7px;padding:11px 13px;border:1px solid #315973;border-radius:5px;background:#071726;color:#e9f6ff;font:11px 'Montserrat',sans-serif;outline:none}.register-search input:focus{border-color:#26aef3;box-shadow:0 0 0 3px #159edf22}.register-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:9px}.register-card{display:grid;grid-template-columns:44px minmax(0,1fr);gap:13px;min-height:116px;padding:15px;border:1px solid #233f53;border-radius:7px;background:#0a1b2a}.register-number{display:grid;place-items:center;width:44px;height:44px;border:1px solid #2c6689;border-radius:50%;color:#7cccf3;font-family:'Barlow Condensed',sans-serif;font-size:18px}.register-main h3{margin:1px 0 6px;font-size:13px;line-height:1.35}.register-main>p{font-size:8px;line-height:1.5}.register-statuses{display:flex;flex-wrap:wrap;gap:6px;margin-top:12px}.register-statuses span{padding:5px 7px;border-radius:3px;font-size:7px;letter-spacing:.45px;text-transform:uppercase}.scope-ok{border:1px solid #2f8068;background:#11372f;color:#84e2c1}.digital-draft{border:1px solid #776533;background:#302b18;color:#e0c874}.register-grid>.empty-state{grid-column:1/-1}
12
  @media(max-width:900px){.register-grid{grid-template-columns:1fr}}
13
  @media(max-width:750px){.register-hero{grid-template-columns:1fr}.register-hero>div{min-height:0}.register-toolbar{align-items:stretch;flex-direction:column}.register-search{width:100%}.register-card{grid-template-columns:38px minmax(0,1fr);padding:13px}.register-number{width:38px;height:38px}.register-truth>small{margin-left:0}}
14
+ .featured-register{border-color:#299dd0;background:linear-gradient(135deg,#102f47,#0a1b2a);box-shadow:0 0 24px #159edf14}.featured-register .register-number{background:#123c58;box-shadow:0 0 18px #22a9ed33}.register-main h3 em{float:right;color:#54c8ff;font:7px 'Montserrat',sans-serif;letter-spacing:.8px}.build-banner{display:grid;grid-template-columns:1.25fr 1fr 1fr;gap:10px;margin:22px 0}.build-banner>div{padding:17px;border:1px solid #2a4e67;border-radius:7px;background:#0a1d2d}.build-banner span{display:block;margin-bottom:7px;color:#6394b4;font-size:7px;letter-spacing:1.3px}.build-banner b{color:#ddecf5;font-size:10px}.build-banner>div:nth-child(2) b{color:#74dfb7}.build-banner>div:nth-child(3){border-color:#755f2c;background:#282513}.build-banner>div:nth-child(3) b{color:#e0c76f}.course-build-grid{display:grid;grid-template-columns:minmax(0,1.55fr) minmax(260px,.7fr);gap:18px}.module-roadmap{display:grid;gap:8px;margin-bottom:26px}.module-roadmap article{display:grid;grid-template-columns:42px minmax(0,1fr) auto;align-items:center;gap:14px;padding:14px 16px;border:1px solid #223f53;border-radius:7px;background:#091b2a}.module-roadmap article>span{display:grid;place-items:center;width:38px;height:38px;border-radius:50%;background:#102f46;color:#71c8ef;font:16px 'Barlow Condensed',sans-serif}.module-roadmap h3{margin-bottom:4px;font-size:12px}.module-roadmap p{font-size:8px}.module-roadmap i{color:#b7a55f;font-size:7px;font-style:normal;letter-spacing:.8px}.assessment-flow{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}.assessment-flow>div{padding:15px;border:1px solid #26455a;border-radius:7px;background:#0b1e2e}.assessment-flow span,.assessment-flow small{display:block;color:#6e94aa;font-size:7px}.assessment-flow b{display:block;margin:10px 0;font-size:10px;line-height:1.4}.build-side{display:grid;align-content:start;gap:14px}.build-side .panel{padding:19px}.build-side .button{margin-top:16px}.release-panel{border-color:#66572e}.gate-list{display:grid;gap:8px;margin:14px 0 0;padding:0;list-style:none}.gate-list li{display:flex;gap:8px;color:#88a8bb;font-size:9px;line-height:1.5}.gate-list li::marker{color:#3e9ec9}.release-panel>p:last-child{margin-top:16px;color:#bfae69;font-size:8px;line-height:1.6}.node-map{display:grid;grid-template-columns:minmax(250px,.72fr) 1.4fr;gap:14px;margin:22px 0}.node-core{display:flex;min-height:270px;padding:28px;flex-direction:column;align-items:center;justify-content:center;text-align:center;border:1px solid #299fca;border-radius:10px;background:radial-gradient(circle at 50% 35%,#164f67,#0a2233 60%,#071725);box-shadow:0 0 45px #149ddd1a}.node-core>span svg{width:54px;color:#58d7d1}.node-core h2{margin:17px 0 6px;font-size:24px}.node-core p{font-size:9px}.node-core b{margin-top:20px;padding:7px 10px;border:1px solid #5d6c76;border-radius:3px;color:#9babb5;font-size:7px;letter-spacing:1px}.node-core b.online{border-color:#268e67;color:#78e2b8;background:#11372d}.node-spokes{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}.node-spokes article{padding:20px;border:1px solid #27465b;border-radius:8px;background:linear-gradient(145deg,#102a3f,#081927)}.node-spokes svg{width:27px;color:#52bde9}.node-spokes h3{margin:16px 0 7px;font-size:12px}.node-spokes p{font-size:8px}.node-status-strip{display:grid;grid-template-columns:repeat(4,1fr);margin-bottom:18px;border:1px solid #29485c;border-radius:7px;background:#091a28}.node-status-strip>div{padding:15px;border-right:1px solid #29485c}.node-status-strip>div:last-child{border:0}.node-status-strip span{display:block;color:#648da7;font-size:7px;letter-spacing:1px}.node-status-strip b{display:block;margin-top:7px;color:#d0e5f0;font-size:8px}.node-explain{display:grid;grid-template-columns:1fr 1fr;gap:30px}.node-explain>div>p{font-size:10px;line-height:1.8}.node-rules{display:grid;gap:9px}.node-rules span{padding:12px;border-left:2px solid #2ea7d8;background:#0a1b29;color:#5e93b2;font-size:9px}.node-rules b{margin-left:8px;color:#b9d2df}
15
+ @media(max-width:900px){.course-build-grid,.node-map{grid-template-columns:1fr}.build-side{grid-template-columns:1fr 1fr}.node-core{min-height:220px}}
16
+ @media(max-width:750px){.build-banner,.assessment-flow,.node-status-strip{grid-template-columns:1fr}.course-build-grid,.node-explain{grid-template-columns:1fr}.build-side{grid-template-columns:1fr}.module-roadmap article{grid-template-columns:38px minmax(0,1fr)}.module-roadmap article>i{grid-column:2}.node-spokes{grid-template-columns:1fr}.node-status-strip>div{border-right:0;border-bottom:1px solid #29485c}.register-main h3 em{float:none;display:block;margin-top:6px}}
academy.html CHANGED
@@ -16,6 +16,7 @@
16
  <a href="#home" data-page="home"><span data-icon="home"></span>Overview</a>
17
  <a href="#learning" data-page="learning"><span data-icon="book"></span>My learning <b>3</b></a>
18
  <a href="#catalog" data-page="catalog"><span data-icon="shield"></span>Course register <b>25</b></a>
 
19
  <a href="simulator.html"><span data-icon="plane"></span>Scenario lab <i>3D</i></a>
20
  <a href="#progress" data-page="progress"><span data-icon="chart"></span>My progress</a>
21
  <a href="#resources" data-page="resources"><span data-icon="folder"></span>Resource library</a>
 
16
  <a href="#home" data-page="home"><span data-icon="home"></span>Overview</a>
17
  <a href="#learning" data-page="learning"><span data-icon="book"></span>My learning <b>3</b></a>
18
  <a href="#catalog" data-page="catalog"><span data-icon="shield"></span>Course register <b>25</b></a>
19
+ <a href="#node" data-page="node"><span data-icon="radio"></span>Local node <i>LAN</i></a>
20
  <a href="simulator.html"><span data-icon="plane"></span>Scenario lab <i>3D</i></a>
21
  <a href="#progress" data-page="progress"><span data-icon="chart"></span>My progress</a>
22
  <a href="#resources" data-page="resources"><span data-icon="folder"></span>Resource library</a>
academy.js CHANGED
@@ -1,12 +1,12 @@
1
  import { lessons } from './lessons.js';
2
  import { sources } from './sources.js';
3
- import { courseRegister } from './course-catalog.js';
4
  import { readState, saveState, summary, storageAvailable, freshSession } from './state.js';
5
  const $=id=>document.getElementById(id), esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
6
  const shapes={home:'<path d="m3 10 9-7 9 7v10H3Z"/><path d="M9 20v-7h6v7"/>',book:'<path d="M12 5C8 2 3 3 2 4v16c3-2 7-1 10 1 3-2 7-3 10-1V4c-3-2-7-1-10 1v16"/>',plane:'<path d="m12 2 2 8 8 5v2l-8-2v5l3 2H7l3-2v-5l-8 2v-2l8-5Z"/>',chart:'<path d="M3 21h19M5 17v-5h3v5m3 0V8h3v9m3 0V3h3v14"/>',folder:'<path d="M2 7V4h7l3 3h10v13H2Z"/>',people:'<circle cx="9" cy="7" r="3"/><path d="M2 21v-3a7 7 0 0 1 14 0v3M17 4a3 3 0 0 1 0 6m1 4a6 6 0 0 1 4 6"/>',person:'<circle cx="12" cy="7" r="4"/><path d="M3 22v-3a9 9 0 0 1 18 0v3"/>',shield:'<path d="m12 2 9 4v6c0 5-9 10-9 10S3 17 3 12V6Z"/><path d="m8 12 3 3 5-6"/>',play:'<path d="m8 4 12 8-12 8Z"/>',arrow:'<path d="M4 12h16m-6-6 6 6-6 6"/>',spark:'<path d="m12 2 3 7 7 3-7 3-3 7-3-7-7-3 7-3ZM20 2v4m-2-2h4"/>',voice:'<path d="M11 5 6 9H2v6h4l5 4ZM15 8a6 6 0 0 1 0 8m3-11a10 10 0 0 1 0 14"/>',stop:'<rect x="5" y="5" width="14" height="14" rx="2"/>',clock:'<circle cx="12" cy="12" r="9"/><path d="M12 6v6l4 2"/>',check:'<path d="m4 12 5 5L20 6"/>',radio:'<rect x="6" y="7" width="12" height="15" rx="2"/><path d="M8 7V2m3 10h4m-4 4h4m-4 3h4"/>',tower:'<path d="M8 22V10L4 6V3h16v3l-4 4v12M4 6h16M8 10h8M6 22h12"/>',bird:'<path d="M2 8c5-3 8 1 10 4 3-5 6-7 10-7-3 5-4 8-10 11C6 15 3 12 2 8Z"/>',download:'<path d="M12 2v13m-5-5 5 5 5-5M3 17v5h18v-5"/>',save:'<path d="M3 3h15l3 3v15H3ZM7 3v7h10V3M7 21v-7h10v7"/>'};
7
  const icon=(name)=>`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${shapes[name]||shapes.book}</svg>`;
8
  const fillIcons=()=>document.querySelectorAll('[data-icon]').forEach(e=>e.innerHTML=icon(e.dataset.icon));
9
- const titles={home:'Overview',learning:'My learning',catalog:'Course register',progress:'My progress',resources:'Resource library',instructor:'Instructor desk'};
10
  let state=readState(), page='home', speechRun=0, speaking=false, toastTimer, speechTimer, pendingNotes=null;
11
  const speechOK='speechSynthesis' in window && 'SpeechSynthesisUtterance' in window;
12
  const route=i=>`simulator.html?lesson=${i}`;
@@ -21,16 +21,19 @@ function narrate(text){stopSpeech();if(!speechOK){toast('Voice is unavailable in
21
  function courseCards(){return `<div class="course-grid">${lessons.map((l,i)=>{const s=state.sessions[i];return `<a class="course-card" href="${route(i)}"><div class="course-art ${i===1?'communication':i===2?'command':''}">${i===0?'<img src="assets/advanced-arff-cover.png" alt="AM Risk ARFF course artwork" loading="lazy">':''}${i===1?'<div class="radio-lines">'+[12,24,40,27,57,35,48,20,36,60,30,17].map(h=>`<i style="height:${h}px"></i>`).join('')+'</div>':''}<span class="course-tag">GUIDED 3D EXERCISE</span>${icon(i===0?'plane':i===1?'radio':'people')}</div><div class="course-body"><span class="course-level">0${i+1} / ${esc(l.level)}</span><h3>${esc(l.title)}</h3><p>${lessonDescriptions[i]}</p><div class="course-meta"><span>${icon('clock')} 3 decisions</span><span class="${s.ended?'course-done':''}">${s.ended?'✓ Completed':s.started?`${s.answers.length}/3 answered`:'Practice exercise'}</span></div><span class="course-action">${s.ended?'Review exercise':s.started?'Continue exercise':'Enter scenario'} ${icon('arrow')}</span></div></a>`;}).join('')}</div>`;}
22
  function heading(eyebrow,title,body){return `<div class="page-title"><p class="eyebrow">${eyebrow}</p><h1>${title}</h1><p>${body}</p></div>`;}
23
  function outlines(){return `<div class="outline-grid">${pathways.map((x,i)=>`<button class="outline-card" data-pathway="${i}">${icon(x.icon)}<span>${x.title}<small>${x.subtitle}</small></span><b>↗</b></button>`).join('')}</div>`;}
24
- function catalogRows(query=''){const q=query.trim().toLowerCase(),rows=courseRegister.courses.filter(c=>!q||`${c.sequence} ${c.officialName} ${c.approvalDate}`.toLowerCase().includes(q));return rows.length?rows.map(c=>`<article class="register-card"><span class="register-number">${String(c.sequence).padStart(2,'0')}</span><div class="register-main"><h3>${esc(c.officialName)}</h3><p>Official Opspec name · approved ${esc(c.approvalDate)}</p><div class="register-statuses"><span class="scope-ok">✓ Scope entry verified</span><span class="digital-draft">Digital profile draft</span></div></div></article>`).join(''):`<div class="empty-state">${icon('book')}<h3>No matching scope entry.</h3><p>Try another course name or sequence number.</p></div>`;}
25
  function catalog(){return heading('CONTROLLED COURSE REGISTER','The real AM Risk training scope.','A read-only academy view of the official SACAA Opspec course names. It shows what is in the approved scope and what still needs to be built for digital delivery.')+`<section class="register-hero"><div><span>ATO NUMBER</span><strong>${esc(courseRegister.atoNumber)}</strong><small>${esc(courseRegister.organisation)}</small></div><div><span>VERIFIED SCOPE ENTRIES</span><strong>25</strong><small>Current Opspec source snapshot</small></div><div><span>DIGITALLY RELEASED</span><strong>0</strong><small>Profiles still require controlled mapping</small></div></section><section class="register-truth"><div>${icon('shield')}<span><b>Two different truths are shown.</b> “Scope entry verified” confirms the course name appears in the controlled Opspec source. “Digital profile draft” means the online curriculum is not yet approved or released.</span></div><small>Source effective ${esc(courseRegister.effectiveDate)} · expires ${esc(courseRegister.expiryDate)} · register verified ${esc(courseRegister.verifiedDate)}</small></section><section class="panel register-panel"><div class="register-toolbar"><div><h2>25 official scope entries</h2><p>Official source spelling is preserved.</p></div><label class="register-search"><span>SEARCH THE REGISTER</span><input id="catalog-search" type="search" placeholder="Try ARFF, driver, fire officer…" autocomplete="off"></label></div><div id="catalog-results" class="register-grid">${catalogRows()}</div></section><p class="progress-note">Register status: human ATO review required. ${esc(courseRegister.reconciliation)}. The three scenario-lab exercises remain demonstrations and are not listed here as released courses.</p>`;}
26
  function bindCatalog(){if(!$('catalog-search'))return;$('catalog-search').addEventListener('input',e=>{$('catalog-results').innerHTML=catalogRows(e.target.value);fillIcons();});}
 
 
 
27
  function home(){const s=summary(state),all=s.complete===3;return `<div class="welcome-line"><div><h1>Your next step towards readiness.</h1><p>A place to learn, practise and build clearer decisions.</p></div><span class="date-label">PEOPLE · PREPARE · PROTECT</span></div><div class="home-columns"><div class="home-left"><section class="hero"><div class="hero-art"><img src="assets/arff-excellence.png" alt="AM Risk rescue vehicle at an airport at sunset"></div><div class="hero-content"><span class="hero-eyebrow">AM RISK & TRAINING / ARFF ACADEMY</span><h2>Train today.<br><span>Be ready tomorrow.</span></h2><p>Step into the scenario. Read the scene.<br>Build your reasoning with Prof.</p><a class="button" href="${route(s.next)}">${icon('play')} ${all?'Revisit the training lab':s.decisions?'Continue learning':'Start your first scenario'} ${icon('arrow')}</a></div><span class="hero-bottom">REAL TRAINING STARTS WITH A CLEARER PICTURE</span></section><div class="stats-strip"><div class="stat"><span class="stat-icon">${icon('book')}</span><div><strong>03</strong><small>Guided exercises</small></div></div><div class="stat"><span class="stat-icon">${icon('check')}</span><div><strong>${String(s.complete).padStart(2,'0')}<em> / 03</em></strong><small>Exercises completed</small></div></div><div class="stat"><span class="stat-icon">${icon('chart')}</span><div><strong>${String(s.decisions).padStart(2,'0')}<em> / 09</em></strong><small>Decisions practised</small></div></div></div><section><div class="section-title"><h2>Your training ground</h2><a href="#learning">View my learning ↗</a></div>${courseCards()}</section><section class="explore-section"><div class="section-title"><h2>Where the academy can go next</h2><span class="view-label">PATHWAY PREVIEWS</span></div>${outlines()}</section><div class="promise-strip"><span>${icon('shield')} LEARN WITH PURPOSE</span><span>${icon('plane')} THINK THROUGH THE SCENE</span><span>${icon('people')} REFLECT WITH YOUR INSTRUCTOR</span></div></div><aside class="home-right"><section class="prof-card"><div class="prof-card-top"><span>${icon('spark')} YOUR LEARNING GUIDE</span><small>MEET PROF</small></div><div class="prof-art"><img class="prof-watermark" src="assets/badge.png" alt=""><img class="prof-image" src="assets/prof.png" alt="Prof, the AM Risk learning guide"><span class="portrait-label">YOUR PROF PORTRAIT · DEVICE NARRATION</span></div><div class="prof-intro"><h2>Hi, I’m <span>Prof.</span></h2><p>Let’s build your skills for a safer tomorrow. I’ll help you find your next step.</p><div class="prof-actions"><button class="button" data-guide>${icon('spark')} Ask Prof ${icon('arrow')}</button><button class="icon-button" id="prof-welcome" aria-label="Hear Prof’s welcome">${icon('voice')}</button></div><span class="prof-status" id="prof-status">Portrait with device narration</span></div></section><section class="journey-card"><div class="journey-top"><h3>Your journey</h3><span>ARFF PRACTICE</span></div><div class="journey-body"><div class="progress-ring" style="--pct:${s.percent}%"><strong>${s.percent}<small>%</small></strong></div><p>${all?'Three exercises. Well done.':s.complete?'Keep building your skills.':'Your journey starts here.'}<small>${s.complete} of 3 exercises completed<br>Saved on this device</small></p></div><a href="#progress">See your progress ${icon('arrow')}</a></section></aside></div>`;}
28
  function learning(){return heading('YOUR LEARNING JOURNEY','Small steps. Stronger understanding.','Work through three guided ARFF practice exercises, then discuss your decisions with an instructor.')+`<section class="learning-feature"><img src="assets/advanced-arff-cover.png" alt="Advanced ARFF course concept artwork"><div><p class="eyebrow">ARFF / PRACTICE PATHWAY</p><h2>Read. Decide. Reflect.</h2><p>Explore an illustrative aircraft scene, choose a response and see why it matters. Each exercise ends with your own debrief.</p><div class="mini-list"><span><b>01</b> Observe the scene and identify missing information</span><span><b>02</b> Make three decisions and review the feedback</span><span><b>03</b> Keep a practice record for an instructor discussion</span></div><div class="action-row"><a class="button" href="${route(summary(state).next)}">Enter the scenario lab ${icon('arrow')}</a></div></div></section><div class="section-title"><h2>Available exercises</h2><span class="view-label">${summary(state).complete} OF 3 COMPLETED</span></div>${courseCards()}<div class="blue-line"></div><div class="section-title"><h2>Future learning pathways</h2><span class="view-label">OUTLINES FOR REVIEW</span></div>${outlines()}<p class="progress-note">The supplied Advanced ARFF artwork shows the academy’s intended direction. This version contains the three practice exercises above; it is not the complete Advanced ARFF course.</p>`;}
29
  function progress(){const s=summary(state);return heading('YOUR PRACTICE RECORD','See how far you’ve come.','Your actual exercise activity, saved in this browser. These are practice records, not competence certificates.')+`<div class="progress-stats"><div class="metric"><small>PATHWAY COMPLETION</small><strong>${s.percent}%</strong><span>Completed exercises</span></div><div class="metric"><small>EXERCISES COMPLETED</small><strong>${s.complete}<em> / 3</em></strong><span>Current practice attempts</span></div><div class="metric"><small>DECISIONS PRACTISED</small><strong>${s.decisions}<em> / 9</em></strong><span>Across current attempts</span></div><div class="metric"><small>STRONGEST RESPONSES</small><strong>${s.correct}<em> / ${s.decisions}</em></strong><span>First choices in these exercises</span></div></div><section class="panel"><div class="section-title"><h2>Your exercises</h2><span class="view-label">SAVED ON THIS DEVICE</span></div><div class="table-scroll"><table class="progress-table"><thead><tr><th>EXERCISE</th><th>STATUS</th><th>FIRST CHOICES</th><th></th></tr></thead><tbody>${lessons.map((l,i)=>{const a=state.sessions[i];return `<tr><td>0${i+1} · ${l.title}</td><td><span class="pill ${a.ended?'green':''}">${status(a)}</span></td><td>${a.answers.length?a.answers.filter(x=>x.correct).length+' / '+a.answers.length:'—'}</td><td><a href="${route(i)}">${a.ended?'Review':a.started?'Continue':'Start'} ↗</a></td></tr>`;}).join('')}</tbody></table></div><div class="action-row"><button class="button secondary" id="export-progress">${icon('download')} View / save practice record</button><button class="button ghost" id="reset-practice">Reset practice progress</button></div><p class="progress-note">Progress stays in this browser and is not synced to other devices. Clearing browser data removes it. Restarting an exercise replaces that attempt; completed attempts also appear below.</p></section><section class="panel" style="margin-top:20px"><div class="section-title"><h2>Completed attempt history</h2><span class="view-label">LATEST 40 ATTEMPTS</span></div>${state.history.length?`<div class="table-scroll"><table class="progress-table"><thead><tr><th>EXERCISE</th><th>COMPLETED</th><th>FIRST CHOICES</th></tr></thead><tbody>${[...state.history].reverse().map(x=>`<tr><td>${lessons[x.lesson].title}</td><td>${esc(formatDate(x.date))}</td><td>${x.score} / 3</td></tr>`).join('')}</tbody></table></div>`:'<div class="empty-state">'+icon('chart')+'<h3>Your first completion will appear here.</h3><p>Complete all three decisions and open the debrief in an exercise.</p><a class="button secondary" href="simulator.html">Start practising →</a></div>'}</section>`;}
30
  function resources(){return heading('THE RESOURCE LIBRARY','Know where your learning comes from.','Start with the official organisations. Applicable editions and local procedures must be selected for your approved training programme.')+`<div class="resource-list">${sources.map(s=>`<a class="resource-card" href="${s.url}" target="_blank" rel="noopener noreferrer"><div class="source-name">${s.name}</div><div><h3>${s.title}</h3><p>${s.scope}</p><small>${s.status}</small></div><span>↗</span></a>`).join('')}</div><section class="panel" style="margin-top:24px"><h2>Your organisation’s manuals</h2><div class="empty-state">${icon('folder')}<h3>A place for your approved material.</h3><p>No AM Risk manuals, aerodrome emergency plans or local SOPs have been added to this demonstration. The instructor desk includes a space to record the documents and editions you want to use.</p><a class="button secondary" href="#instructor">Open instructor desk →</a></div></section><p class="progress-note">These links are source directories and publication pages. The academy does not claim approval, accreditation or full alignment with ICAO, NFPA, EASA, SACAA or UK CAA.</p>`;}
31
  function instructor(){return heading('INSTRUCTOR DESK','Put your expertise into the academy.','Review the authored exercises, record local context and export your review notes. This desk is a demonstration workspace on this device.')+`<div class="panel-grid"><section class="panel"><div class="instructor-overview"><div><h2>Curriculum review</h2><p>3 exercises · 9 decisions · authored feedback</p></div><span class="review-status">Awaiting qualified instructor review</span></div>${lessons.map((l,i)=>`<details class="review-item"><summary><b>0${i+1} · ${l.title}</b> — ${l.aim}</summary><div><p>${l.intro}</p>${l.steps.map((q,j)=>`<h3>${j+1}. ${q.title}</h3><p>${q.prompt}</p><ol type="A">${q.options.map(o=>`<li class="${o.correct?'correct-answer':''}">${o.correct?'✓ ':''}${o.text}</li>`).join('')}</ol><p><b>Feedback for the strongest response:</b> ${q.options.find(o=>o.correct).feedback}</p>`).join('')}</div></details>`).join('')}<p class="progress-note">Review the applicable authority, adopted editions, learning outcomes and assessment arrangements before using content in formal training.</p></section><section class="panel"><p class="eyebrow">YOUR LOCAL CONTEXT</p><h2>Instructor review notes</h2><div class="field"><label for="review-notes">Documents, changes and follow-up</label><textarea id="review-notes" maxlength="6000" placeholder="Record the approved local SOPs, document editions, site details and changes you want your instructor to review."></textarea><small>Saved only in this browser. This is not a shared approval workflow. Avoid entering sensitive operational or learner information.</small></div><div class="action-row"><button id="save-notes" class="button">${icon('save')} Save notes</button><button id="export-review" class="button secondary">${icon('download')} Export review</button></div><p class="progress-note" id="notes-status" role="status">${state.reviewNotes?'Saved notes loaded from this device.':'Your notes will appear here when saved.'}</p><div class="blue-line"></div><h3>Before a formal course launches</h3><div class="mini-list"><span><b>01</b> Confirm authority, syllabus and editions</span><span><b>02</b> Add approved local procedures and scenarios</span><span><b>03</b> Validate questions and practical assessments</span><span><b>04</b> Establish learner records and instructor sign-off</span></div></section></div>`;}
32
  function formatDate(value){const d=new Date(value);return Number.isNaN(d.getTime())?'Date unavailable':d.toLocaleString('en-ZA',{dateStyle:'medium',timeStyle:'short'});}
33
- function render(){stopSpeech();state=readState();page=location.hash.slice(1);if(!titles[page])page='home';$('page-name').textContent=titles[page].toUpperCase();document.title=`${titles[page]} | AM Risk & Training`;$('page-content').innerHTML=({home,learning,catalog,progress,resources,instructor}[page])();document.querySelectorAll('[data-page]').forEach(a=>{a.classList.toggle('selected',a.dataset.page===page);if(a.dataset.page===page)a.setAttribute('aria-current','page');else a.removeAttribute('aria-current');});updateHeader();fillIcons();bindPage();bindCatalog();}
34
  function bindPage(){document.querySelectorAll('[data-pathway]').forEach(b=>b.addEventListener('click',()=>{const p=pathways[Number(b.dataset.pathway)];openModal('PATHWAY PREVIEW',`<h2>${p.title}</h2><p>${p.body}</p><h3>Suggested learning areas</h3><ul>${p.items.map(x=>`<li>${x}</li>`).join('')}</ul><p>${p.note} This outline is for curriculum planning, not a completed course.</p><div class="action-row"><a class="button" href="#instructor" id="outline-instructor">Plan in instructor desk →</a></div>`);$('outline-instructor').onclick=()=>$('academy-dialog').close();}));document.querySelectorAll('[data-guide]').forEach(b=>b.onclick=openGuide);if($('prof-welcome'))$('prof-welcome').onclick=()=>speaking?stopSpeech():narrate('Hi, I’m Prof, your AM Risk learning guide. Start with Smoke at stand four. Explore the three observation points, then make your decisions. We will reflect on what you knew, what was missing, and what you should discuss with your instructor. Your progress is saved on this device.');if($('export-progress'))$('export-progress').onclick=()=>showReport('YOUR PRACTICE RECORD',practiceReport(),'AM-Risk-practice-record.txt');if($('reset-practice'))$('reset-practice').onclick=()=>{openModal('RESET PRACTICE PROGRESS','<h2>Start a fresh practice pathway?</h2><p>This clears the three exercise attempts and their completion history on this device. Your display name and instructor notes stay saved. Export a practice record first if you want to keep your results.</p><div class="action-row"><button class="button" id="confirm-reset">Reset these practice records</button><button class="button secondary" id="cancel-reset">Keep my progress</button></div>');$('cancel-reset').onclick=()=>$('academy-dialog').close();$('confirm-reset').onclick=()=>{const s=readState();s.sessions=lessons.map(freshSession);s.history=[];if(saveState(s)){$('academy-dialog').close();render();toast('Practice progress reset on this device.');}else toast('Browser storage unavailable. Progress was not reset.');};};if($('review-notes')){const input=$('review-notes');input.value=pendingNotes??state.reviewNotes;input.oninput=()=>{pendingNotes=input.value;$('notes-status').textContent='Unsaved changes — use Save notes.';};$('save-notes').onclick=saveNotes;$('export-review').onclick=()=>{saveNotes();showReport('INSTRUCTOR REVIEW',reviewReport(),'AM-Risk-curriculum-review.txt');};}}
35
  function saveNotes(){const s=readState();s.reviewNotes=$('review-notes').value;const ok=saveState(s);pendingNotes=ok?null:s.reviewNotes;$('notes-status').textContent=ok?'Notes saved on this device.':'Notes could not be saved. Export or copy them before leaving.';toast(ok?'Instructor notes saved.':'Browser storage unavailable. Copy your notes.');}
36
  const guideTopics=[{label:'Where do I start?',keys:/start|begin|first|course|learn/i,text:'Start with “Smoke at stand 04”. Explore the three observation points, choose Start briefing, and work through three decisions. You can return later: this browser keeps your current attempt.',href:route(0),link:'Start the first exercise'}, {label:'What is ARFF?',keys:/arff|rescue|fire|purpose|life/i,text:'ARFF means aircraft rescue and firefighting. The high-level purpose centres on saving lives, supporting survivable conditions and escape. This demo practises observation, communication and reflection; operational methods belong to your approved training and local procedures.',href:'#resources',link:'Explore the official references'}, {label:'What can I ask you?',keys:/prof|avatar|ai|talk|voice|question/i,text:'I’m a guided learning character in this version. These answers are written learning notes, and the audio uses a voice on your device. My photograph is a still portrait. For now I can explain the exercises, progress and reference library; I am not an open-ended AI instructor.',href:'#learning',link:'Explore your learning pathway'}, {label:'Where are the standards?',keys:/icao|nfpa|easa|sacaa|caa|standard|regulation|certif|qualif/i,text:'The resource library links to ICAO, NFPA, EASA, SACAA and UK CAA. Full standards and licensed publications have not been loaded. A qualified instructor must confirm the applicable editions and map the curriculum before this becomes formal standards-based training.',href:'#resources',link:'Open the reference library'}, {label:'How is progress saved?',keys:/progress|score|save|record|result/i,text:'Completing an exercise adds one of three completions. The score counts your first choices against the authored responses. Both your current attempts and up to 40 completed attempts stay in this browser. Clearing its data removes them; records do not sync to another device.',href:'#progress',link:'View your actual progress'}];
 
1
  import { lessons } from './lessons.js';
2
  import { sources } from './sources.js';
3
+ import { courseRegister, advancedArffPlan } from './course-catalog.js';
4
  import { readState, saveState, summary, storageAvailable, freshSession } from './state.js';
5
  const $=id=>document.getElementById(id), esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
6
  const shapes={home:'<path d="m3 10 9-7 9 7v10H3Z"/><path d="M9 20v-7h6v7"/>',book:'<path d="M12 5C8 2 3 3 2 4v16c3-2 7-1 10 1 3-2 7-3 10-1V4c-3-2-7-1-10 1v16"/>',plane:'<path d="m12 2 2 8 8 5v2l-8-2v5l3 2H7l3-2v-5l-8 2v-2l8-5Z"/>',chart:'<path d="M3 21h19M5 17v-5h3v5m3 0V8h3v9m3 0V3h3v14"/>',folder:'<path d="M2 7V4h7l3 3h10v13H2Z"/>',people:'<circle cx="9" cy="7" r="3"/><path d="M2 21v-3a7 7 0 0 1 14 0v3M17 4a3 3 0 0 1 0 6m1 4a6 6 0 0 1 4 6"/>',person:'<circle cx="12" cy="7" r="4"/><path d="M3 22v-3a9 9 0 0 1 18 0v3"/>',shield:'<path d="m12 2 9 4v6c0 5-9 10-9 10S3 17 3 12V6Z"/><path d="m8 12 3 3 5-6"/>',play:'<path d="m8 4 12 8-12 8Z"/>',arrow:'<path d="M4 12h16m-6-6 6 6-6 6"/>',spark:'<path d="m12 2 3 7 7 3-7 3-3 7-3-7-7-3 7-3ZM20 2v4m-2-2h4"/>',voice:'<path d="M11 5 6 9H2v6h4l5 4ZM15 8a6 6 0 0 1 0 8m3-11a10 10 0 0 1 0 14"/>',stop:'<rect x="5" y="5" width="14" height="14" rx="2"/>',clock:'<circle cx="12" cy="12" r="9"/><path d="M12 6v6l4 2"/>',check:'<path d="m4 12 5 5L20 6"/>',radio:'<rect x="6" y="7" width="12" height="15" rx="2"/><path d="M8 7V2m3 10h4m-4 4h4m-4 3h4"/>',tower:'<path d="M8 22V10L4 6V3h16v3l-4 4v12M4 6h16M8 10h8M6 22h12"/>',bird:'<path d="M2 8c5-3 8 1 10 4 3-5 6-7 10-7-3 5-4 8-10 11C6 15 3 12 2 8Z"/>',download:'<path d="M12 2v13m-5-5 5 5 5-5M3 17v5h18v-5"/>',save:'<path d="M3 3h15l3 3v15H3ZM7 3v7h10V3M7 21v-7h10v7"/>'};
7
  const icon=(name)=>`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${shapes[name]||shapes.book}</svg>`;
8
  const fillIcons=()=>document.querySelectorAll('[data-icon]').forEach(e=>e.innerHTML=icon(e.dataset.icon));
9
+ const titles={home:'Overview',learning:'My learning',catalog:'Course register','advanced-arff':'Advanced ARFF',node:'Local node',progress:'My progress',resources:'Resource library',instructor:'Instructor desk'};
10
  let state=readState(), page='home', speechRun=0, speaking=false, toastTimer, speechTimer, pendingNotes=null;
11
  const speechOK='speechSynthesis' in window && 'SpeechSynthesisUtterance' in window;
12
  const route=i=>`simulator.html?lesson=${i}`;
 
21
  function courseCards(){return `<div class="course-grid">${lessons.map((l,i)=>{const s=state.sessions[i];return `<a class="course-card" href="${route(i)}"><div class="course-art ${i===1?'communication':i===2?'command':''}">${i===0?'<img src="assets/advanced-arff-cover.png" alt="AM Risk ARFF course artwork" loading="lazy">':''}${i===1?'<div class="radio-lines">'+[12,24,40,27,57,35,48,20,36,60,30,17].map(h=>`<i style="height:${h}px"></i>`).join('')+'</div>':''}<span class="course-tag">GUIDED 3D EXERCISE</span>${icon(i===0?'plane':i===1?'radio':'people')}</div><div class="course-body"><span class="course-level">0${i+1} / ${esc(l.level)}</span><h3>${esc(l.title)}</h3><p>${lessonDescriptions[i]}</p><div class="course-meta"><span>${icon('clock')} 3 decisions</span><span class="${s.ended?'course-done':''}">${s.ended?'✓ Completed':s.started?`${s.answers.length}/3 answered`:'Practice exercise'}</span></div><span class="course-action">${s.ended?'Review exercise':s.started?'Continue exercise':'Enter scenario'} ${icon('arrow')}</span></div></a>`;}).join('')}</div>`;}
22
  function heading(eyebrow,title,body){return `<div class="page-title"><p class="eyebrow">${eyebrow}</p><h1>${title}</h1><p>${body}</p></div>`;}
23
  function outlines(){return `<div class="outline-grid">${pathways.map((x,i)=>`<button class="outline-card" data-pathway="${i}">${icon(x.icon)}<span>${x.title}<small>${x.subtitle}</small></span><b>↗</b></button>`).join('')}</div>`;}
24
+ function catalogRows(query=''){const q=query.trim().toLowerCase(),rows=courseRegister.courses.filter(c=>!q||`${c.sequence} ${c.officialName} ${c.approvalDate}`.toLowerCase().includes(q));return rows.length?rows.map(c=>{const tag=c.sequence===6?'a':'article',link=c.sequence===6?' href="#advanced-arff"':'';return `<${tag} class="register-card${c.sequence===6?' featured-register':''}"${link}><span class="register-number">${String(c.sequence).padStart(2,'0')}</span><div class="register-main"><h3>${esc(c.officialName)}${c.sequence===6?' <em>OPEN BUILD →</em>':''}</h3><p>Official Opspec name · approved ${esc(c.approvalDate)}</p><div class="register-statuses"><span class="scope-ok">✓ Scope entry verified</span><span class="digital-draft">Digital profile draft</span></div></div></${tag}>`;}).join(''):`<div class="empty-state">${icon('book')}<h3>No matching scope entry.</h3><p>Try another course name or sequence number.</p></div>`;}
25
  function catalog(){return heading('CONTROLLED COURSE REGISTER','The real AM Risk training scope.','A read-only academy view of the official SACAA Opspec course names. It shows what is in the approved scope and what still needs to be built for digital delivery.')+`<section class="register-hero"><div><span>ATO NUMBER</span><strong>${esc(courseRegister.atoNumber)}</strong><small>${esc(courseRegister.organisation)}</small></div><div><span>VERIFIED SCOPE ENTRIES</span><strong>25</strong><small>Current Opspec source snapshot</small></div><div><span>DIGITALLY RELEASED</span><strong>0</strong><small>Profiles still require controlled mapping</small></div></section><section class="register-truth"><div>${icon('shield')}<span><b>Two different truths are shown.</b> “Scope entry verified” confirms the course name appears in the controlled Opspec source. “Digital profile draft” means the online curriculum is not yet approved or released.</span></div><small>Source effective ${esc(courseRegister.effectiveDate)} · expires ${esc(courseRegister.expiryDate)} · register verified ${esc(courseRegister.verifiedDate)}</small></section><section class="panel register-panel"><div class="register-toolbar"><div><h2>25 official scope entries</h2><p>Official source spelling is preserved.</p></div><label class="register-search"><span>SEARCH THE REGISTER</span><input id="catalog-search" type="search" placeholder="Try ARFF, driver, fire officer…" autocomplete="off"></label></div><div id="catalog-results" class="register-grid">${catalogRows()}</div></section><p class="progress-note">Register status: human ATO review required. ${esc(courseRegister.reconciliation)}. The three scenario-lab exercises remain demonstrations and are not listed here as released courses.</p>`;}
26
  function bindCatalog(){if(!$('catalog-search'))return;$('catalog-search').addEventListener('input',e=>{$('catalog-results').innerHTML=catalogRows(e.target.value);fillIcons();});}
27
+ function advancedArff(){const p=advancedArffPlan;return heading('COURSE BUILD / '+p.courseId,'Advanced Aircraft Firefighting.','The first controlled digital course build. Its training journey is designed; formal release remains locked until authorised humans approve every source, assessment and practical definition.')+`<section class="build-banner"><div><span>COURSE VERSION</span><b>${esc(p.versionId)}</b></div><div><span>ATO SCOPE</span><b>✓ VERIFIED</b></div><div><span>DIGITAL RELEASE</span><b>LOCKED · DRAFT</b></div></section><section class="course-build-grid"><div class="build-main"><div class="section-title"><h2>Six-module learner journey</h2><span class="view-label">CONTROLLED BUILD</span></div><div class="module-roadmap">${p.modules.map(m=>`<article><span>${m[0]}</span><div><h3>${esc(m[1])}</h3><p>${esc(m[2])}</p></div><i>${m[0]==='04'?'SCENARIOS READY':'DRAFT'}</i></article>`).join('')}</div><div class="section-title"><h2>Assessment pathway</h2><span class="view-label">NO AUTOMATIC COMPETENCY</span></div><div class="assessment-flow">${p.assessments.map((x,i)=>`<div><span>0${i+1}</span><b>${esc(x)}</b><small>Authorised review required</small></div>`).join('')}</div></div><aside class="build-side"><section class="panel"><p class="eyebrow">PRACTICAL DEFINITIONS</p><h2>From simulator to supervised work.</h2><ol class="gate-list">${p.practicals.map(x=>`<li>${esc(x)}</li>`).join('')}</ol><a class="button" href="simulator.html">Open current scenario lab ${icon('arrow')}</a></section><section class="panel release-panel"><p class="eyebrow">RELEASE CONTROL</p><h2>Seven gates stay locked.</h2><ul class="gate-list">${p.gates.map(x=>`<li><span>○</span>${esc(x)}</li>`).join('')}</ul><p>Only an authorised AM Risk human can release this course. Prof cannot approve a gate.</p></section></aside></section>`;}
28
+ function nodePage(){return heading('AM RISK TRAINING NODE','Your academy, inside the client site.','The same academy can run from one local computer and serve approved tablets and laptops over the site network—even when the internet is disconnected.')+`<section class="node-map"><div class="node-core"><span>${icon('shield')}</span><h2>AM Risk Training Node</h2><p>Academy · records · controlled content</p><b id="node-live">CHECKING THIS HOST…</b></div><div class="node-spokes"><article>${icon('folder')}<h3>Local content drive</h3><p id="node-content">Videos, manuals and site overlays</p></article><article>${icon('spark')}<h3>Local Prof model</h3><p id="node-ai">Optional Ollama model</p></article><article>${icon('radio')}<h3>MCP tools</h3><p>Five read-only training tools</p></article><article>${icon('people')}<h3>Client-site network</h3><p>Approved devices use the node IP</p></article></div></section><section class="node-status-strip"><div><span>ACADEMY</span><b>PORT 8080</b></div><div><span>MCP</span><b>PORT 8000</b></div><div><span>LOCAL AI</span><b>PORT 11434</b></div><div><span>INTERNET</span><b>OPTIONAL AFTER SETUP</b></div></section><section class="panel node-explain"><div><p class="eyebrow">HOW THE CLIENT SITE WORKS</p><h2>One appliance. Many approved devices.</h2><p>The server and training drive stay at the client site. Learners open the academy in a browser. Videos stream from the local SSD, Prof uses the local model and approved retrieval library, and records remain on the controlled runtime volume.</p></div><div class="node-rules"><span>01 <b>Content volume is read-only</b></span><span>02 <b>Learner records use a separate protected volume</b></span><span>03 <b>AI cannot approve competence or certificates</b></span><span>04 <b>Updates arrive as signed controlled packs</b></span></div></section><p class="progress-note" id="node-note">On the public Hugging Face preview this page shows the architecture. When served by the local Training Node, it reads live storage, model and course-pack status from the same machine.</p>`;}
29
+ async function bindNode(){if(!$('node-live'))return;try{const r=await fetch('/api/node/status',{cache:'no-store'});if(!r.ok)throw new Error('preview');const s=await r.json();$('node-live').textContent='● LOCAL NODE ONLINE';$('node-live').classList.add('online');$('node-content').textContent=`${s.content.videos} videos · ${s.content.documents} documents · ${s.content.images} images`;$('node-ai').textContent=s.local_ai.available?`${s.local_ai.model} ready`:`${s.local_ai.model} configured · model offline`;$('node-note').textContent=`Live node: ${s.node_name}. Content root ${s.content_root_available?'available':'missing'}. Course release: ${s.course_pack.release_status}.`; }catch{$('node-live').textContent='PUBLIC CLOUD PREVIEW';}}
30
  function home(){const s=summary(state),all=s.complete===3;return `<div class="welcome-line"><div><h1>Your next step towards readiness.</h1><p>A place to learn, practise and build clearer decisions.</p></div><span class="date-label">PEOPLE · PREPARE · PROTECT</span></div><div class="home-columns"><div class="home-left"><section class="hero"><div class="hero-art"><img src="assets/arff-excellence.png" alt="AM Risk rescue vehicle at an airport at sunset"></div><div class="hero-content"><span class="hero-eyebrow">AM RISK & TRAINING / ARFF ACADEMY</span><h2>Train today.<br><span>Be ready tomorrow.</span></h2><p>Step into the scenario. Read the scene.<br>Build your reasoning with Prof.</p><a class="button" href="${route(s.next)}">${icon('play')} ${all?'Revisit the training lab':s.decisions?'Continue learning':'Start your first scenario'} ${icon('arrow')}</a></div><span class="hero-bottom">REAL TRAINING STARTS WITH A CLEARER PICTURE</span></section><div class="stats-strip"><div class="stat"><span class="stat-icon">${icon('book')}</span><div><strong>03</strong><small>Guided exercises</small></div></div><div class="stat"><span class="stat-icon">${icon('check')}</span><div><strong>${String(s.complete).padStart(2,'0')}<em> / 03</em></strong><small>Exercises completed</small></div></div><div class="stat"><span class="stat-icon">${icon('chart')}</span><div><strong>${String(s.decisions).padStart(2,'0')}<em> / 09</em></strong><small>Decisions practised</small></div></div></div><section><div class="section-title"><h2>Your training ground</h2><a href="#learning">View my learning ↗</a></div>${courseCards()}</section><section class="explore-section"><div class="section-title"><h2>Where the academy can go next</h2><span class="view-label">PATHWAY PREVIEWS</span></div>${outlines()}</section><div class="promise-strip"><span>${icon('shield')} LEARN WITH PURPOSE</span><span>${icon('plane')} THINK THROUGH THE SCENE</span><span>${icon('people')} REFLECT WITH YOUR INSTRUCTOR</span></div></div><aside class="home-right"><section class="prof-card"><div class="prof-card-top"><span>${icon('spark')} YOUR LEARNING GUIDE</span><small>MEET PROF</small></div><div class="prof-art"><img class="prof-watermark" src="assets/badge.png" alt=""><img class="prof-image" src="assets/prof.png" alt="Prof, the AM Risk learning guide"><span class="portrait-label">YOUR PROF PORTRAIT · DEVICE NARRATION</span></div><div class="prof-intro"><h2>Hi, I’m <span>Prof.</span></h2><p>Let’s build your skills for a safer tomorrow. I’ll help you find your next step.</p><div class="prof-actions"><button class="button" data-guide>${icon('spark')} Ask Prof ${icon('arrow')}</button><button class="icon-button" id="prof-welcome" aria-label="Hear Prof’s welcome">${icon('voice')}</button></div><span class="prof-status" id="prof-status">Portrait with device narration</span></div></section><section class="journey-card"><div class="journey-top"><h3>Your journey</h3><span>ARFF PRACTICE</span></div><div class="journey-body"><div class="progress-ring" style="--pct:${s.percent}%"><strong>${s.percent}<small>%</small></strong></div><p>${all?'Three exercises. Well done.':s.complete?'Keep building your skills.':'Your journey starts here.'}<small>${s.complete} of 3 exercises completed<br>Saved on this device</small></p></div><a href="#progress">See your progress ${icon('arrow')}</a></section></aside></div>`;}
31
  function learning(){return heading('YOUR LEARNING JOURNEY','Small steps. Stronger understanding.','Work through three guided ARFF practice exercises, then discuss your decisions with an instructor.')+`<section class="learning-feature"><img src="assets/advanced-arff-cover.png" alt="Advanced ARFF course concept artwork"><div><p class="eyebrow">ARFF / PRACTICE PATHWAY</p><h2>Read. Decide. Reflect.</h2><p>Explore an illustrative aircraft scene, choose a response and see why it matters. Each exercise ends with your own debrief.</p><div class="mini-list"><span><b>01</b> Observe the scene and identify missing information</span><span><b>02</b> Make three decisions and review the feedback</span><span><b>03</b> Keep a practice record for an instructor discussion</span></div><div class="action-row"><a class="button" href="${route(summary(state).next)}">Enter the scenario lab ${icon('arrow')}</a></div></div></section><div class="section-title"><h2>Available exercises</h2><span class="view-label">${summary(state).complete} OF 3 COMPLETED</span></div>${courseCards()}<div class="blue-line"></div><div class="section-title"><h2>Future learning pathways</h2><span class="view-label">OUTLINES FOR REVIEW</span></div>${outlines()}<p class="progress-note">The supplied Advanced ARFF artwork shows the academy’s intended direction. This version contains the three practice exercises above; it is not the complete Advanced ARFF course.</p>`;}
32
  function progress(){const s=summary(state);return heading('YOUR PRACTICE RECORD','See how far you’ve come.','Your actual exercise activity, saved in this browser. These are practice records, not competence certificates.')+`<div class="progress-stats"><div class="metric"><small>PATHWAY COMPLETION</small><strong>${s.percent}%</strong><span>Completed exercises</span></div><div class="metric"><small>EXERCISES COMPLETED</small><strong>${s.complete}<em> / 3</em></strong><span>Current practice attempts</span></div><div class="metric"><small>DECISIONS PRACTISED</small><strong>${s.decisions}<em> / 9</em></strong><span>Across current attempts</span></div><div class="metric"><small>STRONGEST RESPONSES</small><strong>${s.correct}<em> / ${s.decisions}</em></strong><span>First choices in these exercises</span></div></div><section class="panel"><div class="section-title"><h2>Your exercises</h2><span class="view-label">SAVED ON THIS DEVICE</span></div><div class="table-scroll"><table class="progress-table"><thead><tr><th>EXERCISE</th><th>STATUS</th><th>FIRST CHOICES</th><th></th></tr></thead><tbody>${lessons.map((l,i)=>{const a=state.sessions[i];return `<tr><td>0${i+1} · ${l.title}</td><td><span class="pill ${a.ended?'green':''}">${status(a)}</span></td><td>${a.answers.length?a.answers.filter(x=>x.correct).length+' / '+a.answers.length:'—'}</td><td><a href="${route(i)}">${a.ended?'Review':a.started?'Continue':'Start'} ↗</a></td></tr>`;}).join('')}</tbody></table></div><div class="action-row"><button class="button secondary" id="export-progress">${icon('download')} View / save practice record</button><button class="button ghost" id="reset-practice">Reset practice progress</button></div><p class="progress-note">Progress stays in this browser and is not synced to other devices. Clearing browser data removes it. Restarting an exercise replaces that attempt; completed attempts also appear below.</p></section><section class="panel" style="margin-top:20px"><div class="section-title"><h2>Completed attempt history</h2><span class="view-label">LATEST 40 ATTEMPTS</span></div>${state.history.length?`<div class="table-scroll"><table class="progress-table"><thead><tr><th>EXERCISE</th><th>COMPLETED</th><th>FIRST CHOICES</th></tr></thead><tbody>${[...state.history].reverse().map(x=>`<tr><td>${lessons[x.lesson].title}</td><td>${esc(formatDate(x.date))}</td><td>${x.score} / 3</td></tr>`).join('')}</tbody></table></div>`:'<div class="empty-state">'+icon('chart')+'<h3>Your first completion will appear here.</h3><p>Complete all three decisions and open the debrief in an exercise.</p><a class="button secondary" href="simulator.html">Start practising →</a></div>'}</section>`;}
33
  function resources(){return heading('THE RESOURCE LIBRARY','Know where your learning comes from.','Start with the official organisations. Applicable editions and local procedures must be selected for your approved training programme.')+`<div class="resource-list">${sources.map(s=>`<a class="resource-card" href="${s.url}" target="_blank" rel="noopener noreferrer"><div class="source-name">${s.name}</div><div><h3>${s.title}</h3><p>${s.scope}</p><small>${s.status}</small></div><span>↗</span></a>`).join('')}</div><section class="panel" style="margin-top:24px"><h2>Your organisation’s manuals</h2><div class="empty-state">${icon('folder')}<h3>A place for your approved material.</h3><p>No AM Risk manuals, aerodrome emergency plans or local SOPs have been added to this demonstration. The instructor desk includes a space to record the documents and editions you want to use.</p><a class="button secondary" href="#instructor">Open instructor desk →</a></div></section><p class="progress-note">These links are source directories and publication pages. The academy does not claim approval, accreditation or full alignment with ICAO, NFPA, EASA, SACAA or UK CAA.</p>`;}
34
  function instructor(){return heading('INSTRUCTOR DESK','Put your expertise into the academy.','Review the authored exercises, record local context and export your review notes. This desk is a demonstration workspace on this device.')+`<div class="panel-grid"><section class="panel"><div class="instructor-overview"><div><h2>Curriculum review</h2><p>3 exercises · 9 decisions · authored feedback</p></div><span class="review-status">Awaiting qualified instructor review</span></div>${lessons.map((l,i)=>`<details class="review-item"><summary><b>0${i+1} · ${l.title}</b> — ${l.aim}</summary><div><p>${l.intro}</p>${l.steps.map((q,j)=>`<h3>${j+1}. ${q.title}</h3><p>${q.prompt}</p><ol type="A">${q.options.map(o=>`<li class="${o.correct?'correct-answer':''}">${o.correct?'✓ ':''}${o.text}</li>`).join('')}</ol><p><b>Feedback for the strongest response:</b> ${q.options.find(o=>o.correct).feedback}</p>`).join('')}</div></details>`).join('')}<p class="progress-note">Review the applicable authority, adopted editions, learning outcomes and assessment arrangements before using content in formal training.</p></section><section class="panel"><p class="eyebrow">YOUR LOCAL CONTEXT</p><h2>Instructor review notes</h2><div class="field"><label for="review-notes">Documents, changes and follow-up</label><textarea id="review-notes" maxlength="6000" placeholder="Record the approved local SOPs, document editions, site details and changes you want your instructor to review."></textarea><small>Saved only in this browser. This is not a shared approval workflow. Avoid entering sensitive operational or learner information.</small></div><div class="action-row"><button id="save-notes" class="button">${icon('save')} Save notes</button><button id="export-review" class="button secondary">${icon('download')} Export review</button></div><p class="progress-note" id="notes-status" role="status">${state.reviewNotes?'Saved notes loaded from this device.':'Your notes will appear here when saved.'}</p><div class="blue-line"></div><h3>Before a formal course launches</h3><div class="mini-list"><span><b>01</b> Confirm authority, syllabus and editions</span><span><b>02</b> Add approved local procedures and scenarios</span><span><b>03</b> Validate questions and practical assessments</span><span><b>04</b> Establish learner records and instructor sign-off</span></div></section></div>`;}
35
  function formatDate(value){const d=new Date(value);return Number.isNaN(d.getTime())?'Date unavailable':d.toLocaleString('en-ZA',{dateStyle:'medium',timeStyle:'short'});}
36
+ function render(){stopSpeech();state=readState();page=location.hash.slice(1);if(!titles[page])page='home';$('page-name').textContent=titles[page].toUpperCase();document.title=`${titles[page]} | AM Risk & Training`;$('page-content').innerHTML=({home,learning,catalog,'advanced-arff':advancedArff,node:nodePage,progress,resources,instructor}[page])();document.querySelectorAll('[data-page]').forEach(a=>{a.classList.toggle('selected',a.dataset.page===page);if(a.dataset.page===page)a.setAttribute('aria-current','page');else a.removeAttribute('aria-current');});updateHeader();fillIcons();bindPage();bindCatalog();bindNode();}
37
  function bindPage(){document.querySelectorAll('[data-pathway]').forEach(b=>b.addEventListener('click',()=>{const p=pathways[Number(b.dataset.pathway)];openModal('PATHWAY PREVIEW',`<h2>${p.title}</h2><p>${p.body}</p><h3>Suggested learning areas</h3><ul>${p.items.map(x=>`<li>${x}</li>`).join('')}</ul><p>${p.note} This outline is for curriculum planning, not a completed course.</p><div class="action-row"><a class="button" href="#instructor" id="outline-instructor">Plan in instructor desk →</a></div>`);$('outline-instructor').onclick=()=>$('academy-dialog').close();}));document.querySelectorAll('[data-guide]').forEach(b=>b.onclick=openGuide);if($('prof-welcome'))$('prof-welcome').onclick=()=>speaking?stopSpeech():narrate('Hi, I’m Prof, your AM Risk learning guide. Start with Smoke at stand four. Explore the three observation points, then make your decisions. We will reflect on what you knew, what was missing, and what you should discuss with your instructor. Your progress is saved on this device.');if($('export-progress'))$('export-progress').onclick=()=>showReport('YOUR PRACTICE RECORD',practiceReport(),'AM-Risk-practice-record.txt');if($('reset-practice'))$('reset-practice').onclick=()=>{openModal('RESET PRACTICE PROGRESS','<h2>Start a fresh practice pathway?</h2><p>This clears the three exercise attempts and their completion history on this device. Your display name and instructor notes stay saved. Export a practice record first if you want to keep your results.</p><div class="action-row"><button class="button" id="confirm-reset">Reset these practice records</button><button class="button secondary" id="cancel-reset">Keep my progress</button></div>');$('cancel-reset').onclick=()=>$('academy-dialog').close();$('confirm-reset').onclick=()=>{const s=readState();s.sessions=lessons.map(freshSession);s.history=[];if(saveState(s)){$('academy-dialog').close();render();toast('Practice progress reset on this device.');}else toast('Browser storage unavailable. Progress was not reset.');};};if($('review-notes')){const input=$('review-notes');input.value=pendingNotes??state.reviewNotes;input.oninput=()=>{pendingNotes=input.value;$('notes-status').textContent='Unsaved changes — use Save notes.';};$('save-notes').onclick=saveNotes;$('export-review').onclick=()=>{saveNotes();showReport('INSTRUCTOR REVIEW',reviewReport(),'AM-Risk-curriculum-review.txt');};}}
38
  function saveNotes(){const s=readState();s.reviewNotes=$('review-notes').value;const ok=saveState(s);pendingNotes=ok?null:s.reviewNotes;$('notes-status').textContent=ok?'Notes saved on this device.':'Notes could not be saved. Export or copy them before leaving.';toast(ok?'Instructor notes saved.':'Browser storage unavailable. Copy your notes.');}
39
  const guideTopics=[{label:'Where do I start?',keys:/start|begin|first|course|learn/i,text:'Start with “Smoke at stand 04”. Explore the three observation points, choose Start briefing, and work through three decisions. You can return later: this browser keeps your current attempt.',href:route(0),link:'Start the first exercise'}, {label:'What is ARFF?',keys:/arff|rescue|fire|purpose|life/i,text:'ARFF means aircraft rescue and firefighting. The high-level purpose centres on saving lives, supporting survivable conditions and escape. This demo practises observation, communication and reflection; operational methods belong to your approved training and local procedures.',href:'#resources',link:'Explore the official references'}, {label:'What can I ask you?',keys:/prof|avatar|ai|talk|voice|question/i,text:'I’m a guided learning character in this version. These answers are written learning notes, and the audio uses a voice on your device. My photograph is a still portrait. For now I can explain the exercises, progress and reference library; I am not an open-ended AI instructor.',href:'#learning',link:'Explore your learning pathway'}, {label:'Where are the standards?',keys:/icao|nfpa|easa|sacaa|caa|standard|regulation|certif|qualif/i,text:'The resource library links to ICAO, NFPA, EASA, SACAA and UK CAA. Full standards and licensed publications have not been loaded. A qualified instructor must confirm the applicable editions and map the curriculum before this becomes formal standards-based training.',href:'#resources',link:'Open the reference library'}, {label:'How is progress saved?',keys:/progress|score|save|record|result/i,text:'Completing an exercise adds one of three completions. The score counts your first choices against the authored responses. Both your current attempts and up to 40 completed attempts stay in this browser. Clearing its data removes them; records do not sync to another device.',href:'#progress',link:'View your actual progress'}];
course-catalog.js CHANGED
@@ -40,3 +40,20 @@ export const courseRegister = {
40
  digitalStatus: 'DRAFT_PROFILE_PENDING_MAPPING'
41
  }))
42
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  digitalStatus: 'DRAFT_PROFILE_PENDING_MAPPING'
41
  }))
42
  };
43
+
44
+ export const advancedArffPlan = {
45
+ courseId: 'CRS-AMR-006',
46
+ versionId: 'CRV-AMR-006-OPSPEC-20251119',
47
+ releaseStatus: 'DRAFT · CONTROLLED MAPPING REQUIRED',
48
+ modules: [
49
+ ['01','Course orientation and rescue priorities','Source mapping required'],
50
+ ['02','Aircraft construction, systems and hazards','Source mapping required'],
51
+ ['03','Fire behaviour, agents and application strategy','Source mapping required'],
52
+ ['04','Incident command, communications and scene management','3 scenario exercises available'],
53
+ ['05','Supervised practical activities','Instructor definitions required'],
54
+ ['06','Assessment, remedial training and closeout','Assessment validation required']
55
+ ],
56
+ assessments: ['Theory knowledge assessment','Instructor-led oral assessment','Authorised practical assessment'],
57
+ practicals: ['Scene assessment and initial report','Changing conditions and communications','Command handover','Site-approved live practical'],
58
+ gates: ['Controlled source mapping','Approved module content','Validated assessments and rubrics','Approved practical definitions','Current instructor authorisation','Selected site overlay','Verified offline content integrity']
59
+ };
index.html CHANGED
@@ -16,6 +16,7 @@
16
  <a href="#home" data-page="home"><span data-icon="home"></span>Overview</a>
17
  <a href="#learning" data-page="learning"><span data-icon="book"></span>My learning <b>3</b></a>
18
  <a href="#catalog" data-page="catalog"><span data-icon="shield"></span>Course register <b>25</b></a>
 
19
  <a href="simulator.html"><span data-icon="plane"></span>Scenario lab <i>3D</i></a>
20
  <a href="#progress" data-page="progress"><span data-icon="chart"></span>My progress</a>
21
  <a href="#resources" data-page="resources"><span data-icon="folder"></span>Resource library</a>
 
16
  <a href="#home" data-page="home"><span data-icon="home"></span>Overview</a>
17
  <a href="#learning" data-page="learning"><span data-icon="book"></span>My learning <b>3</b></a>
18
  <a href="#catalog" data-page="catalog"><span data-icon="shield"></span>Course register <b>25</b></a>
19
+ <a href="#node" data-page="node"><span data-icon="radio"></span>Local node <i>LAN</i></a>
20
  <a href="simulator.html"><span data-icon="plane"></span>Scenario lab <i>3D</i></a>
21
  <a href="#progress" data-page="progress"><span data-icon="chart"></span>My progress</a>
22
  <a href="#resources" data-page="resources"><span data-icon="folder"></span>Resource library</a>