eaglelandsonce's picture
Update index.html
96c29ab verified
Raw
History Blame Contribute Delete
46.7 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Signal Cascade 3D: Wireless Core Fault Simulator</title>
<style>
:root {
--bg: #070b18;
--panel: rgba(16, 24, 48, 0.92);
--panel2: rgba(30, 42, 78, 0.92);
--text: #eef3ff;
--muted: #a9b7d9;
--line: #344266;
--accent: #7dd3fc;
--healthy: #27c46b;
--degraded: #ffd166;
--faulted: #ef476f;
--mitigating: #4cc9f0;
--restored: #b388ff;
--unknown: #80889f;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
font-family: Arial, Helvetica, sans-serif;
color: var(--text);
background: radial-gradient(circle at top, #172b5d 0%, var(--bg) 58%);
overflow-x: hidden;
}
header {
padding: 20px 24px 8px;
text-align: center;
}
h1 {
margin: 0 0 8px;
font-size: clamp(28px, 4vw, 46px);
letter-spacing: 0.4px;
}
.subtitle {
margin: 0 auto;
max-width: 1050px;
color: var(--muted);
line-height: 1.45;
font-size: 16px;
}
.app {
display: grid;
grid-template-columns: minmax(620px, 1fr) 430px;
gap: 16px;
padding: 16px;
max-width: 1500px;
min-width: 1060px;
margin: 0 auto;
align-items: start;
}
.stage-card, .side-card {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 20px;
box-shadow: 0 18px 42px rgba(0,0,0,0.35);
overflow: hidden;
}
.stage-card { position: relative; min-height: 720px; }
#scene {
display: block;
width: 100%;
height: 720px;
cursor: grab;
}
#scene:active { cursor: grabbing; }
.hud {
position: absolute;
top: 14px;
left: 14px;
right: 14px;
display: grid;
grid-template-columns: repeat(6, minmax(90px, 1fr));
gap: 8px;
pointer-events: none;
}
.stat {
background: rgba(7, 12, 28, 0.76);
border: 1px solid rgba(125, 211, 252, 0.18);
border-radius: 14px;
padding: 9px 10px;
text-align: center;
backdrop-filter: blur(8px);
}
.stat strong { display: block; font-size: 20px; }
.stat span { color: var(--muted); font-size: 12px; }
.hint {
position: absolute;
bottom: 14px;
left: 14px;
right: 14px;
color: #cfe9ff;
background: rgba(7, 12, 28, 0.72);
border: 1px solid rgba(125, 211, 252, 0.18);
border-radius: 14px;
padding: 10px 12px;
font-size: 13px;
line-height: 1.35;
backdrop-filter: blur(8px);
}
.side-card {
padding: 16px;
position: sticky;
top: 12px;
max-height: calc(100vh - 24px);
overflow-y: auto;
}
.controls {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin: 12px 0;
}
button, select {
border: 1px solid var(--line);
background: #263456;
color: var(--text);
border-radius: 12px;
padding: 11px 12px;
font-size: 14px;
}
button { cursor: pointer; font-weight: 700; }
button:hover { background: #314374; }
.primary { background: #0f79a8; border-color: #23a7dc; }
.danger { background: #8c1d3b; border-color: #ef476f; }
.good { background: #137346; border-color: #27c46b; }
h2, h3 { margin: 10px 0 8px; }
.explain, .small {
color: var(--muted);
line-height: 1.45;
font-size: 13px;
}
.scenario-box, .selected-box {
background: rgba(125,211,252,0.08);
border: 1px solid rgba(125,211,252,0.25);
border-radius: 14px;
padding: 12px;
color: #dff7ff;
font-size: 13px;
line-height: 1.45;
margin: 10px 0;
}
.selected-box {
background: rgba(179, 136, 255, 0.08);
border-color: rgba(179, 136, 255, 0.28);
min-height: 118px;
}
.legend {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
margin: 10px 0 14px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
color: var(--muted);
font-size: 13px;
}
.dot {
width: 14px;
height: 14px;
border-radius: 50%;
display: inline-block;
}
.domain-list {
display: grid;
gap: 8px;
margin-top: 8px;
max-height: 240px;
overflow: auto;
padding-right: 4px;
}
.domain {
display: grid;
grid-template-columns: 28px 1fr auto;
gap: 8px;
align-items: center;
background: rgba(255,255,255,0.045);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 12px;
padding: 8px;
font-size: 13px;
}
.domain b { display: block; }
.domain span { color: var(--muted); font-size: 11px; }
.pill {
border-radius: 999px;
padding: 4px 8px;
font-size: 11px;
font-weight: 700;
color: #08111f;
background: var(--muted);
}
.log {
background: #070c1c;
border: 1px solid var(--line);
border-radius: 14px;
padding: 12px;
height: 180px;
overflow: auto;
font-size: 12px;
line-height: 1.45;
color: #d9e6ff;
white-space: pre-wrap;
}
@media (max-width: 1120px) {
body { overflow-x: auto; }
.app {
grid-template-columns: minmax(620px, 1fr) 430px;
min-width: 1060px;
}
.stage-card { min-height: 620px; }
#scene { height: 620px; }
.side-card {
position: sticky;
top: 12px;
max-height: calc(100vh - 24px);
overflow-y: auto;
}
}
@media (max-width: 680px) {
body { overflow-x: auto; }
.app {
grid-template-columns: minmax(620px, 1fr) 430px;
min-width: 1060px;
padding: 10px;
}
.hud { grid-template-columns: repeat(3, 1fr); }
.controls { grid-template-columns: repeat(2, 1fr); }
.stage-card { min-height: 560px; }
#scene { height: 560px; }
}
.hud { grid-template-columns: repeat(3, 1fr); }
.controls { grid-template-columns: 1fr; }
.stage-card { min-height: 560px; }
#scene { height: 560px; }
}
</style>
</head>
<body>
<header>
<h1>Signal Cascade 3D: Wireless Core Fault Simulator</h1>
<p class="subtitle">
A 3D telecom mission-control simulator where ten Wireless Core domains are connected as a live dependency graph. Faults pulse, spread, mitigate, and restore across Subscriber, RAN, Packet Core, Voice, Messaging, E911, Policy, Identity, Operations, and External Partner systems.
</p>
</header>
<main class="app">
<section class="stage-card">
<canvas id="scene"></canvas>
<div class="hud">
<div class="stat"><strong id="turnCount">0</strong><span>Turns</span></div>
<div class="stat"><strong id="healthyCount">0</strong><span>Healthy</span></div>
<div class="stat"><strong id="degradedCount">0</strong><span>Degraded</span></div>
<div class="stat"><strong id="faultedCount">0</strong><span>Faulted</span></div>
<div class="stat"><strong id="riskScore">0</strong><span>Risk</span></div>
<div class="stat"><strong id="statusText">Ready</strong><span>Status</span></div>
</div>
<div class="hint">
Drag to rotate the 3D Wireless Core. Scroll to zoom. Click any node to select a persona and cycle its state. Animated links show dependency paths where faults can cascade.
</div>
</section>
<aside class="side-card">
<h2>Command Controls</h2>
<p class="explain">Load a known Wireless Core incident, introduce a specific fault, inspect evidence, choose a playbook, or let the cascade run. Faults now spread more slowly and playbooks reduce the active incident when the evidence points to the right domain.</p>
<div class="controls">
<button id="playBtn" class="primary">▶ Play</button>
<button id="stepBtn">Step Once</button>
<button id="resetBtn" class="danger">Reset</button>
<button id="randomBtn">Random Set</button>
<button id="inspectBtn" class="good">Inspect Signals</button>
<button id="playbookBtn" class="good">Apply Playbook</button>
</div>
<select id="patternSelect" style="width:100%; margin-bottom:10px;"></select>
<button id="loadPatternBtn" style="width:100%;">Load Known Pattern</button>
<h3>Introduce a Specific Fault</h3>
<select id="faultSelect" style="width:100%; margin-bottom:10px;"></select>
<button id="introduceFaultBtn" class="danger" style="width:100%;">Introduce Selected Fault</button>
<h3>Choose a Playbook</h3>
<select id="playbookSelect" style="width:100%; margin-bottom:10px;"></select>
<div class="controls" style="margin-top:0;">
<button id="applySelectedPlaybookBtn" class="good">Apply Selected Playbook</button>
<button id="hintBtn">Need a Hint?</button>
</div>
<div id="scenarioBox" class="scenario-box">Choose a known pattern or create a random incident.</div>
<div id="selectedBox" class="selected-box">Selected node: Wireless Core center. Click an outer domain to view its persona, function, state, and playbook.</div>
<h3>Legend</h3>
<div class="legend">
<div class="legend-item"><span class="dot" style="background:var(--healthy)"></span> Healthy</div>
<div class="legend-item"><span class="dot" style="background:var(--degraded)"></span> Degraded</div>
<div class="legend-item"><span class="dot" style="background:var(--faulted)"></span> Faulted</div>
<div class="legend-item"><span class="dot" style="background:var(--mitigating)"></span> Mitigating</div>
<div class="legend-item"><span class="dot" style="background:var(--restored)"></span> Restored</div>
<div class="legend-item"><span class="dot" style="background:var(--unknown)"></span> Unknown</div>
</div>
<h3>10 Wireless Core Personas</h3>
<div id="domainList" class="domain-list"></div>
<h3>Event Log</h3>
<div id="log" class="log"></div>
</aside>
</main>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { CSS2DRenderer, CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
const STATES = ["healthy", "degraded", "faulted", "mitigating", "restored", "unknown"];
const stateColors = {
healthy: 0x27c46b,
degraded: 0xffd166,
faulted: 0xef476f,
mitigating: 0x4cc9f0,
restored: 0xb388ff,
unknown: 0x80889f
};
const domains = [
{ id: 0, abbr: "SUB", emoji: "📱", name: "Ava Connect", fn: "Subscriber / Device Requests", playbook: "Subscriber validation and provisioning check" },
{ id: 1, abbr: "RAN", emoji: "📡", name: "Rafi RAN", fn: "Radio Access Network", playbook: "Coverage, handoff, congestion, and RAN transport check" },
{ id: 2, abbr: "PKT", emoji: "🌐", name: "Pax Packet", fn: "Packet Core Sessions", playbook: "Attach, session, AMF, SMF, UPF validation" },
{ id: 3, abbr: "VOC", emoji: "☎️", name: "Vera Voice", fn: "Voice Core / IMS", playbook: "IMS, SIP, SBC, VoLTE/VoNR troubleshooting" },
{ id: 4, abbr: "MSG", emoji: "💬", name: "Milo Message", fn: "Messaging", playbook: "SMS/MMS queue, retry, gateway, delivery check" },
{ id: 5, abbr: "911", emoji: "🚨", name: "Elena Emergency", fn: "E911 Emergency Services", playbook: "Immediate emergency routing and location escalation" },
{ id: 6, abbr: "POL", emoji: "⚖️", name: "Priya Policy", fn: "Policy and Charging", playbook: "PCRF/PCF/OCS/CHF policy and charging validation" },
{ id: 7, abbr: "ID", emoji: "🪪", name: "Uri Identity", fn: "Subscriber Identity", playbook: "HSS/UDM/AUSF authentication and profile check" },
{ id: 8, abbr: "OPS", emoji: "📊", name: "Olivia Ops", fn: "Operations Telemetry", playbook: "KPI, alarm, log, dashboard, and change-record validation" },
{ id: 9, abbr: "EXT", emoji: "🔗", name: "Evan Edge", fn: "External Partners / OSS/BSS", playbook: "Roaming, interconnect, OSS/BSS, billing, DNS check" }
];
const dependencyEdges = {
0: [1,2,7,6],
1: [0,2,3],
2: [0,1,3,4,6,7,8],
3: [1,2,5,7,8],
4: [2,6,8,9],
5: [3,8,9],
6: [0,2,4,8],
7: [0,2,3,6],
8: [2,3,4,5,6,9],
9: [4,5,8]
};
// How strongly a fault in one domain affects another domain.
// These weights slow the game down and make cascades feel more like a real telecom incident.
const dependencyWeights = {
"2-0": 0.55, "2-6": 0.52, "2-7": 0.48, "2-3": 0.30, "2-4": 0.25, "2-8": 0.28,
"6-2": 0.58, "6-0": 0.48, "6-4": 0.28, "6-8": 0.22,
"7-0": 0.62, "7-2": 0.55, "7-3": 0.36, "7-6": 0.22,
"1-0": 0.55, "1-2": 0.42, "1-3": 0.36,
"3-5": 0.30, "3-8": 0.22, "3-2": 0.20,
"4-9": 0.28, "4-8": 0.22, "4-2": 0.18,
"5-3": 0.18, "5-8": 0.24, "5-9": 0.20,
"8-2": 0.20, "8-3": 0.20, "8-4": 0.20, "8-5": 0.28, "8-6": 0.20, "8-9": 0.18,
"9-4": 0.42, "9-5": 0.30, "9-8": 0.22, "9-0": 0.18
};
const evidenceChecklist = {
0: "attach/service requests, SIM/profile status, provisioning, APN/DNN settings, roaming eligibility",
1: "cell congestion, coverage, handoff attempts, access rejects, RAN transport alarms",
2: "attach success, AMF/MME alarms, SMF/SGW/PGW errors, UPF health, PDU/session setup, IP allocation",
3: "IMS registration, SIP response codes, SBC alarms, VoLTE/VoNR call setup, dropped calls, one-way audio",
4: "SMSC/MMSC queues, retry rates, gateway errors, delivery receipts, routing failures, backlog depth",
5: "911 call completion, emergency routing, location lookup, PSAP handoff, location accuracy, compliance alarms",
6: "PCRF/PCF latency, policy rule changes, quota checks, charging records, OCS/CHF status, QoS rules",
7: "HSS/UDM/AUSF lookups, auth timeouts, subscriber profile mismatch, SIM validation, replication lag",
8: "KPI feed health, alarm pipeline, log ingestion, dashboard freshness, topology data, tickets and change records",
9: "roaming partner status, interconnect health, OSS/BSS integrations, billing feeds, partner APIs, DNS/routing"
};
const patterns = [
{ name: "1. Packet Core Attach Failure", desc: "Attach success drops. Packet Core is faulted; Subscriber and Identity begin to degrade.", states: {2:"faulted",0:"degraded",7:"degraded",8:"degraded"} },
{ name: "2. Voice / IMS Registration Failure", desc: "Voice registration failures appear. Voice Core is faulted; RAN and Identity show pressure.", states: {3:"faulted",1:"degraded",7:"degraded",8:"degraded"} },
{ name: "3. Messaging Queue Backlog", desc: "SMS/MMS delivery is delayed. Messaging is faulted; External and Packet domains degrade.", states: {4:"faulted",9:"degraded",2:"degraded",8:"degraded"} },
{ name: "4. E911 Location Lookup Failure", desc: "Emergency location lookup is failing. E911 is faulted and requires immediate escalation.", states: {5:"faulted",3:"degraded",8:"degraded",9:"degraded"} },
{ name: "5. PCF / PCRF Policy Timeout", desc: "Policy response latency increases. Policy and Charging is faulted; Packet and Subscriber degrade.", states: {6:"faulted",2:"degraded",0:"degraded",8:"degraded"} },
{ name: "6. UDM / HSS Authentication Failure", desc: "Authentication requests time out. Identity is faulted; Subscriber and Packet Core degrade.", states: {7:"faulted",0:"degraded",2:"degraded",3:"degraded"} },
{ name: "7. RAN Congestion Cascade", desc: "Cell congestion and access rejects create downstream service symptoms.", states: {1:"faulted",0:"degraded",2:"degraded",3:"degraded"} },
{ name: "8. Monitoring Blind Spot", desc: "Telemetry is missing or stale. Fault confidence drops and domains become unknown.", states: {8:"faulted",2:"unknown",3:"unknown",4:"unknown",6:"degraded"} },
{ name: "9. Roaming Partner Outage", desc: "External partner problems affect roaming and message/voice handoff.", states: {9:"faulted",0:"degraded",3:"degraded",4:"degraded",5:"degraded"} },
{ name: "10. Change-Induced Multi-Domain Incident", desc: "A recent change creates policy, packet core, and monitoring symptoms.", states: {6:"faulted",2:"faulted",8:"degraded",0:"degraded",7:"degraded"} }
];
let domainStates = Array(10).fill("healthy");
let domainAges = Array(10).fill(0); // How long a domain has been in its current state.
let evidenceSeen = Array(10).fill(false); // Player inspected or received a hint for this domain.
let turn = 0;
let timer = null;
let selectedId = null;
let pulseTime = 0;
const canvas = document.getElementById('scene');
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x070b18);
const camera = new THREE.PerspectiveCamera(55, 1, 0.1, 1000);
camera.position.set(0, 8, 18);
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
const labelRenderer = new CSS2DRenderer();
labelRenderer.domElement.style.position = 'absolute';
labelRenderer.domElement.style.top = '0';
labelRenderer.domElement.style.pointerEvents = 'none';
document.querySelector('.stage-card').appendChild(labelRenderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.06;
controls.minDistance = 9;
controls.maxDistance = 32;
controls.maxPolarAngle = Math.PI * 0.82;
const ambient = new THREE.AmbientLight(0xffffff, 0.55);
scene.add(ambient);
const keyLight = new THREE.DirectionalLight(0xffffff, 1.1);
keyLight.position.set(8, 12, 10);
scene.add(keyLight);
const rimLight = new THREE.PointLight(0x7dd3fc, 2.2, 40);
rimLight.position.set(-8, 6, -8);
scene.add(rimLight);
const nodeGroup = new THREE.Group();
const edgeGroup = new THREE.Group();
const particleGroup = new THREE.Group();
scene.add(edgeGroup, particleGroup, nodeGroup);
const nodeMeshes = [];
const labelObjects = [];
const edges = [];
const particles = [];
const centerGeo = new THREE.SphereGeometry(1.35, 48, 48);
const centerMat = new THREE.MeshStandardMaterial({ color: 0x7dd3fc, emissive: 0x184c66, roughness: 0.35, metalness: 0.25 });
const centerMesh = new THREE.Mesh(centerGeo, centerMat);
centerMesh.userData = { center: true };
nodeGroup.add(centerMesh);
addLabel(centerMesh, "Wireless Core", "Command Center", 0, 1.75, true);
function makeTextSprite(text, sub, isCenter=false) {
const div = document.createElement('div');
div.style.color = isCenter ? '#e8fbff' : '#eef3ff';
div.style.fontFamily = 'Arial, Helvetica, sans-serif';
div.style.fontSize = isCenter ? '18px' : '13px';
div.style.fontWeight = '700';
div.style.textAlign = 'center';
div.style.textShadow = '0 2px 8px rgba(0,0,0,0.9)';
div.style.padding = '4px 8px';
div.style.borderRadius = '999px';
div.style.background = isCenter ? 'rgba(15,121,168,0.35)' : 'rgba(7,12,28,0.58)';
div.innerHTML = `${text}<br><span style="font-size:11px;font-weight:500;color:#a9b7d9">${sub}</span>`;
return div;
}
function addLabel(mesh, text, sub, x=0, y=1.1, isCenter=false) {
const obj = new CSS2DObject(makeTextSprite(text, sub, isCenter));
obj.position.set(x, y, 0);
mesh.add(obj);
labelObjects.push(obj);
}
function createNodes() {
const radius = 7.2;
const verticalWave = [0, 1.1, -0.4, 0.9, -1.0, 1.6, -0.8, 0.8, -1.5, 0.2];
domains.forEach((d, i) => {
const angle = (i / domains.length) * Math.PI * 2 - Math.PI / 2;
const x = Math.cos(angle) * radius;
const z = Math.sin(angle) * radius;
const y = verticalWave[i];
const geo = new THREE.SphereGeometry(0.82, 40, 40);
const mat = new THREE.MeshStandardMaterial({
color: stateColors.healthy,
emissive: 0x082214,
roughness: 0.34,
metalness: 0.18
});
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(x, y, z);
mesh.userData = { id: i };
nodeGroup.add(mesh);
nodeMeshes[i] = mesh;
addLabel(mesh, `${d.emoji} ${d.name}`, d.fn);
const coreLine = createLine(new THREE.Vector3(0,0,0), mesh.position, 0x3d5f92, 0.45);
edgeGroup.add(coreLine);
edges.push({ line: coreLine, from: null, to: i, core: true });
});
const created = new Set();
Object.entries(dependencyEdges).forEach(([from, tos]) => {
tos.forEach(to => {
const key = [Number(from), to].sort((a,b) => a-b).join('-');
if (!created.has(key)) {
created.add(key);
const line = createLine(nodeMeshes[from].position, nodeMeshes[to].position, 0x5b6f9f, 0.30);
edgeGroup.add(line);
edges.push({ line, from: Number(from), to, core: false });
const particle = createParticle(nodeMeshes[from].position, nodeMeshes[to].position);
particleGroup.add(particle.mesh);
particles.push(particle);
}
});
});
}
function createLine(start, end, color, opacity) {
const geometry = new THREE.BufferGeometry().setFromPoints([start.clone(), end.clone()]);
const material = new THREE.LineBasicMaterial({ color, transparent: true, opacity });
return new THREE.Line(geometry, material);
}
function createParticle(start, end) {
const geo = new THREE.SphereGeometry(0.065, 12, 12);
const mat = new THREE.MeshBasicMaterial({ color: 0x7dd3fc, transparent: true, opacity: 0.72 });
const mesh = new THREE.Mesh(geo, mat);
return { mesh, start: start.clone(), end: end.clone(), t: Math.random(), speed: 0.003 + Math.random() * 0.006 };
}
function addStarField() {
const geo = new THREE.BufferGeometry();
const points = [];
for (let i=0; i<800; i++) {
points.push((Math.random()-0.5)*80, (Math.random()-0.5)*45, (Math.random()-0.5)*80);
}
geo.setAttribute('position', new THREE.Float32BufferAttribute(points, 3));
const mat = new THREE.PointsMaterial({ color: 0x7dd3fc, size: 0.035, transparent: true, opacity: 0.38 });
scene.add(new THREE.Points(geo, mat));
}
createNodes();
addStarField();
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
canvas.addEventListener('click', (event) => {
const rect = canvas.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(nodeMeshes, false);
if (intersects.length > 0) {
const id = intersects[0].object.userData.id;
selectedId = id;
cycleState(id);
updateSelectedBox(id);
}
});
function resize() {
const rect = canvas.parentElement.getBoundingClientRect();
const w = rect.width;
const h = canvas.clientHeight || 720;
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderer.setSize(w, h, false);
labelRenderer.setSize(w, h);
}
window.addEventListener('resize', resize);
resize();
function updateNodeMaterials() {
nodeMeshes.forEach((mesh, i) => {
const state = domainStates[i];
mesh.material.color.setHex(stateColors[state]);
const c = new THREE.Color(stateColors[state]);
mesh.material.emissive.copy(c).multiplyScalar(state === 'faulted' ? 0.55 : state === 'degraded' ? 0.28 : 0.18);
});
edges.forEach(e => {
if (e.core) {
e.line.material.color.setHex(0x3d5f92);
e.line.material.opacity = 0.34;
return;
}
const a = domainStates[e.from];
const b = domainStates[e.to];
if (a === 'faulted' || b === 'faulted') {
e.line.material.color.setHex(0xef476f);
e.line.material.opacity = 0.82;
} else if (a === 'degraded' || b === 'degraded') {
e.line.material.color.setHex(0xffd166);
e.line.material.opacity = 0.58;
} else if (a === 'mitigating' || b === 'mitigating') {
e.line.material.color.setHex(0x4cc9f0);
e.line.material.opacity = 0.75;
} else {
e.line.material.color.setHex(0x5b6f9f);
e.line.material.opacity = 0.26;
}
});
}
function animate() {
requestAnimationFrame(animate);
pulseTime += 0.018;
nodeGroup.rotation.y += 0.0009;
edgeGroup.rotation.y = nodeGroup.rotation.y;
particleGroup.rotation.y = nodeGroup.rotation.y;
nodeMeshes.forEach((mesh, i) => {
const state = domainStates[i];
const pulse = state === 'faulted' ? 1 + Math.sin(pulseTime * 8) * 0.16 :
state === 'degraded' ? 1 + Math.sin(pulseTime * 5) * 0.08 :
state === 'mitigating' ? 1 + Math.sin(pulseTime * 6) * 0.11 : 1;
const selectedBoost = selectedId === i ? 1.18 : 1;
mesh.scale.setScalar(pulse * selectedBoost);
});
centerMesh.scale.setScalar(1 + Math.sin(pulseTime * 2) * 0.035);
particles.forEach(p => {
p.t += p.speed;
if (p.t > 1) p.t = 0;
p.mesh.position.lerpVectors(p.start, p.end, p.t);
});
controls.update();
renderer.render(scene, camera);
labelRenderer.render(scene, camera);
}
animate();
function stateWeight(state) {
return { healthy: 0, restored: 0, mitigating: 1, unknown: 1, degraded: 2, faulted: 4 }[state] ?? 0;
}
function statusLabel() {
const faults = domainStates.filter(s => s === "faulted").length;
const degraded = domainStates.filter(s => s === "degraded").length;
if (domainStates[5] === "faulted") return "E911 CRITICAL";
if (faults >= 4) return "Major Incident";
if (faults > 0) return "Active Fault";
if (degraded > 0) return "Degraded";
return "Stable";
}
function renderUI() {
const counts = Object.fromEntries(STATES.map(s => [s, domainStates.filter(x => x === s).length]));
document.getElementById("turnCount").textContent = turn;
document.getElementById("healthyCount").textContent = counts.healthy + counts.restored;
document.getElementById("degradedCount").textContent = counts.degraded;
document.getElementById("faultedCount").textContent = counts.faulted;
document.getElementById("riskScore").textContent = domainStates.reduce((a, s) => a + stateWeight(s), 0);
document.getElementById("statusText").textContent = statusLabel();
renderDomains();
updateNodeMaterials();
if (selectedId !== null) updateSelectedBox(selectedId);
}
function renderDomains() {
const list = document.getElementById("domainList");
list.innerHTML = "";
domains.forEach((d, i) => {
const item = document.createElement("div");
item.className = "domain";
item.innerHTML = `<div>${d.emoji}</div><div><b>${d.name}</b><span>${d.fn}</span></div><div class="pill">${domainStates[i]}</div>`;
item.addEventListener('click', () => { selectedId = i; updateSelectedBox(i); });
list.appendChild(item);
});
}
function updateSelectedBox(id) {
const d = domains[id];
const neighborNames = (dependencyEdges[id] || []).map(n => domains[n].abbr).join(', ');
document.getElementById('selectedBox').innerHTML = `
<b>${d.emoji} ${d.name}</b><br>
<span style="color:#a9b7d9">${d.fn}</span><br><br>
<b>State:</b> ${domainStates[id]}<br>
<b>Connected domains:</b> ${neighborNames}<br>
<b>Playbook:</b> ${d.playbook}
`;
}
function cycleState(id) {
const idx = STATES.indexOf(domainStates[id]);
domainStates[id] = STATES[(idx + 1) % STATES.length];
log(`${domains[id].name} changed to ${domainStates[id]}.`);
renderUI();
}
function resetGame() {
turn = 0;
domainStates = Array(10).fill("healthy");
domainAges = Array(10).fill(0);
evidenceSeen = Array(10).fill(false);
selectedId = null;
document.getElementById('scenarioBox').textContent = "Board reset. Choose a known pattern, introduce a specific fault, or create a random incident.";
document.getElementById('selectedBox').textContent = "Selected node: Wireless Core center. Click an outer domain to view its persona, function, state, and playbook.";
document.getElementById('log').textContent = "";
stopPlay();
log("Reset complete. All Wireless Core domains are healthy.");
renderUI();
}
function setState(id, newState) {
if (domainStates[id] !== newState) {
domainStates[id] = newState;
domainAges[id] = 0;
}
}
function randomSet() {
turn = 0;
domainStates = domainStates.map(() => {
const x = Math.random();
if (x < 0.62) return "healthy";
if (x < 0.82) return "degraded";
if (x < 0.90) return "faulted";
if (x < 0.96) return "unknown";
return "restored";
});
domainAges = Array(10).fill(0);
evidenceSeen = Array(10).fill(false);
document.getElementById('scenarioBox').textContent = "Random network condition created. Use Inspect Signals before choosing a playbook. Fault spread is gradual unless ignored.";
log("Random incident set created.");
renderUI();
}
function loadPattern(index) {
turn = 0;
domainStates = Array(10).fill("healthy");
domainAges = Array(10).fill(0);
evidenceSeen = Array(10).fill(false);
const p = patterns[index];
Object.entries(p.states).forEach(([id, state]) => domainStates[Number(id)] = state);
document.getElementById('scenarioBox').textContent = p.desc;
log(`Known pattern loaded: ${p.name}`);
log("Tip: inspect signals or ask for a hint before choosing a playbook. The correct playbook targets the highest-risk active domain, not always the first symptom.");
renderUI();
}
function spreadProbability(from, to) {
const key1 = `${from}-${to}`;
const key2 = `${to}-${from}`;
return dependencyWeights[key1] ?? dependencyWeights[key2] ?? 0.18;
}
function step() {
turn++;
const next = [...domainStates];
const nextAges = domainAges.map(a => a + 1);
// Slow, probabilistic telecom cascade: faults do not instantly infect every neighbor.
for (let i = 0; i < domains.length; i++) {
const state = domainStates[i];
const neighbors = dependencyEdges[i] || [];
const faultedNeighbors = neighbors.filter(n => domainStates[n] === "faulted");
const degradedNeighbors = neighbors.filter(n => domainStates[n] === "degraded");
if (state === "healthy") {
let pressure = 0;
faultedNeighbors.forEach(n => pressure += spreadProbability(n, i) * 0.35);
degradedNeighbors.forEach(n => pressure += spreadProbability(n, i) * 0.10);
if (Math.random() < Math.min(0.28, pressure)) {
next[i] = "degraded";
nextAges[i] = 0;
log(`${domains[i].name} became degraded due to dependency pressure.`);
}
}
if (state === "degraded") {
let escalation = 0.035 + (domainAges[i] * 0.015);
faultedNeighbors.forEach(n => escalation += spreadProbability(n, i) * 0.12);
if (Math.random() < Math.min(0.24, escalation)) {
next[i] = "faulted";
nextAges[i] = 0;
log(`${domains[i].name} escalated from degraded to faulted.`);
} else if (faultedNeighbors.length === 0 && degradedNeighbors.length === 0 && domainAges[i] >= 2 && Math.random() < 0.18) {
next[i] = "healthy";
nextAges[i] = 0;
log(`${domains[i].name} stabilized naturally after degradation.`);
}
}
if (state === "faulted") {
// Faults stay faulted until playbook action, but non-E911 can sometimes reduce to degraded.
const chance = i === 5 ? 0.01 : 0.035;
if (domainAges[i] >= 4 && Math.random() < chance) {
next[i] = "degraded";
nextAges[i] = 0;
log(`${domains[i].name} partially stabilized but still requires validation.`);
}
}
if (state === "mitigating") {
if (domainAges[i] >= 1) {
next[i] = Math.random() < 0.82 ? "restored" : "degraded";
nextAges[i] = 0;
log(`${domains[i].name} moved from mitigating to ${next[i]}.`);
}
}
if (state === "restored") {
if (domainAges[i] >= 2 && Math.random() < 0.62) {
next[i] = "healthy";
nextAges[i] = 0;
log(`${domains[i].name} returned to healthy after observation.`);
}
}
if (state === "unknown") {
if (domainStates[8] !== "faulted" && domainAges[i] >= 1 && Math.random() < 0.45) {
next[i] = "degraded";
nextAges[i] = 0;
log(`${domains[i].name} unknown state resolved into degraded evidence.`);
}
}
}
// Telemetry fault creates blind spots, but not too aggressively.
if (domainStates[8] === "faulted" && domainAges[8] >= 1) {
[2,3,4,6].forEach(id => {
if (Math.random() < 0.08 && next[id] !== "faulted") {
next[id] = "unknown";
nextAges[id] = 0;
log(`${domains[id].name} visibility became unknown because telemetry is faulted.`);
}
});
}
domainStates = next;
domainAges = nextAges;
log(`Turn ${turn}: realistic cascade rules applied. Status: ${statusLabel()}.`);
if (domainStates[5] === "faulted") log("E911 critical: immediate validation and escalation required.");
renderUI();
}
function inspectSignals() {
const active = domains.map((d, i) => ({...d, state: domainStates[i], id: i})).filter(d => d.state !== "healthy");
if (active.length === 0) {
selectedId = null;
document.getElementById('scenarioBox').textContent = "Inspection result: all domains appear healthy. No playbook is recommended right now.";
document.getElementById('selectedBox').textContent = "Inspection result: no active degraded, faulted, or unknown domain was found.";
log("Inspection: all domains appear healthy. Continue monitoring KPIs, alarms, logs, and tickets.");
renderUI();
return;
}
// Mark evidence as gathered for every active domain.
active.forEach(d => evidenceSeen[d.id] = true);
// Pick the highest-priority active fault and visibly select it.
const target = getHighestPriorityActiveFault();
selectedId = target;
document.getElementById('playbookSelect').value = String(target);
const targetDomain = domains[target];
const activeSummary = active.map(d => `${d.abbr}:${d.state}`).join(' | ');
log("Inspection Summary:");
active.forEach(d => {
log(`- ${d.name} (${d.fn}) is ${d.state}. Evidence to check: ${evidenceChecklist[d.id]}.`);
});
log(`Inspection recommendation: select the ${targetDomain.fn} playbook for ${targetDomain.name}.`);
document.getElementById('scenarioBox').innerHTML = `
<b>Inspection Result:</b> ${active.length} active signal domain(s) found.<br>
<b>Active states:</b> ${activeSummary}<br>
<b>Recommended playbook:</b> ${targetDomain.name}${targetDomain.fn}<br>
<b>Why:</b> This is the highest-priority active fault based on E911-first, then Packet Core, Policy, Identity, Voice, Messaging, RAN, Subscriber, Telemetry, and External dependency priority.
`;
document.getElementById('selectedBox').innerHTML = `
<b>🔎 Inspected Domain: ${targetDomain.emoji} ${targetDomain.name}</b><br>
<span style="color:#a9b7d9">${targetDomain.fn}</span><br><br>
<b>Current State:</b> ${domainStates[target]}<br>
<b>Evidence to check:</b> ${evidenceChecklist[target]}<br>
<b>Recommended Playbook:</b> ${targetDomain.playbook}<br>
<b>Next Step:</b> Click “Apply Selected Playbook.”
`;
renderUI();
}
function introduceFault() {
const id = Number(document.getElementById('faultSelect').value);
const d = domains[id];
setState(id, "faulted");
evidenceSeen[id] = true;
// Create light first-order symptoms only; the cascade engine will handle additional spread.
const neighbors = dependencyEdges[id] || [];
neighbors.forEach(n => {
if (domainStates[n] === "healthy" && Math.random() < 0.22) setState(n, "degraded");
});
selectedId = id;
document.getElementById('scenarioBox').textContent = `Manual fault introduced: ${d.fn}. Use hints or select the correct playbook to mitigate it.`;
log(`Manual fault introduced in ${d.name} (${d.fn}). Neighboring dependent domains may degrade.`);
renderUI();
}
function getHighestPriorityActiveFault() {
// Prefer explicit faulted domains, then degraded domains, then unknown domains.
// E911 is always first if active; then core dependency domains; then supporting domains.
const priority = [5,2,6,7,3,4,1,0,8,9];
const faulted = priority.find(id => domainStates[id] === "faulted");
if (faulted !== undefined) return faulted;
const degraded = priority.find(id => domainStates[id] === "degraded");
if (degraded !== undefined) return degraded;
return priority.find(id => domainStates[id] === "unknown");
}
function provideHint() {
const target = getHighestPriorityActiveFault();
if (target === undefined) {
log("Hint: no active degraded, faulted, or unknown domain exists. Keep monitoring.");
document.getElementById('scenarioBox').textContent = "Hint: all domains are healthy or restored. No playbook is needed right now.";
return;
}
const d = domains[target];
const reasonMap = {
0: "Subscriber/device request issues often start with provisioning, SIM/profile validation, roaming eligibility, or APN/DNN configuration.",
1: "RAN issues can create downstream symptoms before the Wireless Core sees clean service requests, so coverage, handoff, congestion, and access rejects should be checked first.",
2: "Packet Core is central to mobile data sessions. Attach, registration, AMF, SMF, UPF, session setup, and IP allocation failures can cascade to multiple services.",
3: "Voice issues commonly involve IMS registration, SIP signaling, SBC behavior, call setup, codec negotiation, or VoLTE/VoNR transport.",
4: "Messaging delays or failures often come from SMSC/MMSC queues, gateway timeouts, routing failures, retry storms, or delivery receipt issues.",
5: "E911 affects emergency routing and location lookup, so it should be handled as the highest-priority playbook with immediate escalation.",
6: "Policy and Charging faults can block or degrade Packet Core sessions through PCRF/PCF timeouts, quota checks, service rules, or charging failures.",
7: "Identity faults can prevent subscribers from authenticating, registering, roaming, or receiving authorized services through HSS, UDM, or AUSF failures.",
8: "Operations Telemetry faults reduce visibility. Bad KPIs, stale dashboards, missing alarms, or log ingestion failures can hide the real incident.",
9: "External Partner or OSS/BSS issues can affect roaming, interconnects, billing feeds, partner APIs, enterprise services, or DNS dependencies."
};
evidenceSeen[target] = true;
const hintText = `Hint: choose the ${d.fn} playbook for ${d.name}. Why: ${reasonMap[target]} Evidence to check: ${evidenceChecklist[target]}.`;
document.getElementById('scenarioBox').textContent = hintText;
log(hintText);
selectedId = target;
updateSelectedBox(target);
renderUI();
}
function applySelectedPlaybook() {
const selected = Number(document.getElementById('playbookSelect').value);
const target = getHighestPriorityActiveFault();
const d = domains[selected];
if (target === undefined) {
log(`Playbook selected: ${d.fn}. No active fault requires mitigation.`);
return;
}
const targetDomain = domains[target];
selectedId = selected;
const directMatch = selected === target;
const connectedMatch = (dependencyEdges[target] || []).includes(selected) && evidenceSeen[target];
if (directMatch || connectedMatch) {
const solvedId = directMatch ? selected : target;
const solvedDomain = domains[solvedId];
if (domainStates[solvedId] === "faulted") setState(solvedId, "mitigating");
else if (["degraded", "unknown"].includes(domainStates[solvedId])) setState(solvedId, "restored");
// A correct playbook also relieves nearby symptoms.
(dependencyEdges[solvedId] || []).forEach(n => {
if (domainStates[n] === "degraded" && Math.random() < 0.70) setState(n, "restored");
if (domainStates[n] === "unknown" && Math.random() < 0.55) setState(n, "degraded");
});
log(`Correct playbook: ${d.name} (${d.fn}). Active incident target: ${solvedDomain.name} (${solvedDomain.fn}).`);
log(`Why it worked: the playbook addressed the highest-priority active fault path and checked the right evidence: ${evidenceChecklist[solvedId]}.`);
document.getElementById('scenarioBox').textContent = `Playbook applied successfully. ${solvedDomain.name} is now ${domainStates[solvedId]}, and nearby symptoms may begin restoring.`;
if (solvedId === 5) log("E911 note: continue immediate escalation, validation, documentation, and compliance review even after mitigation starts.");
} else {
// Wrong playbook: small penalty, not catastrophic.
turn++;
domainAges[target] += 1;
if (domainStates[target] === "degraded" && domainAges[target] >= 2 && Math.random() < 0.35) setState(target, "faulted");
const neighbor = (dependencyEdges[target] || []).find(n => domainStates[n] === "healthy");
if (neighbor !== undefined && Math.random() < 0.25) setState(neighbor, "degraded");
log(`Wrong playbook: ${d.name} (${d.fn}) was selected, but current evidence points to ${targetDomain.name} (${targetDomain.fn}).`);
log(`Why it missed: it does not address the primary failing evidence path: ${evidenceChecklist[target]}. Use Need a Hint or Inspect Signals.`);
document.getElementById('scenarioBox').textContent = `That playbook did not match the main fault. Current evidence points to ${targetDomain.name}${targetDomain.fn}.`;
}
renderUI();
}
function applyPlaybook() {
const target = getHighestPriorityActiveFault();
if (target === undefined) {
log("Auto Playbook: no degraded or faulted domain found. No action needed.");
return;
}
document.getElementById('playbookSelect').value = String(target);
applySelectedPlaybook();
}
function playPause() {
if (timer) stopPlay();
else {
timer = setInterval(step, 1000);
document.getElementById("playBtn").textContent = "⏸ Pause";
log("Play started. Fault cascade advancing every second.");
}
}
function stopPlay() {
if (timer) clearInterval(timer);
timer = null;
document.getElementById("playBtn").textContent = "▶ Play";
}
function log(message) {
const logEl = document.getElementById('log');
const stamp = new Date().toLocaleTimeString();
logEl.textContent += `[${stamp}] ${message}\n`;
logEl.scrollTop = logEl.scrollHeight;
}
function setupPatterns() {
const patternSelect = document.getElementById('patternSelect');
patterns.forEach((p, i) => {
const opt = document.createElement("option");
opt.value = i;
opt.textContent = p.name;
patternSelect.appendChild(opt);
});
}
function setupFaultAndPlaybookSelectors() {
const faultSelect = document.getElementById('faultSelect');
const playbookSelect = document.getElementById('playbookSelect');
domains.forEach((d, i) => {
const faultOpt = document.createElement('option');
faultOpt.value = i;
faultOpt.textContent = `${d.name}${d.fn} Fault`;
faultSelect.appendChild(faultOpt);
const playbookOpt = document.createElement('option');
playbookOpt.value = i;
playbookOpt.textContent = `${d.name}${d.fn} Playbook`;
playbookSelect.appendChild(playbookOpt);
});
}
document.getElementById("playBtn").addEventListener("click", playPause);
document.getElementById("stepBtn").addEventListener("click", step);
document.getElementById("resetBtn").addEventListener("click", resetGame);
document.getElementById("randomBtn").addEventListener("click", randomSet);
document.getElementById("inspectBtn").addEventListener("click", inspectSignals);
document.getElementById("playbookBtn").addEventListener("click", applyPlaybook);
document.getElementById("applySelectedPlaybookBtn").addEventListener("click", applySelectedPlaybook);
document.getElementById("hintBtn").addEventListener("click", provideHint);
document.getElementById("introduceFaultBtn").addEventListener("click", introduceFault);
document.getElementById("loadPatternBtn").addEventListener("click", () => loadPattern(Number(document.getElementById('patternSelect').value)));
setupPatterns();
setupFaultAndPlaybookSelectors();
resetGame();
</script>
</body>
</html>