Sync network redesign repo state and cutover artifacts
This commit is contained in:
642
home/doris-dashboard/public/network-redesign.html
Normal file
642
home/doris-dashboard/public/network-redesign.html
Normal file
@@ -0,0 +1,642 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Doris Network Redesign Plan</title>
|
||||
<style>
|
||||
:root{
|
||||
color-scheme:dark;
|
||||
--hf-red:#d53600;
|
||||
--hf-red-deep:#910f3f;
|
||||
--hf-amber:#f2c14e;
|
||||
--hf-blue:#7b8fb2;
|
||||
--bg:#090b10;
|
||||
--bg-accent:#131824;
|
||||
--panel:#11151d;
|
||||
--panel-soft:#171d27;
|
||||
--panel-raise:#1d2430;
|
||||
--text:#f2efdd;
|
||||
--muted:#cbbda7;
|
||||
--line:rgba(242,239,221,.11);
|
||||
--line-soft:rgba(242,239,221,.06);
|
||||
--accent:var(--hf-red);
|
||||
--warn:#ff7b72;
|
||||
--ok:#82c29a;
|
||||
--attention:#ffd166;
|
||||
--shadow:0 20px 48px rgba(0,0,0,.34);
|
||||
--radius:22px;
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
html{scroll-behavior:smooth}
|
||||
body{
|
||||
margin:0;
|
||||
font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
|
||||
background:
|
||||
radial-gradient(circle at top left,rgba(213,54,0,.22),transparent 24rem),
|
||||
radial-gradient(circle at top right,rgba(123,143,178,.16),transparent 22rem),
|
||||
linear-gradient(180deg,#07090d 0%,#0d1017 52%,#131824 100%);
|
||||
color:var(--text);
|
||||
}
|
||||
a{color:inherit}
|
||||
.incident-tape{height:14px;background:repeating-linear-gradient(135deg,var(--hf-amber) 0 18px,#111 18px 36px);box-shadow:0 3px 14px rgba(0,0,0,.32)}
|
||||
main{max-width:1400px;margin:0 auto;padding:20px}
|
||||
.hero,.panel,.vlan-card,.ssid-card,.phase-card,.matrix-card,.legend-card,.topology,.coverage-card{
|
||||
background:rgba(17,21,29,.92);
|
||||
border:1px solid var(--line);
|
||||
box-shadow:var(--shadow);
|
||||
border-radius:var(--radius);
|
||||
}
|
||||
.hero{
|
||||
padding:22px;
|
||||
display:grid;
|
||||
grid-template-columns:1.4fr .9fr;
|
||||
gap:16px;
|
||||
position:relative;
|
||||
overflow:hidden;
|
||||
}
|
||||
.hero:before{
|
||||
content:"";
|
||||
position:absolute;
|
||||
inset:0;
|
||||
background:
|
||||
radial-gradient(circle at 82% 18%, rgba(213,54,0,.18), transparent 18rem),
|
||||
radial-gradient(circle at 14% 10%, rgba(123,143,178,.16), transparent 16rem),
|
||||
repeating-linear-gradient(0deg, rgba(255,255,255,.018) 0 2px, transparent 2px 5px);
|
||||
pointer-events:none;
|
||||
opacity:.5;
|
||||
}
|
||||
.eyebrow{margin:0 0 8px;color:var(--hf-amber);text-transform:uppercase;letter-spacing:.18em;font-size:.74rem}
|
||||
h1{margin:0;font-size:clamp(2.3rem,5vw,4.1rem);line-height:.95;letter-spacing:-.04em}
|
||||
.sub{margin:12px 0 0;color:var(--muted);max-width:68ch;line-height:1.5}
|
||||
.hero-actions,.filters,.quick-links{display:flex;flex-wrap:wrap;gap:10px}
|
||||
.pill,button.filter,button.toggle{
|
||||
border:1px solid rgba(242,193,78,.26);
|
||||
background:rgba(242,193,78,.12);
|
||||
color:#ffe9b3;
|
||||
border-radius:999px;
|
||||
padding:8px 12px;
|
||||
font-size:.83rem;
|
||||
}
|
||||
button.filter,button.toggle{cursor:pointer}
|
||||
button.filter.active,button.toggle.active{background:rgba(213,54,0,.2);border-color:rgba(213,54,0,.45);color:#fff1e8}
|
||||
.hero-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin-top:16px}
|
||||
.hero-stat{background:rgba(15,18,25,.9);border:1px solid rgba(242,239,221,.08);border-radius:18px;padding:14px}
|
||||
.hero-stat strong{display:block;font-size:1.35rem}
|
||||
.hero-stat small{display:block;color:var(--muted);margin-top:4px;line-height:1.35}
|
||||
.stack{display:grid;gap:16px;margin-top:16px}
|
||||
.panel{padding:18px}
|
||||
.panel h2{margin:0 0 12px;font-size:1.25rem}
|
||||
.section-head{display:flex;justify-content:space-between;align-items:flex-end;gap:12px;flex-wrap:wrap}
|
||||
.section-head p{margin:0;color:var(--muted)}
|
||||
.topology{padding:18px}
|
||||
.topology-grid{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:12px;margin-top:14px;align-items:stretch}
|
||||
.node{background:linear-gradient(180deg,rgba(255,255,255,.04),rgba(255,255,255,.015)),rgba(15,18,25,.96);border:1px solid rgba(242,239,221,.1);border-radius:18px;padding:14px;min-height:120px;position:relative}
|
||||
.node small{display:block;color:var(--muted);margin-top:6px;line-height:1.35}
|
||||
.node .tag{display:inline-block;margin-top:10px;padding:4px 8px;border-radius:999px;font-size:.72rem;border:1px solid var(--line)}
|
||||
.node.current{outline:1px solid rgba(255,123,114,.3)}
|
||||
.node.target{outline:1px solid rgba(130,194,154,.32)}
|
||||
.arrow{display:flex;align-items:center;justify-content:center;color:var(--hf-amber);font-size:1.35rem}
|
||||
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:14px}
|
||||
.vlan-card,.ssid-card,.phase-card,.matrix-card,.legend-card,.coverage-card{padding:18px;position:relative}
|
||||
.vlan-card h3,.ssid-card h3,.phase-card h3,.matrix-card h3,.coverage-card h3{margin:0 0 8px;font-size:1.1rem}
|
||||
.meta{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px}
|
||||
.chip{padding:5px 9px;border-radius:999px;font-size:.76rem;background:rgba(123,143,178,.15);border:1px solid rgba(123,143,178,.24)}
|
||||
.chip.warn{background:rgba(255,123,114,.14);border-color:rgba(255,123,114,.28)}
|
||||
.chip.ok{background:rgba(130,194,154,.15);border-color:rgba(130,194,154,.28)}
|
||||
.chip.attention{background:rgba(242,193,78,.14);border-color:rgba(242,193,78,.28)}
|
||||
ul{margin:10px 0 0 18px;padding:0}
|
||||
li{margin:5px 0;color:#e4ddcf}
|
||||
.muted{color:var(--muted)}
|
||||
details{margin-top:12px;border-top:1px solid var(--line-soft);padding-top:10px}
|
||||
summary{cursor:pointer;color:#ffe9b3}
|
||||
.hidden-advanced .advanced{display:none !important}
|
||||
.filter-hide{display:none !important}
|
||||
.matrix{display:grid;grid-template-columns:1.2fr repeat(4,minmax(0,1fr));gap:1px;background:var(--line);border-radius:16px;overflow:hidden;margin-top:12px}
|
||||
.cell{background:rgba(15,18,25,.96);padding:12px;font-size:.92rem;line-height:1.35}
|
||||
.cell.head{background:rgba(123,143,178,.16);font-weight:700}
|
||||
.coverage-layout{display:grid;grid-template-columns:1.1fr .9fr;gap:14px}
|
||||
.floor-map{background:rgba(15,18,25,.96);border:1px solid var(--line-soft);border-radius:18px;padding:16px;min-height:280px;position:relative;overflow:hidden}
|
||||
.floor{position:absolute;border:1px dashed rgba(242,239,221,.18);border-radius:18px;padding:10px;color:var(--muted)}
|
||||
.floor.upstairs{left:20px;right:20px;top:20px;height:110px}
|
||||
.floor.downstairs{left:20px;right:20px;bottom:20px;height:110px}
|
||||
.ap{position:absolute;width:56px;height:56px;border-radius:50%;display:grid;place-items:center;font-weight:800;background:rgba(123,143,178,.22);border:1px solid rgba(123,143,178,.45);box-shadow:0 0 0 1px rgba(123,143,178,.1),0 0 45px rgba(123,143,178,.18)}
|
||||
.ap.u7{top:48px;right:80px}
|
||||
.ap.u6{bottom:52px;left:90px;background:rgba(242,193,78,.18);border-color:rgba(242,193,78,.45);box-shadow:0 0 0 1px rgba(242,193,78,.1),0 0 45px rgba(242,193,78,.18)}
|
||||
.ap.off{opacity:.25;filter:grayscale(1)}
|
||||
.coverage-zone{position:absolute;border-radius:999px;filter:blur(2px);opacity:.32}
|
||||
.zone-u7{background:radial-gradient(circle, rgba(123,143,178,.82), transparent 70%);width:260px;height:180px;top:8px;right:8px}
|
||||
.zone-u6{background:radial-gradient(circle, rgba(242,193,78,.82), transparent 70%);width:260px;height:180px;bottom:6px;left:8px}
|
||||
.footer-note{margin-top:18px;color:var(--muted);font-size:.92rem}
|
||||
.nav{display:flex;gap:10px;flex-wrap:wrap;margin:14px 0 0}
|
||||
.nav a{padding:9px 12px;border-radius:999px;background:rgba(255,255,255,.04);border:1px solid var(--line);text-decoration:none}
|
||||
@media (max-width: 1080px){
|
||||
.hero,.coverage-layout{grid-template-columns:1fr}
|
||||
.topology-grid{grid-template-columns:repeat(3,minmax(0,1fr))}
|
||||
}
|
||||
@media (max-width: 760px){
|
||||
main{padding:14px}
|
||||
.hero-grid,.cards,.topology-grid{grid-template-columns:1fr}
|
||||
.matrix{grid-template-columns:1fr}
|
||||
.arrow{display:none}
|
||||
.floor-map{min-height:340px}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="hidden-advanced">
|
||||
<div class="incident-tape"></div>
|
||||
<main>
|
||||
<section class="hero">
|
||||
<div style="position:relative;z-index:1">
|
||||
<p class="eyebrow">Operator evidence board</p>
|
||||
<h1>Home Network Redesign</h1>
|
||||
<p class="sub">This is the proposed future-state design for your house and homelab: six purposeful VLANs, a real camera/security lane now instead of later, cleaner SSIDs, stricter policy boundaries, and a staged migration path that avoids lighting the place on fire.</p>
|
||||
<div class="nav">
|
||||
<a href="#vlan-plan">VLAN plan</a>
|
||||
<a href="#ssid-plan">SSID plan</a>
|
||||
<a href="#firewall">Firewall</a>
|
||||
<a href="#migration">Migration</a>
|
||||
<a href="#coverage">Wi-Fi coverage</a>
|
||||
</div>
|
||||
<div class="hero-grid">
|
||||
<div class="hero-stat">
|
||||
<strong>6 target VLANs</strong>
|
||||
<small>Management, Trusted, Servers, IoT, Guest, Cameras/Security. No more historical junk lanes.</small>
|
||||
</div>
|
||||
<div class="hero-stat">
|
||||
<strong>1 legacy VLAN to kill</strong>
|
||||
<small>Old IoT / VLAN 2 goes away after device migration and validation.</small>
|
||||
</div>
|
||||
<div class="hero-stat">
|
||||
<strong>2 AP posture</strong>
|
||||
<small>Keep U7 Pro as main AP. Reinstall the U6 LR downstairs if coverage or camera RSSI needs it.</small>
|
||||
</div>
|
||||
<div class="hero-stat">
|
||||
<strong>Camera growth ready now</strong>
|
||||
<small>Doorbell, chimes, and future camera gear live on a security lane from day one.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="position:relative;z-index:1" class="panel">
|
||||
<h2>Interactive controls</h2>
|
||||
<div class="stack" style="margin-top:0">
|
||||
<div>
|
||||
<div class="muted" style="margin-bottom:8px">Card filter</div>
|
||||
<div class="filters">
|
||||
<button class="filter active" data-filter="all">All</button>
|
||||
<button class="filter" data-filter="network">Network</button>
|
||||
<button class="filter" data-filter="wifi">Wi-Fi</button>
|
||||
<button class="filter" data-filter="security">Security</button>
|
||||
<button class="filter" data-filter="migration">Migration</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="muted" style="margin-bottom:8px">Detail level</div>
|
||||
<div class="quick-links">
|
||||
<button class="toggle" id="detail-toggle">Show advanced details</button>
|
||||
<button class="toggle" id="u6-toggle">Enable downstairs U6 LR</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="muted" style="margin-bottom:8px">Operator calls</div>
|
||||
<div class="hero-actions">
|
||||
<span class="pill">Move chimes off Management</span>
|
||||
<span class="pill">Retire VLAN 2</span>
|
||||
<span class="pill">Split Servers from Trusted</span>
|
||||
<span class="pill">Pre-stage Camera VLAN now</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="stack">
|
||||
<section class="topology" id="topology">
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2 style="margin:0">Current to target topology</h2>
|
||||
<p>What exists now versus the lane structure I would put in place before you hand me admin access.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="topology-grid">
|
||||
<div class="node current" data-tags="network migration">
|
||||
<strong>Current Management</strong>
|
||||
<small>UDM, switches, APs, plus two Protect chimes that do not belong there.</small>
|
||||
<span class="tag">10.5.0.0/24</span>
|
||||
</div>
|
||||
<div class="arrow">→</div>
|
||||
<div class="node target" data-tags="network security">
|
||||
<strong>Mgmt VLAN 10</strong>
|
||||
<small>Infra only: gateway, switches, APs, management IPs, admin surfaces.</small>
|
||||
<span class="tag">10.5.10.0/24</span>
|
||||
</div>
|
||||
<div class="node current" data-tags="network wifi">
|
||||
<strong>Trusted + Servers mixed</strong>
|
||||
<small>Main LAN currently holds user clients and service hosts together.</small>
|
||||
<span class="tag">10.5.1.0/24</span>
|
||||
</div>
|
||||
<div class="arrow">→</div>
|
||||
<div class="node target" data-tags="network">
|
||||
<strong>Trusted VLAN 20</strong>
|
||||
<small>Phones, desktops, laptops, Steam Deck, personal endpoints.</small>
|
||||
<span class="tag">10.5.20.0/24</span>
|
||||
</div>
|
||||
<div class="node target" data-tags="network">
|
||||
<strong>Servers VLAN 30</strong>
|
||||
<small>PD, Serenity, Nomad, Rocinante, and other service hosts.</small>
|
||||
<span class="tag">10.5.30.0/24</span>
|
||||
</div>
|
||||
<div class="node current" data-tags="network migration wifi">
|
||||
<strong>Old IoT still real</strong>
|
||||
<small>CIA Via on VLAN 2 is still carrying the meaningful smart-home traffic.</small>
|
||||
<span class="tag">192.168.1.0/24</span>
|
||||
</div>
|
||||
<div class="arrow">→</div>
|
||||
<div class="node target" data-tags="network migration wifi">
|
||||
<strong>IoT VLAN 40</strong>
|
||||
<small>All retained smart-home gear lands here after phased migration.</small>
|
||||
<span class="tag">10.5.40.0/24</span>
|
||||
</div>
|
||||
<div class="node current" data-tags="security network">
|
||||
<strong>Camera lane underused</strong>
|
||||
<small>Doorbell is there, but security gear is not consistently placed yet.</small>
|
||||
<span class="tag">10.5.20.0/24 now / repurpose later</span>
|
||||
</div>
|
||||
<div class="arrow">→</div>
|
||||
<div class="node target" data-tags="security network wifi">
|
||||
<strong>Camera VLAN 60</strong>
|
||||
<small>Doorbell, chimes, future cameras, Protect accessories. Built now, not “someday.”</small>
|
||||
<span class="tag">10.5.60.0/24</span>
|
||||
</div>
|
||||
<div class="node target" data-tags="network wifi">
|
||||
<strong>Guest VLAN 50</strong>
|
||||
<small>Pure internet-only guest lane with client isolation.</small>
|
||||
<span class="tag">10.5.50.0/24</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel" id="vlan-plan">
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2>Target VLAN plan</h2>
|
||||
<p>These cards are the final lane definitions I would work from as your network engineer.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cards" id="vlan-cards"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel" id="ssid-plan">
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2>SSID plan</h2>
|
||||
<p>Fewer SSIDs, clearer meaning, and explicit temporary lanes only where justified.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cards" id="ssid-cards"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel" id="firewall">
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2>Firewall intent matrix</h2>
|
||||
<p>Not every protocol spelled out yet, but the policy posture is explicit and sane.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="matrix" id="fw-matrix"></div>
|
||||
<p class="footer-note advanced">Advanced note: discovery helpers like mDNS reflection, Chromecast flows, and specific Home Assistant / MQTT allowances should be explicit exceptions, never implied by a broad allow-any-to-any rule.</p>
|
||||
</section>
|
||||
|
||||
<section class="panel" id="migration">
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2>Migration phases</h2>
|
||||
<p>Ordered to reduce blast radius: infrastructure first, then servers, then security, then IoT cleanup.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cards" id="phase-cards"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel" id="coverage">
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2>Wi-Fi coverage and AP posture</h2>
|
||||
<p>The downstairs U6 LR is part of the plan if coverage or camera RSSI says it should be.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="coverage-layout">
|
||||
<div class="coverage-card">
|
||||
<h3>Coverage sketch</h3>
|
||||
<div class="floor-map">
|
||||
<div class="coverage-zone zone-u7"></div>
|
||||
<div class="coverage-zone zone-u6" id="zone-u6"></div>
|
||||
<div class="floor upstairs">Upstairs / main coverage zone / existing U7 Pro area</div>
|
||||
<div class="floor downstairs">Downstairs / camera + IoT pressure zone / candidate U6 LR restore point</div>
|
||||
<div class="ap u7">U7</div>
|
||||
<div class="ap u6 off" id="u6-ap">U6</div>
|
||||
</div>
|
||||
<p class="footer-note">If downstairs coverage is already fine, the U6 can stay cold. If signal loss, roaming pain, or camera RSSI looks weak, install it with wired backhaul and let it carry downstairs 2.4/5 GHz load.</p>
|
||||
</div>
|
||||
<div class="coverage-card" data-tags="wifi security network">
|
||||
<h3>AP guidance</h3>
|
||||
<div class="meta">
|
||||
<span class="chip ok">Preferred: wired backhaul</span>
|
||||
<span class="chip attention">Delay RF micro-tuning</span>
|
||||
</div>
|
||||
<ul>
|
||||
<li>Keep the U7 Pro as the main upstairs AP.</li>
|
||||
<li>Reinstall the U6 LR downstairs if validation shows weak coverage or poor roaming.</li>
|
||||
<li>Bias cameras toward wired PoE whenever practical; do not solve camera growth with “more Wi-Fi” by default.</li>
|
||||
<li>Put both AP management interfaces on Management VLAN 10.</li>
|
||||
</ul>
|
||||
<details class="advanced">
|
||||
<summary>Why not over-tune on day one?</summary>
|
||||
<p class="muted">Because the real first win is correct segmentation and sane physical placement. Fancy channel math before the VLAN model is cleaned up is premature wizardry.</p>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2>Decision summary</h2>
|
||||
<p>The crisp operator answer.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cards">
|
||||
<div class="legend-card" data-tags="network">
|
||||
<h3>Best-fit final shape</h3>
|
||||
<ul>
|
||||
<li>Management</li>
|
||||
<li>Trusted</li>
|
||||
<li>Servers</li>
|
||||
<li>IoT</li>
|
||||
<li>Guest</li>
|
||||
<li>Cameras / Security</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="legend-card" data-tags="migration">
|
||||
<h3>Things to delete</h3>
|
||||
<ul>
|
||||
<li>Old IoT VLAN 2</li>
|
||||
<li>Stale access-port archaeology</li>
|
||||
<li>Non-infra devices on Management</li>
|
||||
<li>Duplicate SSIDs without a real policy reason</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="legend-card" data-tags="security wifi">
|
||||
<h3>Security posture</h3>
|
||||
<ul>
|
||||
<li>IoT and Cameras default-deny toward the rest of the LAN</li>
|
||||
<li>Trusted is the operator lane</li>
|
||||
<li>Servers expose only explicit services</li>
|
||||
<li>Guest is internet-only and isolated</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const vlans = [
|
||||
{
|
||||
name: 'VLAN 10 · Infrastructure / Management',
|
||||
subnet: '10.5.10.0/24',
|
||||
tags: ['network','security'],
|
||||
state: 'ok',
|
||||
chips: ['Infra only','No client junk'],
|
||||
summary: 'Gateway, switches, APs, management interfaces, and admin surfaces. This lane becomes sacred.',
|
||||
bullets: [
|
||||
'Move Protect chimes out of Management.',
|
||||
'Only designated admin clients and tools may initiate into this VLAN.',
|
||||
'Use this as the native management lane for network gear and APs.'
|
||||
],
|
||||
advanced: 'This is where I would park UDM, USW Pro HD 24, USW-24-PoE, U7 Pro, U6 LR, and any OOB or appliance management IPs.'
|
||||
},
|
||||
{
|
||||
name: 'VLAN 20 · Trusted Clients',
|
||||
subnet: '10.5.20.0/24',
|
||||
tags: ['network','wifi'],
|
||||
state: 'ok',
|
||||
chips: ['Phones + personal devices','Main user lane'],
|
||||
summary: 'Your daily-driver devices live here: phones, laptops, desktops, tablets, Steam Deck, handhelds.',
|
||||
bullets: [
|
||||
'Primary trusted SSID maps here.',
|
||||
'Trusted clients can administer servers and selected management surfaces.',
|
||||
'Do not use this lane for “smart” junk just because it is convenient.'
|
||||
],
|
||||
advanced: 'Whiskey Neat Fuck Ice can temporarily remain as a compat/isolation flavor on the same VLAN if you still need it during cleanup.'
|
||||
},
|
||||
{
|
||||
name: 'VLAN 30 · Servers / Core Services',
|
||||
subnet: '10.5.30.0/24',
|
||||
tags: ['network'],
|
||||
state: 'attention',
|
||||
chips: ['Service hosts','Cleaner east-west policy'],
|
||||
summary: 'Separate always-on service hosts from your person-devices so the firewall can speak clearly.',
|
||||
bullets: [
|
||||
'PD, Serenity, Nomad, Rocinante belong here.',
|
||||
'Trusted can reach this lane broadly for admin and use.',
|
||||
'IoT and Cameras get only tightly scoped access into published services.'
|
||||
],
|
||||
advanced: 'If FlyingDutchman is a real service host, it lands here too. Otherwise keep it on Trusted.'
|
||||
},
|
||||
{
|
||||
name: 'VLAN 40 · IoT / Smart Home',
|
||||
subnet: '10.5.40.0/24',
|
||||
tags: ['network','wifi','migration'],
|
||||
state: 'attention',
|
||||
chips: ['Replace Old IoT','Default deny'],
|
||||
summary: 'This is the real home for thermostats, appliances, MyQ, Google Home-class devices, and retained smart-home junk.',
|
||||
bullets: [
|
||||
'Repoint CIA Via here during migration or create a temporary migration SSID.',
|
||||
'Allow DNS, NTP, internet, and only explicit local service exceptions.',
|
||||
'Treat Google/casting discovery as an exception set, not as a reason to flatten the network.'
|
||||
],
|
||||
advanced: 'This replaces Old IoT VLAN 2 entirely once migration is complete.'
|
||||
},
|
||||
{
|
||||
name: 'VLAN 50 · Guest',
|
||||
subnet: '10.5.50.0/24',
|
||||
tags: ['network','wifi'],
|
||||
state: 'ok',
|
||||
chips: ['Internet only','Client isolation'],
|
||||
summary: 'Visitors and untrusted BYOD devices live here and get exactly zero reason to see the rest of the house.',
|
||||
bullets: [
|
||||
'Dedicated guest SSID only.',
|
||||
'Enable client isolation.',
|
||||
'No RFC1918 access back into the lab or house networks.'
|
||||
],
|
||||
advanced: 'Keep this boring. That is the point.'
|
||||
},
|
||||
{
|
||||
name: 'VLAN 60 · Cameras / Security',
|
||||
subnet: '10.5.60.0/24',
|
||||
tags: ['network','security','wifi'],
|
||||
state: 'ok',
|
||||
chips: ['Build now','Ready for camera growth'],
|
||||
summary: 'Doorbell, Protect chimes, future cameras, and security accessories get their own lane now so you do not have to revisit the design later.',
|
||||
bullets: [
|
||||
'Move front-doorbell and both chimes here.',
|
||||
'Prefer wired PoE cameras on this VLAN as the fleet grows.',
|
||||
'Allow only Protect/NVR-related traffic, DNS, NTP, updates, and admin access from Trusted.'
|
||||
],
|
||||
advanced: 'If you add a real NVR or Protect host later, it can stay on Servers VLAN while publishing only the needed interfaces to Cameras/Security.'
|
||||
}
|
||||
];
|
||||
|
||||
const ssids = [
|
||||
{
|
||||
name: 'Trusted SSID',
|
||||
tags: ['wifi'],
|
||||
chips: ['Maps to VLAN 20','Main client Wi-Fi'],
|
||||
summary: 'Use one primary trusted SSID for normal day-to-day devices.',
|
||||
bullets: ['Yer a Wifi Harry is the natural candidate to remain the main trusted SSID.', 'Keep 6 GHz here if the client mix supports it.']
|
||||
},
|
||||
{
|
||||
name: 'Trusted-Compat (temporary)',
|
||||
tags: ['wifi','migration'],
|
||||
chips: ['Optional','Retire later if possible'],
|
||||
summary: 'Only keep a second trusted-flavored SSID if it solves a real compatibility or isolation problem.',
|
||||
bullets: ['Whiskey Neat Fuck Ice can serve as a temporary compat/isolation lane.', 'If it is no longer solving a problem, kill it.']
|
||||
},
|
||||
{
|
||||
name: 'IoT SSID',
|
||||
tags: ['wifi','migration'],
|
||||
chips: ['Maps to VLAN 40','2.4 GHz friendly'],
|
||||
summary: 'This becomes the home for all retained smart-home Wi-Fi devices.',
|
||||
bullets: ['CIA Via can be repointed here during migration to reduce user retraining.', 'Keep broad compatibility; this is not where we get fancy.']
|
||||
},
|
||||
{
|
||||
name: 'Security SSID',
|
||||
tags: ['wifi','security'],
|
||||
chips: ['Maps to VLAN 60','For doorbells/chimes/cameras'],
|
||||
summary: 'A dedicated security Wi-Fi lane lets you separate Protect/security traffic from the general IoT pile.',
|
||||
bullets: ['UNEF\'s Playhouse is the obvious candidate to keep for this purpose.', 'Use it for the doorbell, chimes, and any Wi-Fi cameras/security accessories.']
|
||||
},
|
||||
{
|
||||
name: 'Guest SSID',
|
||||
tags: ['wifi'],
|
||||
chips: ['Maps to VLAN 50','Client isolation'],
|
||||
summary: 'One guest lane. Internet only. No drama.',
|
||||
bullets: ['Keep it separate from your real networks.', 'No internal access exceptions unless there is a compelling reason.']
|
||||
}
|
||||
];
|
||||
|
||||
const phases = [
|
||||
{
|
||||
name: 'Phase 0 · Staging and rollback points',
|
||||
tags: ['migration','network'],
|
||||
chips: ['No cutover yet'],
|
||||
summary: 'Create target VLANs, port profiles, and draft firewall rules before moving anything.',
|
||||
bullets: ['Build trunk-uplink, access-management, access-trusted, access-servers, access-iot, access-guest, access-cameras profiles.', 'Document rollback path before each cutover.']
|
||||
},
|
||||
{
|
||||
name: 'Phase 1 · Infrastructure hygiene',
|
||||
tags: ['migration','network','security'],
|
||||
chips: ['Protect mgmt purity'],
|
||||
summary: 'Move all network gear and only network gear into the Management lane.',
|
||||
bullets: ['UDM, switches, APs, management surfaces.', 'Get the Protect chimes out of Management.']
|
||||
},
|
||||
{
|
||||
name: 'Phase 2 · Servers split from Trusted',
|
||||
tags: ['migration','network'],
|
||||
chips: ['Service boundary'],
|
||||
summary: 'Move core service hosts into Servers VLAN 30 and validate name resolution and reachability.',
|
||||
bullets: ['PD, Serenity, Nomad, Rocinante first.', 'Update static addressing, DNS, ACLs, and any service bindings.']
|
||||
},
|
||||
{
|
||||
name: 'Phase 3 · Security lane now',
|
||||
tags: ['migration','security','wifi'],
|
||||
chips: ['Camera growth ready'],
|
||||
summary: 'Move the doorbell and chimes into Cameras/Security before you expand the camera fleet.',
|
||||
bullets: ['This builds the right landing zone now.', 'Future camera rollout becomes additive rather than redesign work.']
|
||||
},
|
||||
{
|
||||
name: 'Phase 4 · Retire Old IoT',
|
||||
tags: ['migration','wifi'],
|
||||
chips: ['Most user-visible phase'],
|
||||
summary: 'Batch-migrate retained smart-home devices off CIA Via / VLAN 2 and onto the new IoT lane.',
|
||||
bullets: ['Easy cloud devices first.', 'Thermostats next.', 'Google/casting devices last because they are the most likely to be annoying.']
|
||||
},
|
||||
{
|
||||
name: 'Phase 5 · Clean SSIDs and stale rules',
|
||||
tags: ['migration','wifi','network'],
|
||||
chips: ['Final cleanup'],
|
||||
summary: 'Disable the old SSID, delete VLAN 2, and remove historical special cases.',
|
||||
bullets: ['Retire temporary compat SSIDs if not needed.', 'Delete stale firewall rules and stale port assignments.']
|
||||
}
|
||||
];
|
||||
|
||||
const fwRows = [
|
||||
['Source / Target','Management','Servers','IoT','Cameras'],
|
||||
['Trusted','Admin only','Allow','Selective allow','Allow for operators'],
|
||||
['Servers','Rare and explicit','Internal service mesh only','Explicit services only','Protect / NVR flows only'],
|
||||
['IoT','Deny','Explicit exceptions only','Same VLAN only','Deny'],
|
||||
['Cameras','Deny','Protect / NVR only','Deny','Same VLAN only']
|
||||
];
|
||||
|
||||
const stateClass = { ok:'ok', attention:'attention', warn:'warn' };
|
||||
|
||||
function cardTemplate(item, kind='vlan-card') {
|
||||
return `
|
||||
<article class="${kind}" data-tags="${item.tags.join(' ')}">
|
||||
<h3>${item.name}</h3>
|
||||
${item.subnet ? `<div class="meta"><span class="chip ${stateClass[item.state] || ''}">${item.subnet}</span>${item.chips.map(c=>`<span class="chip">${c}</span>`).join('')}</div>` : `<div class="meta">${item.chips.map(c=>`<span class="chip">${c}</span>`).join('')}</div>`}
|
||||
<p class="muted">${item.summary}</p>
|
||||
<ul>${item.bullets.map(b=>`<li>${b}</li>`).join('')}</ul>
|
||||
${item.advanced ? `<details class="advanced"><summary>Advanced detail</summary><p class="muted">${item.advanced}</p></details>` : ''}
|
||||
</article>`;
|
||||
}
|
||||
|
||||
document.getElementById('vlan-cards').innerHTML = vlans.map(v => cardTemplate(v)).join('');
|
||||
document.getElementById('ssid-cards').innerHTML = ssids.map(v => cardTemplate(v,'ssid-card')).join('');
|
||||
document.getElementById('phase-cards').innerHTML = phases.map(v => cardTemplate(v,'phase-card')).join('');
|
||||
document.getElementById('fw-matrix').innerHTML = fwRows.flatMap((row, rIdx) => row.map((cell, cIdx) => `<div class="cell ${(rIdx===0||cIdx===0)?'head':''}">${cell}</div>`)).join('');
|
||||
|
||||
const filters = document.querySelectorAll('.filter');
|
||||
const filterable = document.querySelectorAll('[data-tags]');
|
||||
filters.forEach(btn => btn.addEventListener('click', () => {
|
||||
filters.forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
const tag = btn.dataset.filter;
|
||||
filterable.forEach(el => {
|
||||
if (tag === 'all') el.classList.remove('filter-hide');
|
||||
else el.classList.toggle('filter-hide', !(el.dataset.tags || '').split(' ').includes(tag));
|
||||
});
|
||||
localStorage.setItem('network-plan-filter', tag);
|
||||
}));
|
||||
|
||||
const detailToggle = document.getElementById('detail-toggle');
|
||||
detailToggle.addEventListener('click', () => {
|
||||
document.body.classList.toggle('hidden-advanced');
|
||||
const advancedVisible = !document.body.classList.contains('hidden-advanced');
|
||||
detailToggle.textContent = advancedVisible ? 'Hide advanced details' : 'Show advanced details';
|
||||
detailToggle.classList.toggle('active', advancedVisible);
|
||||
localStorage.setItem('network-plan-advanced', advancedVisible ? '1' : '0');
|
||||
});
|
||||
|
||||
const u6Toggle = document.getElementById('u6-toggle');
|
||||
const u6Ap = document.getElementById('u6-ap');
|
||||
const u6Zone = document.getElementById('zone-u6');
|
||||
function setU6(on){
|
||||
u6Ap.classList.toggle('off', !on);
|
||||
u6Zone.style.display = on ? 'block' : 'none';
|
||||
u6Toggle.textContent = on ? 'Disable downstairs U6 LR' : 'Enable downstairs U6 LR';
|
||||
u6Toggle.classList.toggle('active', on);
|
||||
localStorage.setItem('network-plan-u6', on ? '1' : '0');
|
||||
}
|
||||
u6Toggle.addEventListener('click', () => setU6(u6Ap.classList.contains('off')));
|
||||
|
||||
const savedFilter = localStorage.getItem('network-plan-filter');
|
||||
if (savedFilter) {
|
||||
const btn = [...filters].find(b => b.dataset.filter === savedFilter);
|
||||
if (btn) btn.click();
|
||||
}
|
||||
if (localStorage.getItem('network-plan-advanced') === '1') detailToggle.click();
|
||||
setU6(localStorage.getItem('network-plan-u6') === '1');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
641
home/doris-dashboard/public/network-web.html
Normal file
641
home/doris-dashboard/public/network-web.html
Normal file
@@ -0,0 +1,641 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Doris Network Web</title>
|
||||
<style>
|
||||
:root{
|
||||
color-scheme:dark;
|
||||
--bg:#06080d;
|
||||
--panel:#101620;
|
||||
--panel-soft:#151d2a;
|
||||
--line:rgba(239,233,219,.10);
|
||||
--text:#f2efdd;
|
||||
--muted:#c7bba8;
|
||||
--trusted:#6ea8fe;
|
||||
--mgmt:#f2c14e;
|
||||
--servers:#e07a5f;
|
||||
--iot:#7bd389;
|
||||
--guest:#b392f0;
|
||||
--camera:#ff7b72;
|
||||
--legacy:#7f8796;
|
||||
--topology:#7dd3fc;
|
||||
--flow:#f87171;
|
||||
--allow:#7bd389;
|
||||
--deny:#ff7b72;
|
||||
--migrate:#f2c14e;
|
||||
--shadow:0 22px 52px rgba(0,0,0,.36);
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0;
|
||||
font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
|
||||
background:
|
||||
radial-gradient(circle at 10% 0%, rgba(242,193,78,.12), transparent 22rem),
|
||||
radial-gradient(circle at 100% 10%, rgba(110,168,254,.10), transparent 24rem),
|
||||
radial-gradient(circle at 60% 100%, rgba(224,122,95,.10), transparent 22rem),
|
||||
linear-gradient(180deg,#06080d 0%,#0a1018 50%,#0d131d 100%);
|
||||
color:var(--text);
|
||||
min-height:100vh;
|
||||
}
|
||||
a{color:inherit}
|
||||
.wrap{max-width:1680px;margin:0 auto;padding:20px}
|
||||
.hero,.canvas-shell,.panel,.stage-card,.layer-card{
|
||||
background:rgba(16,22,32,.92);
|
||||
border:1px solid var(--line);
|
||||
border-radius:24px;
|
||||
box-shadow:var(--shadow);
|
||||
}
|
||||
.hero{
|
||||
padding:22px;
|
||||
display:grid;
|
||||
grid-template-columns:1.2fr .8fr;
|
||||
gap:18px;
|
||||
margin-bottom:18px;
|
||||
}
|
||||
.eyebrow{margin:0 0 8px;color:var(--mgmt);text-transform:uppercase;letter-spacing:.18em;font-size:.74rem}
|
||||
h1{margin:0;font-size:clamp(2.4rem,5vw,4.4rem);line-height:.93;letter-spacing:-.04em}
|
||||
.sub{margin:12px 0 0;max-width:76ch;color:var(--muted);line-height:1.55}
|
||||
.chips{display:flex;flex-wrap:wrap;gap:10px;margin-top:16px}
|
||||
.chip{padding:7px 11px;border-radius:999px;border:1px solid var(--line);background:rgba(255,255,255,.04);font-size:.82rem}
|
||||
.summary-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}
|
||||
.summary-card{background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.07);border-radius:18px;padding:14px}
|
||||
.summary-card strong{display:block;font-size:1.05rem}
|
||||
.summary-card small{display:block;margin-top:4px;color:var(--muted);line-height:1.35}
|
||||
.layout{display:grid;grid-template-columns:minmax(0,1.6fr) 380px;gap:18px}
|
||||
.canvas-shell{padding:14px;position:relative;overflow:hidden}
|
||||
.toolbar,.subtoolbar{display:flex;flex-wrap:wrap;gap:10px}
|
||||
.subtoolbar{margin-top:10px}
|
||||
button{
|
||||
appearance:none;border:1px solid var(--line);background:rgba(255,255,255,.04);color:var(--text);
|
||||
padding:9px 12px;border-radius:999px;cursor:pointer;font:inherit;transition:background .18s ease,border-color .18s ease,transform .18s ease
|
||||
}
|
||||
button:hover{transform:translateY(-1px)}
|
||||
button.active{background:rgba(242,193,78,.16);border-color:rgba(242,193,78,.35)}
|
||||
.subtoolbar button.active[data-layer="topology"]{background:rgba(125,211,252,.16);border-color:rgba(125,211,252,.35)}
|
||||
.subtoolbar button.active[data-layer="flow"]{background:rgba(248,113,113,.16);border-color:rgba(248,113,113,.35)}
|
||||
.subtoolbar button.active[data-layer="migration"]{background:rgba(242,193,78,.16);border-color:rgba(242,193,78,.35)}
|
||||
.panel{padding:16px}
|
||||
.panel h2{margin:0 0 12px;font-size:1.15rem}
|
||||
.panel p{margin:0;color:var(--muted);line-height:1.45}
|
||||
.legend{display:grid;gap:10px;margin-top:14px}
|
||||
.legend-row{display:grid;grid-template-columns:14px 1fr;gap:10px;align-items:center}
|
||||
.swatch{width:14px;height:14px;border-radius:50%}
|
||||
.stroke-swatch{width:14px;height:14px;border-radius:50%;background:transparent;border:2px solid currentColor}
|
||||
.info h3{margin:0 0 6px;font-size:1.08rem}
|
||||
.info .meta{display:flex;gap:8px;flex-wrap:wrap;margin:10px 0}
|
||||
.info .meta span{padding:5px 8px;border-radius:999px;border:1px solid var(--line);font-size:.78rem;background:rgba(255,255,255,.04)}
|
||||
.info ul{margin:10px 0 0 18px;padding:0}
|
||||
.info li{margin:6px 0;color:#e6dece}
|
||||
.caption{margin-top:10px;font-size:.88rem;color:var(--muted)}
|
||||
svg{
|
||||
display:block;width:100%;height:min(76vh,980px);border-radius:18px;
|
||||
background:
|
||||
radial-gradient(circle at 50% 50%, rgba(123,211,137,.04), transparent 24rem),
|
||||
linear-gradient(180deg, rgba(255,255,255,.015), rgba(255,255,255,.005));
|
||||
}
|
||||
.ring{fill:none;stroke:rgba(255,255,255,.05);stroke-dasharray:4 8}
|
||||
.mode-fade{opacity:.18}
|
||||
.link{stroke:rgba(255,255,255,.15);stroke-width:2.4;fill:none;transition:stroke .18s ease, opacity .18s ease, stroke-width .18s ease}
|
||||
.link.cross{stroke-dasharray:8 10;opacity:.62}
|
||||
.link.highlight{stroke:#fff2cf;stroke-width:3.8;opacity:1}
|
||||
.link.dim{opacity:.10}
|
||||
.node{cursor:pointer;transition:opacity .18s ease, transform .18s ease}
|
||||
.node.dim{opacity:.14}
|
||||
.node-core{stroke:rgba(255,255,255,.16);stroke-width:2}
|
||||
.node-label{font-size:14px;font-weight:700;fill:var(--text);text-anchor:middle;pointer-events:none}
|
||||
.node-sub{font-size:11px;fill:var(--muted);text-anchor:middle;pointer-events:none}
|
||||
.node.active .node-core{stroke:#fff2cf;stroke-width:3}
|
||||
.cluster-title{font-size:15px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;fill:rgba(242,239,221,.52)}
|
||||
.layer-line{fill:none;stroke-linecap:round;stroke-linejoin:round;opacity:.95;transition:opacity .18s ease}
|
||||
.layer-line.dim{opacity:.12}
|
||||
.layer-line.topology{stroke:var(--topology);stroke-width:4;stroke-dasharray:2 10}
|
||||
.layer-line.flow{stroke:var(--flow);stroke-width:4.2;marker-end:url(#arrow-flow)}
|
||||
.layer-line.flow.allow{stroke:var(--allow)}
|
||||
.layer-line.flow.restrict{stroke:var(--migrate);stroke-dasharray:12 10;marker-end:url(#arrow-migrate)}
|
||||
.layer-line.flow.deny{stroke:var(--deny);stroke-dasharray:5 10;marker-end:url(#arrow-deny)}
|
||||
.layer-line.migration{stroke:var(--migrate);stroke-width:4;stroke-dasharray:12 12;marker-end:url(#arrow-migrate)}
|
||||
.layer-label{font-size:11px;fill:#f4ead7;font-weight:700;letter-spacing:.03em;paint-order:stroke;stroke:#0b1017;stroke-width:4px}
|
||||
.layer-label.dim{opacity:.12}
|
||||
.stage-track{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-top:14px}
|
||||
.stage-card,.layer-card{padding:12px 14px;background:rgba(255,255,255,.03);border-radius:18px}
|
||||
.stage-card.active{border-color:rgba(242,193,78,.35);background:rgba(242,193,78,.08)}
|
||||
.stage-card h3,.layer-card h3,.note h3{margin:0 0 8px;font-size:1rem}
|
||||
.stage-card p,.layer-card p,.note li{color:#e6dece}
|
||||
.stage-card p,.layer-card p{margin:0;font-size:.92rem;line-height:1.45}
|
||||
.layer-grid,.note-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin-top:18px}
|
||||
.note{background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.07);border-radius:18px;padding:14px}
|
||||
.note ul{margin:0 0 0 18px;padding:0}
|
||||
.stage-badge{display:inline-flex;align-items:center;gap:6px;font-size:.78rem;padding:4px 8px;border-radius:999px;border:1px solid rgba(255,255,255,.08);margin-bottom:8px}
|
||||
.stage-badge::before{content:'';display:block;width:8px;height:8px;border-radius:50%;background:var(--migrate)}
|
||||
.small{font-size:.82rem;color:var(--muted)}
|
||||
@media (max-width:1200px){
|
||||
.hero,.layout{grid-template-columns:1fr}
|
||||
}
|
||||
@media (max-width:820px){
|
||||
.wrap{padding:14px}
|
||||
.summary-grid,.note-grid,.layer-grid,.stage-track{grid-template-columns:1fr}
|
||||
svg{height:72vh}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<section class="hero">
|
||||
<div>
|
||||
<p class="eyebrow">Operator map / graphical redesign artifact</p>
|
||||
<h1>Network Web v2</h1>
|
||||
<p class="sub">Now it is the actual operator map instead of just a conceptual graph. Same core redesign, but with three extra layers: physical/topology intent, firewall-flow reasoning, and staged migration sequencing. Click nodes or switch modes to inspect where the pain is and what the end state should look like.</p>
|
||||
<div class="chips">
|
||||
<span class="chip">6 target VLANs</span>
|
||||
<span class="chip">Topology overlay</span>
|
||||
<span class="chip">Firewall flow layer</span>
|
||||
<span class="chip">4-stage migration path</span>
|
||||
<span class="chip">Optional downstairs U6 LR</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-grid">
|
||||
<div class="summary-card"><strong>Topology layer</strong><small>Shows the cabling/placement story: operator, gateway, switch fabric, APs, and where endpoints really hang.</small></div>
|
||||
<div class="summary-card"><strong>Firewall layer</strong><small>Shows allow, deny, and exception paths so policy intent is visible instead of implied.</small></div>
|
||||
<div class="summary-card"><strong>Migration layer</strong><small>Shows what moves first, what is annoying, and what can wait until the end.</small></div>
|
||||
<div class="summary-card"><strong>Operator map</strong><small>Use this to reason about design, staging, and “what breaks if I move this” before touching UniFi.</small></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="layout">
|
||||
<div class="canvas-shell">
|
||||
<div class="toolbar">
|
||||
<button class="active" data-mode="all">Show full web</button>
|
||||
<button data-mode="vlan">VLAN cores only</button>
|
||||
<button data-mode="migration">Migration pain</button>
|
||||
<button data-mode="wifi">Wi-Fi / coverage</button>
|
||||
<button data-mode="topology">Topology view</button>
|
||||
<button data-mode="policy">Firewall policy</button>
|
||||
<button id="u6Toggle">Toggle downstairs U6 LR</button>
|
||||
</div>
|
||||
<div class="subtoolbar">
|
||||
<button class="active" data-layer="topology">Topology overlay</button>
|
||||
<button class="active" data-layer="flow">Firewall flows</button>
|
||||
<button class="active" data-layer="migration">Migration arrows</button>
|
||||
<button data-stage="0" class="active">All stages</button>
|
||||
<button data-stage="1">Stage 1</button>
|
||||
<button data-stage="2">Stage 2</button>
|
||||
<button data-stage="3">Stage 3</button>
|
||||
<button data-stage="4">Stage 4</button>
|
||||
</div>
|
||||
<svg id="networkSvg" viewBox="0 0 1280 960" aria-label="Home network redesign graph">
|
||||
<defs>
|
||||
<marker id="arrow-flow" markerWidth="12" markerHeight="12" refX="10" refY="6" orient="auto" markerUnits="userSpaceOnUse">
|
||||
<path d="M0,0 L12,6 L0,12 z" fill="var(--flow)"></path>
|
||||
</marker>
|
||||
<marker id="arrow-deny" markerWidth="12" markerHeight="12" refX="10" refY="6" orient="auto" markerUnits="userSpaceOnUse">
|
||||
<path d="M0,0 L12,6 L0,12 z" fill="var(--deny)"></path>
|
||||
</marker>
|
||||
<marker id="arrow-migrate" markerWidth="12" markerHeight="12" refX="10" refY="6" orient="auto" markerUnits="userSpaceOnUse">
|
||||
<path d="M0,0 L12,6 L0,12 z" fill="var(--migrate)"></path>
|
||||
</marker>
|
||||
</defs>
|
||||
<circle class="ring" cx="640" cy="470" r="125"></circle>
|
||||
<circle class="ring" cx="640" cy="470" r="270"></circle>
|
||||
<circle class="ring" cx="640" cy="470" r="415"></circle>
|
||||
<text class="cluster-title" x="640" y="125" text-anchor="middle">Operator / routing core</text>
|
||||
<text class="cluster-title" x="640" y="245" text-anchor="middle">Target VLANs and policy lanes</text>
|
||||
<text class="cluster-title" x="640" y="900" text-anchor="middle">Endpoints, legacy scar tissue, migration pressure, and future growth</text>
|
||||
<g id="baseLinks"></g>
|
||||
<g id="layerLines"></g>
|
||||
<g id="layerLabels"></g>
|
||||
<g id="nodes"></g>
|
||||
</svg>
|
||||
|
||||
<div class="stage-track" id="stageTrack">
|
||||
<div class="stage-card" data-stage-card="1">
|
||||
<div class="stage-badge">Stage 1</div>
|
||||
<h3>Stop obvious sins</h3>
|
||||
<p>Move Protect chimes out of Management, stand up Camera VLAN 60, and make Management clean again.</p>
|
||||
</div>
|
||||
<div class="stage-card" data-stage-card="2">
|
||||
<div class="stage-badge">Stage 2</div>
|
||||
<h3>Split infrastructure from life</h3>
|
||||
<p>Move PD, Serenity, Nomad, and Rocinante into Servers VLAN 30 so trusted clients stop sharing a lane with infrastructure.</p>
|
||||
</div>
|
||||
<div class="stage-card" data-stage-card="3">
|
||||
<div class="stage-badge">Stage 3</div>
|
||||
<h3>Drain old IoT</h3>
|
||||
<p>Migrate ecobees, appliances, and low-drama devices first. Leave Google discovery weirdness for last inside IoT.</p>
|
||||
</div>
|
||||
<div class="stage-card" data-stage-card="4">
|
||||
<div class="stage-badge">Stage 4</div>
|
||||
<h3>Refine radio and policy</h3>
|
||||
<p>Decide whether the downstairs U6 LR returns, clean up SSIDs, then tune firewall exceptions with evidence instead of superstition.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layer-grid">
|
||||
<div class="layer-card">
|
||||
<h3>Topology interpretation</h3>
|
||||
<p>The UDM is routing brains, the USW is the practical center of gravity, and the APs are infrastructure nodes carrying policy-bound SSIDs. That means client traffic categories should be separated by VLAN intent, not by whatever happened to be easiest last year.</p>
|
||||
</div>
|
||||
<div class="layer-card">
|
||||
<h3>Firewall interpretation</h3>
|
||||
<p>Trusted gets explicit admin reach into Management and Servers. IoT and Cameras only get DNS/NTP/internet plus tightly scoped local-service exceptions. Guest gets nothing but internet. Old IoT gets a one-way ticket to deletion.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="note-grid">
|
||||
<div class="note">
|
||||
<h3>What this graph is saying now</h3>
|
||||
<ul>
|
||||
<li>Management is not a junk drawer.</li>
|
||||
<li>Servers are infrastructure and deserve their own lane.</li>
|
||||
<li>Cameras/security is a first-class lane because future growth is already obvious.</li>
|
||||
<li>Migration difficulty lives mostly in discovery-heavy IoT, not in the easy appliances.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="note">
|
||||
<h3>What changes first</h3>
|
||||
<ul>
|
||||
<li>Move chimes out of Management.</li>
|
||||
<li>Create Camera VLAN 60 now.</li>
|
||||
<li>Pull core servers out of Trusted.</li>
|
||||
<li>Drain and kill Old IoT / VLAN 2.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="panel info">
|
||||
<h2>Selected node</h2>
|
||||
<div id="infoBox">
|
||||
<h3>Whole design</h3>
|
||||
<p>Click a node in the graph. I will show what it is, what it should talk to, and where it sits in the migration sequence.</p>
|
||||
<div class="meta">
|
||||
<span>Tip: click Mgmt</span>
|
||||
<span>Tip: click Cameras</span>
|
||||
<span>Tip: click Old IoT</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="legend">
|
||||
<div class="legend-row"><span class="swatch" style="background:var(--mgmt)"></span><span>Management / infrastructure</span></div>
|
||||
<div class="legend-row"><span class="swatch" style="background:var(--trusted)"></span><span>Trusted clients / operator lane</span></div>
|
||||
<div class="legend-row"><span class="swatch" style="background:var(--servers)"></span><span>Servers / core services</span></div>
|
||||
<div class="legend-row"><span class="swatch" style="background:var(--iot)"></span><span>IoT / smart home</span></div>
|
||||
<div class="legend-row"><span class="swatch" style="background:var(--guest)"></span><span>Guest</span></div>
|
||||
<div class="legend-row"><span class="swatch" style="background:var(--camera)"></span><span>Cameras / security</span></div>
|
||||
<div class="legend-row"><span class="swatch" style="background:var(--legacy)"></span><span>Legacy / migration pain</span></div>
|
||||
<div class="legend-row" style="color:var(--topology)"><span class="stroke-swatch"></span><span>Topology / physical intent overlay</span></div>
|
||||
<div class="legend-row" style="color:var(--allow)"><span class="stroke-swatch"></span><span>Allowed policy path</span></div>
|
||||
<div class="legend-row" style="color:var(--deny)"><span class="stroke-swatch"></span><span>Denied / blocked path</span></div>
|
||||
<div class="legend-row" style="color:var(--migrate)"><span class="stroke-swatch"></span><span>Migration or restricted exception path</span></div>
|
||||
</div>
|
||||
<p class="caption">This is no longer just a decision picture. It is an operator map for topology, policy, and staged cutover thinking.</p>
|
||||
</aside>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const palette = {
|
||||
mgmt: getComputedStyle(document.documentElement).getPropertyValue('--mgmt').trim(),
|
||||
trusted: getComputedStyle(document.documentElement).getPropertyValue('--trusted').trim(),
|
||||
servers: getComputedStyle(document.documentElement).getPropertyValue('--servers').trim(),
|
||||
iot: getComputedStyle(document.documentElement).getPropertyValue('--iot').trim(),
|
||||
guest: getComputedStyle(document.documentElement).getPropertyValue('--guest').trim(),
|
||||
camera: getComputedStyle(document.documentElement).getPropertyValue('--camera').trim(),
|
||||
legacy: getComputedStyle(document.documentElement).getPropertyValue('--legacy').trim(),
|
||||
core: '#f2efdd'
|
||||
};
|
||||
|
||||
const nodes = [
|
||||
{id:'operator', label:'John / Doris', sub:'operator lane', x:640, y:470, r:32, color:palette.core, group:'core', tags:['all'], stage:0,
|
||||
info:{title:'Operator / control point', meta:['Human admin','Design center'], text:'The network is supposed to serve an operator, not confuse one. Trusted becomes the homeowner lane and the place from which management and service access are explicitly granted.', bullets:['Trusted becomes the human/operator lane.','Admin access is explicit, not accidental.']}}
|
||||
,{id:'udm', label:'UDM Pro', sub:'gateway / routing', x:640, y:320, r:24, color:palette.mgmt, group:'core', tags:['all','vlan','topology','policy'], stage:0,
|
||||
info:{title:'UDM Pro', meta:['Gateway','Routing core'], text:'Routing brain. Terminates VLANs and enforces policy. Management plane belongs in VLAN 10, not mixed into random endpoint space.', bullets:['Core of routing and firewall policy.','Upstream of every lane.']}}
|
||||
,{id:'prohd', label:'USW Pro HD 24', sub:'main switch fabric', x:790, y:425, r:22, color:palette.mgmt, group:'core', tags:['all','vlan','topology'], stage:0,
|
||||
info:{title:'USW Pro HD 24', meta:['Main access/core switch'], text:'This is the physical gravity well of the live network. Trunks to APs and servers should reflect deliberate lane design.', bullets:['Carries trunks to all major lanes.','Physical center of gravity.']}}
|
||||
,{id:'u7', label:'U7 Pro', sub:'main upstairs AP', x:760, y:605, r:20, color:palette.mgmt, group:'core', tags:['all','wifi','topology'], stage:4,
|
||||
info:{title:'U7 Pro', meta:['Primary AP','Infrastructure'], text:'The U7 stays as the primary AP. Infrastructure device in Management, serving SSIDs that map to real policy lanes.', bullets:['Broadcasts Trusted, IoT, Guest, Security SSIDs.','Management interface remains isolated.']}}
|
||||
,{id:'u6', label:'U6 LR', sub:'downstairs / optional restore', x:500, y:610, r:18, color:palette.mgmt, group:'core optional-u6', tags:['all','wifi','topology'], hidden:true, stage:4,
|
||||
info:{title:'U6 LR', meta:['Optional second AP','Coverage lever'], text:'Add back if coverage, roaming, or future camera/security RSSI actually needs it. Wired backhaul preferred.', bullets:['Treat this as a design lever, not a guess.','Verify with real coverage pain, not vibes.']}}
|
||||
,{id:'mgmt', label:'Mgmt VLAN 10', sub:'10.5.10.0/24', x:640, y:195, r:28, color:palette.mgmt, group:'vlan', tags:['all','vlan','policy'], stage:1,
|
||||
info:{title:'Management VLAN 10', meta:['Infra only','No junk'], text:'Gateway, switches, APs, management interfaces, and admin surfaces. This lane should be sacred and boring.', bullets:['No smart-home accessories.','No chimes.','Only explicit admin initiators reach here.']}}
|
||||
,{id:'trusted', label:'Trusted VLAN 20', sub:'10.5.20.0/24', x:890, y:355, r:28, color:palette.trusted, group:'vlan', tags:['all','vlan','wifi','policy'], stage:0,
|
||||
info:{title:'Trusted VLAN 20', meta:['Operator lane','Main user devices'], text:'Phones, laptops, desktops, tablets, Steam Deck, and daily-driver endpoints. This is where the humans live.', bullets:['Can administer Management and Servers with explicit rules.','Not a dumping ground for semi-smart junk.']}}
|
||||
,{id:'servers', label:'Servers VLAN 30', sub:'10.5.30.0/24', x:890, y:585, r:28, color:palette.servers, group:'vlan', tags:['all','vlan','policy'], stage:2,
|
||||
info:{title:'Servers VLAN 30', meta:['Core services','Homelab spine'], text:'PD, Serenity, Nomad, Rocinante, and other real service hosts belong here so policy is clean and intent is obvious.', bullets:['Split from Trusted for cleaner firewalling.','Expose only required services to other lanes.']}}
|
||||
,{id:'iot', label:'IoT VLAN 40', sub:'10.5.40.0/24', x:640, y:745, r:28, color:palette.iot, group:'vlan', tags:['all','vlan','migration','wifi','policy'], stage:3,
|
||||
info:{title:'IoT VLAN 40', meta:['Smart home','Default deny'], text:'Real landing zone for thermostats, appliances, Google devices, and retained smart-home junk. Explicit local-service exceptions only.', bullets:['Replaces Old IoT.','Allow DNS/NTP/internet plus specific helpers only.']}}
|
||||
,{id:'guest', label:'Guest VLAN 50', sub:'10.5.50.0/24', x:390, y:585, r:26, color:palette.guest, group:'vlan', tags:['all','vlan','wifi','policy'], stage:0,
|
||||
info:{title:'Guest VLAN 50', meta:['Internet only','Client isolation'], text:'Visitors and random untrusted devices get internet and nothing else. Boring is correct here.', bullets:['Pure guest SSID.','No internal access.']}}
|
||||
,{id:'camera', label:'Camera VLAN 60', sub:'10.5.60.0/24', x:390, y:355, r:28, color:palette.camera, group:'vlan', tags:['all','vlan','migration','security','wifi','policy'], stage:1,
|
||||
info:{title:'Camera / Security VLAN 60', meta:['Build now','Future growth'], text:'Doorbell, Protect chimes, future cameras, and other security devices get a first-class lane now instead of being bolted on later.', bullets:['Prefer wired PoE cameras as you grow.','Allow only NVR/Protect/admin flows.']}}
|
||||
,{id:'pd', label:'PD', sub:'core server', x:1090, y:635, r:18, color:palette.servers, group:'device', tags:['all','vlan','topology'], stage:2,
|
||||
info:{title:'PlausibleDeniability', meta:['Server VLAN resident'], text:'Infrastructure host. Belongs in Servers VLAN 30, not mixed into the human client lane.', bullets:['Keep service exposure explicit.','Infrastructure, not a casual endpoint.']}}
|
||||
,{id:'serenity', label:'Serenity', sub:'core server', x:985, y:740, r:17, color:palette.servers, group:'device', tags:['all','vlan','topology'], stage:2,
|
||||
info:{title:'Serenity', meta:['Server VLAN resident'], text:'Lives with the rest of the homelab spine.', bullets:['Cleaner trust boundary.']}}
|
||||
,{id:'nomad', label:'Nomad', sub:'core server', x:855, y:790, r:17, color:palette.servers, group:'device', tags:['all','vlan','topology'], stage:2,
|
||||
info:{title:'Nomad', meta:['Server VLAN resident'], text:'Part of the real service lane, not the personal device lane.', bullets:['Reachable from Trusted with explicit policy.']}}
|
||||
,{id:'rocinante', label:'Rocinante', sub:'core host', x:1145, y:530, r:17, color:palette.servers, group:'device', tags:['all','vlan','topology'], stage:2,
|
||||
info:{title:'Rocinante', meta:['Server/core host'], text:'Another device that reads as infrastructure rather than a casual client.', bullets:['Belongs in Servers unless proven otherwise.']}}
|
||||
,{id:'phones', label:'Phones / laptops', sub:'trusted clients', x:1060, y:280, r:19, color:palette.trusted, group:'device', tags:['all','wifi','topology'], stage:0,
|
||||
info:{title:'Trusted client devices', meta:['Trusted VLAN'], text:'Your daily-driver human devices stay on Trusted.', bullets:['Admin from here.','User experience from here.']}}
|
||||
,{id:'steamdeck', label:'Steam Deck / handhelds', sub:'trusted clients', x:975, y:185, r:16, color:palette.trusted, group:'device', tags:['all','wifi'], stage:0,
|
||||
info:{title:'Trusted handhelds', meta:['Trusted VLAN'], text:'Still trusted, still yours, still not IoT.', bullets:['Keep on Trusted unless an edge-case really demands otherwise.']}}
|
||||
,{id:'ecobee', label:'ecobees', sub:'thermostats', x:620, y:870, r:17, color:palette.iot, group:'device', tags:['all','migration'], stage:3,
|
||||
info:{title:'ecobees', meta:['IoT lane'], text:'Meaningful smart devices that belong in IoT, not in leftover legacy space.', bullets:['Validate app control after migration.']}}
|
||||
,{id:'google', label:'Google Home / cast', sub:'voice + discovery pain', x:455, y:840, r:18, color:palette.iot, group:'device', tags:['all','migration','wifi'], stage:3,
|
||||
info:{title:'Google ecosystem devices', meta:['IoT lane','Discovery exceptions'], text:'These are the annoying part of migration because discovery/casting can want special treatment.', bullets:['Likely the last IoT batch.','Use helpers/exceptions, not trust flattening.']}}
|
||||
,{id:'appliances', label:'Appliances / MyQ', sub:'fridge dryer garage', x:760, y:845, r:17, color:palette.iot, group:'device', tags:['all','migration'], stage:3,
|
||||
info:{title:'Appliances and garage', meta:['IoT lane'], text:'Easy low-drama migration targets. Keep them but do not trust them.', bullets:['Good early migration batch.']}}
|
||||
,{id:'doorbell', label:'Doorbell', sub:'security endpoint', x:225, y:305, r:17, color:palette.camera, group:'device', tags:['all','security','wifi'], stage:1,
|
||||
info:{title:'Doorbell', meta:['Camera VLAN'], text:'Security-class device. Belongs with the security lane and future camera footprint.', bullets:['Separate from generic IoT where possible.']}}
|
||||
,{id:'chimes', label:'Protect chimes', sub:'currently misplaced', x:190, y:405, r:17, color:palette.legacy, group:'legacy', tags:['all','migration','security'], stage:1,
|
||||
info:{title:'Protect chimes', meta:['Migration pain','Move now'], text:'Poster children for why Management should not be a junk drawer. Move these into Camera VLAN 60 first.', bullets:['One of the first obvious cleanups.']}}
|
||||
,{id:'futurecams', label:'Future cameras', sub:'PoE preferred', x:260, y:500, r:18, color:palette.camera, group:'device', tags:['all','security','topology'], stage:1,
|
||||
info:{title:'Future cameras', meta:['Growth justification'], text:'You already know more cameras are coming. That makes VLAN 60 real, not theoretical.', bullets:['Prefer wired PoE when practical.','This is why the security lane exists now.']}}
|
||||
,{id:'guests', label:'Visitors', sub:'internet only', x:255, y:640, r:16, color:palette.guest, group:'device', tags:['all','wifi'], stage:0,
|
||||
info:{title:'Guest devices', meta:['Guest VLAN'], text:'Boring by design. That is healthy.', bullets:['No lateral access.']}}
|
||||
,{id:'oldiot', label:'Old IoT / VLAN 2', sub:'kill this', x:345, y:780, r:20, color:palette.legacy, group:'legacy', tags:['all','migration'], stage:3,
|
||||
info:{title:'Old IoT / VLAN 2', meta:['Legacy pain','Retire completely'], text:'Historical scar tissue. Drain it in phases, then delete it and its stale profiles for good.', bullets:['Drain it in phases.','Delete once empty.']}}
|
||||
,{id:'ssids', label:'SSID cleanup', sub:'fewer, clearer names', x:990, y:95, r:18, color:palette.trusted, group:'legacy', tags:['all','wifi','migration'], stage:4,
|
||||
info:{title:'SSID cleanup', meta:['Wi-Fi policy cleanup'], text:'Long-term goal: one Trusted, one IoT, one Guest, one Security. Add more only if they solve a real problem.', bullets:['Either collapse duplicate trusted SSIDs or rename them to reflect real policy.']}}
|
||||
];
|
||||
|
||||
const baseLinks = [
|
||||
['operator','trusted'],['operator','mgmt'],['operator','servers'],
|
||||
['udm','mgmt'],['udm','trusted'],['udm','servers'],['udm','iot'],['udm','guest'],['udm','camera'],
|
||||
['prohd','udm'],['u7','prohd'],['u6','prohd'],
|
||||
['mgmt','prohd'],['mgmt','u7'],['mgmt','u6'],
|
||||
['trusted','phones'],['trusted','steamdeck'],['trusted','ssids'],
|
||||
['servers','pd'],['servers','serenity'],['servers','nomad'],['servers','rocinante'],
|
||||
['iot','ecobee'],['iot','google'],['iot','appliances'],['iot','oldiot','cross'],
|
||||
['camera','doorbell'],['camera','futurecams'],['camera','chimes','cross'],
|
||||
['guest','guests'],
|
||||
['mgmt','chimes','cross'],
|
||||
['trusted','servers','cross'],
|
||||
['u7','trusted','cross'],['u7','iot','cross'],['u7','guest','cross'],['u7','camera','cross'],
|
||||
['u6','trusted','cross optional-u6'],['u6','iot','cross optional-u6'],['u6','camera','cross optional-u6']
|
||||
];
|
||||
|
||||
const overlays = [
|
||||
{id:'topo-1', type:'topology', a:'operator', b:'udm', text:'admin path'},
|
||||
{id:'topo-2', type:'topology', a:'udm', b:'prohd', text:'main trunk'},
|
||||
{id:'topo-3', type:'topology', a:'prohd', b:'u7', text:'AP uplink'},
|
||||
{id:'topo-4', type:'topology', a:'prohd', b:'u6', text:'optional AP uplink', optional:true},
|
||||
{id:'topo-5', type:'topology', a:'prohd', b:'pd', text:'server uplink'},
|
||||
{id:'topo-6', type:'topology', a:'prohd', b:'rocinante', text:'server uplink'},
|
||||
{id:'flow-1', type:'flow allow', a:'trusted', b:'mgmt', text:'admin allowed'},
|
||||
{id:'flow-2', type:'flow allow', a:'trusted', b:'servers', text:'service/admin allowed'},
|
||||
{id:'flow-3', type:'flow allow', a:'camera', b:'servers', text:'Protect / NVR only'},
|
||||
{id:'flow-4', type:'flow allow', a:'iot', b:'servers', text:'specific local helpers'},
|
||||
{id:'flow-5', type:'flow deny', a:'guest', b:'mgmt', text:'blocked'},
|
||||
{id:'flow-6', type:'flow deny', a:'guest', b:'servers', text:'blocked'},
|
||||
{id:'flow-7', type:'flow deny', a:'iot', b:'mgmt', text:'blocked'},
|
||||
{id:'flow-8', type:'flow restrict', a:'google', b:'phones', text:'discovery exception pain'},
|
||||
{id:'mig-1', type:'migration', a:'chimes', b:'camera', text:'stage 1', stage:1},
|
||||
{id:'mig-2', type:'migration', a:'pd', b:'servers', text:'stage 2', stage:2},
|
||||
{id:'mig-3', type:'migration', a:'serenity', b:'servers', text:'stage 2', stage:2},
|
||||
{id:'mig-4', type:'migration', a:'nomad', b:'servers', text:'stage 2', stage:2},
|
||||
{id:'mig-5', type:'migration', a:'rocinante', b:'servers', text:'stage 2', stage:2},
|
||||
{id:'mig-6', type:'migration', a:'oldiot', b:'iot', text:'stage 3', stage:3},
|
||||
{id:'mig-7', type:'migration', a:'ecobee', b:'iot', text:'stage 3', stage:3},
|
||||
{id:'mig-8', type:'migration', a:'appliances', b:'iot', text:'stage 3', stage:3},
|
||||
{id:'mig-9', type:'migration', a:'google', b:'iot', text:'stage 3 last', stage:3},
|
||||
{id:'mig-10', type:'migration', a:'ssids', b:'u7', text:'stage 4', stage:4},
|
||||
{id:'mig-11', type:'migration', a:'ssids', b:'u6', text:'stage 4 option', stage:4, optional:true}
|
||||
];
|
||||
|
||||
const svgNodes = document.getElementById('nodes');
|
||||
const svgBaseLinks = document.getElementById('baseLinks');
|
||||
const layerLines = document.getElementById('layerLines');
|
||||
const layerLabels = document.getElementById('layerLabels');
|
||||
const infoBox = document.getElementById('infoBox');
|
||||
const stageCards = [...document.querySelectorAll('[data-stage-card]')];
|
||||
const nodeMap = Object.fromEntries(nodes.map(n => [n.id,n]));
|
||||
let selected = null;
|
||||
let mode = 'all';
|
||||
let u6Enabled = false;
|
||||
let activeStage = 0;
|
||||
const layerState = {topology:true, flow:true, migration:true};
|
||||
|
||||
function modeVisible(node){
|
||||
if (node.hidden && !u6Enabled) return false;
|
||||
if (mode === 'all') return true;
|
||||
if (mode === 'vlan') return node.group === 'vlan' || node.group === 'core';
|
||||
if (mode === 'migration') return node.tags.includes('migration') || node.group === 'legacy' || node.id === 'operator' || node.id === 'camera' || node.id === 'servers' || node.id === 'iot';
|
||||
if (mode === 'wifi') return node.tags.includes('wifi') || node.id === 'operator';
|
||||
if (mode === 'topology') return node.tags.includes('topology') || node.group === 'core' || node.group === 'vlan' || node.id === 'operator';
|
||||
if (mode === 'policy') return node.tags.includes('policy') || ['phones','google'].includes(node.id) || node.id === 'operator';
|
||||
return true;
|
||||
}
|
||||
|
||||
function stageVisible(item){
|
||||
return activeStage === 0 || !item.stage || item.stage === activeStage;
|
||||
}
|
||||
|
||||
function midpoint(a,b,dx=0,dy=0){
|
||||
return {x:(a.x+b.x)/2 + dx, y:(a.y+b.y)/2 + dy};
|
||||
}
|
||||
|
||||
function createNode(n){
|
||||
const g = document.createElementNS('http://www.w3.org/2000/svg','g');
|
||||
g.setAttribute('class','node');
|
||||
g.dataset.id = n.id;
|
||||
g.innerHTML = `
|
||||
<circle class="node-core" cx="${n.x}" cy="${n.y}" r="${n.r}" fill="${n.color}"></circle>
|
||||
<text class="node-label" x="${n.x}" y="${n.y-4}">${n.label}</text>
|
||||
<text class="node-sub" x="${n.x}" y="${n.y+14}">${n.sub}</text>
|
||||
`;
|
||||
g.addEventListener('click', () => selectNode(n.id));
|
||||
return g;
|
||||
}
|
||||
|
||||
function createBaseLink(link){
|
||||
const [a,b,klass=''] = link;
|
||||
const na = nodeMap[a], nb = nodeMap[b];
|
||||
const line = document.createElementNS('http://www.w3.org/2000/svg','line');
|
||||
line.setAttribute('x1', na.x); line.setAttribute('y1', na.y);
|
||||
line.setAttribute('x2', nb.x); line.setAttribute('y2', nb.y);
|
||||
line.setAttribute('class', `link ${klass}`.trim());
|
||||
line.dataset.a = a; line.dataset.b = b;
|
||||
return line;
|
||||
}
|
||||
|
||||
function createOverlay(overlay){
|
||||
const a = nodeMap[overlay.a], b = nodeMap[overlay.b];
|
||||
const line = document.createElementNS('http://www.w3.org/2000/svg','line');
|
||||
line.setAttribute('x1', a.x); line.setAttribute('y1', a.y);
|
||||
line.setAttribute('x2', b.x); line.setAttribute('y2', b.y);
|
||||
line.setAttribute('class', `layer-line ${overlay.type}`);
|
||||
line.dataset.a = overlay.a; line.dataset.b = overlay.b; line.dataset.overlay = overlay.id;
|
||||
|
||||
const label = document.createElementNS('http://www.w3.org/2000/svg','text');
|
||||
const bump = overlay.type.includes('migration') ? {x:0,y:-12} : overlay.type.includes('flow') ? {x:0,y:16} : {x:0,y:-16};
|
||||
const mid = midpoint(a,b,bump.x,bump.y);
|
||||
label.setAttribute('x', mid.x);
|
||||
label.setAttribute('y', mid.y);
|
||||
label.setAttribute('text-anchor','middle');
|
||||
label.setAttribute('class','layer-label');
|
||||
label.dataset.a = overlay.a; label.dataset.b = overlay.b; label.dataset.overlay = overlay.id;
|
||||
label.textContent = overlay.text;
|
||||
return {line,label};
|
||||
}
|
||||
|
||||
const nodeEls = new Map();
|
||||
const baseLinkEls = [];
|
||||
const overlayEls = [];
|
||||
nodes.forEach(n => { const el = createNode(n); nodeEls.set(n.id, el); svgNodes.appendChild(el); });
|
||||
baseLinks.forEach(l => { const el = createBaseLink(l); baseLinkEls.push(el); svgBaseLinks.appendChild(el); });
|
||||
overlays.forEach(o => {
|
||||
const pair = createOverlay(o);
|
||||
overlayEls.push({...pair, meta:o});
|
||||
layerLines.appendChild(pair.line);
|
||||
layerLabels.appendChild(pair.label);
|
||||
});
|
||||
|
||||
function isRelatedToSelected(a,b){
|
||||
return selected && (a === selected || b === selected);
|
||||
}
|
||||
|
||||
function renderInfo(n){
|
||||
if (!n) {
|
||||
infoBox.innerHTML = `<h3>Whole design</h3><p>Click a node in the graph. I will show what it is, what it should talk to, and where it sits in the migration sequence.</p><div class="meta"><span>Tip: click Mgmt</span><span>Tip: click Cameras</span><span>Tip: click Old IoT</span></div>`;
|
||||
return;
|
||||
}
|
||||
const stageText = n.stage ? `Stage ${n.stage}` : 'Always relevant';
|
||||
infoBox.innerHTML = `
|
||||
<h3>${n.info.title}</h3>
|
||||
<p>${n.info.text}</p>
|
||||
<div class="meta">${n.info.meta.map(m => `<span>${m}</span>`).join('')}<span>${stageText}</span></div>
|
||||
<ul>${n.info.bullets.map(b => `<li>${b}</li>`).join('')}</ul>
|
||||
`;
|
||||
}
|
||||
|
||||
function updateStageCards(){
|
||||
stageCards.forEach(card => {
|
||||
const stage = Number(card.dataset.stageCard);
|
||||
card.classList.toggle('active', activeStage === 0 || activeStage === stage);
|
||||
});
|
||||
}
|
||||
|
||||
function updateGraph(){
|
||||
nodeEls.forEach((el,id) => {
|
||||
const n = nodeMap[id];
|
||||
const visible = modeVisible(n) && stageVisible(n);
|
||||
el.style.display = visible ? 'block' : 'none';
|
||||
el.classList.toggle('active', selected === id);
|
||||
const related = selected && (id === selected || baseLinks.some(([a,b]) => (a===selected && b===id) || (b===selected && a===id)) || overlays.some(o => (o.a===selected && o.b===id) || (o.b===selected && o.a===id)));
|
||||
el.classList.toggle('dim', !!selected && !related);
|
||||
});
|
||||
|
||||
baseLinkEls.forEach(el => {
|
||||
const a = el.dataset.a, b = el.dataset.b;
|
||||
const optional = el.getAttribute('class').includes('optional-u6');
|
||||
const visible = modeVisible(nodeMap[a]) && modeVisible(nodeMap[b]) && stageVisible(nodeMap[a]) && stageVisible(nodeMap[b]) && (!optional || u6Enabled);
|
||||
el.style.display = visible ? 'block' : 'none';
|
||||
const related = isRelatedToSelected(a,b);
|
||||
el.classList.toggle('highlight', !!selected && related);
|
||||
el.classList.toggle('dim', !!selected && !related);
|
||||
});
|
||||
|
||||
overlayEls.forEach(({line,label,meta}) => {
|
||||
const a = nodeMap[meta.a], b = nodeMap[meta.b];
|
||||
const overlayType = meta.type.split(' ')[0];
|
||||
const layerOn = overlayType === 'topology' ? layerState.topology : overlayType === 'flow' ? layerState.flow : layerState.migration;
|
||||
const optional = !!meta.optional;
|
||||
const modeMatch = mode === 'topology' ? overlayType === 'topology' || overlayType === 'migration' : mode === 'policy' ? overlayType === 'flow' || overlayType === 'migration' : true;
|
||||
const visible = layerOn && modeMatch && modeVisible(a) && modeVisible(b) && stageVisible(meta) && stageVisible(a) && stageVisible(b) && (!optional || u6Enabled);
|
||||
line.style.display = visible ? 'block' : 'none';
|
||||
label.style.display = visible ? 'block' : 'none';
|
||||
const related = isRelatedToSelected(meta.a, meta.b);
|
||||
line.classList.toggle('dim', !!selected && !related);
|
||||
label.classList.toggle('dim', !!selected && !related);
|
||||
});
|
||||
|
||||
if (selected && (!modeVisible(nodeMap[selected]) || !stageVisible(nodeMap[selected]) || (nodeMap[selected].hidden && !u6Enabled))) {
|
||||
selected = null;
|
||||
renderInfo(null);
|
||||
}
|
||||
|
||||
updateStageCards();
|
||||
}
|
||||
|
||||
function selectNode(id){
|
||||
selected = selected === id ? null : id;
|
||||
renderInfo(selected ? nodeMap[selected] : null);
|
||||
updateGraph();
|
||||
localStorage.setItem('network-web-selected', selected || '');
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-mode]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('[data-mode]').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
mode = btn.dataset.mode;
|
||||
localStorage.setItem('network-web-mode', mode);
|
||||
updateGraph();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-layer]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const layer = btn.dataset.layer;
|
||||
layerState[layer] = !layerState[layer];
|
||||
btn.classList.toggle('active', layerState[layer]);
|
||||
localStorage.setItem(`network-web-layer-${layer}`, layerState[layer] ? '1' : '0');
|
||||
updateGraph();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-stage]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('[data-stage]').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
activeStage = Number(btn.dataset.stage);
|
||||
localStorage.setItem('network-web-stage', String(activeStage));
|
||||
updateGraph();
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('u6Toggle').addEventListener('click', (e) => {
|
||||
u6Enabled = !u6Enabled;
|
||||
e.currentTarget.classList.toggle('active', u6Enabled);
|
||||
e.currentTarget.textContent = u6Enabled ? 'Hide downstairs U6 LR' : 'Toggle downstairs U6 LR';
|
||||
localStorage.setItem('network-web-u6', u6Enabled ? '1' : '0');
|
||||
updateGraph();
|
||||
});
|
||||
|
||||
const savedMode = localStorage.getItem('network-web-mode');
|
||||
if (savedMode) {
|
||||
const btn = [...document.querySelectorAll('[data-mode]')].find(b => b.dataset.mode === savedMode);
|
||||
if (btn) btn.click();
|
||||
}
|
||||
|
||||
['topology','flow','migration'].forEach(layer => {
|
||||
const saved = localStorage.getItem(`network-web-layer-${layer}`);
|
||||
if (saved === '0') {
|
||||
layerState[layer] = false;
|
||||
const btn = document.querySelector(`[data-layer="${layer}"]`);
|
||||
if (btn) btn.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
const savedStage = localStorage.getItem('network-web-stage');
|
||||
if (savedStage) {
|
||||
const btn = [...document.querySelectorAll('[data-stage]')].find(b => b.dataset.stage === savedStage);
|
||||
if (btn) btn.click();
|
||||
}
|
||||
|
||||
u6Enabled = localStorage.getItem('network-web-u6') === '1';
|
||||
if (u6Enabled) {
|
||||
const btn = document.getElementById('u6Toggle');
|
||||
btn.classList.add('active');
|
||||
btn.textContent = 'Hide downstairs U6 LR';
|
||||
}
|
||||
|
||||
const savedSelected = localStorage.getItem('network-web-selected');
|
||||
if (savedSelected && nodeMap[savedSelected]) selected = savedSelected;
|
||||
renderInfo(selected ? nodeMap[selected] : null);
|
||||
updateGraph();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user