diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-04 16:46:48 -0400 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-04 16:46:48 -0400 |
| commit | 518ffd7578dbc74689b5303a35f402bfe081aa91 (patch) | |
| tree | e22784c9b34334b69f6f6074c010ce289c129134 /assets/2026-07-03-instrument-console-panels-prototype.html | |
| parent | 9945ad041fca214c2f6c761ce9fd1ccf1759a8ac (diff) | |
| download | archsetup-main.tar.gz archsetup-main.zip | |
I gathered all five self-contained HTML/CSS design prototypes into one home: the instrument-console pair (moved from assets/), plus the net-panel rescan, sound panel, widget gallery, and waybar redesign (moved out of working/). Added a README index and updated every inbound link: build summary, the instrument-console and audio specs, and todo.org.
Also fixed a broken link the earlier sort left in the build summary. It still pointed at the instrument-console spec's old docs/design/ path after the move to docs/specs/.
Diffstat (limited to 'assets/2026-07-03-instrument-console-panels-prototype.html')
| -rw-r--r-- | assets/2026-07-03-instrument-console-panels-prototype.html | 1359 |
1 files changed, 0 insertions, 1359 deletions
diff --git a/assets/2026-07-03-instrument-console-panels-prototype.html b/assets/2026-07-03-instrument-console-panels-prototype.html deleted file mode 100644 index 0258f20..0000000 --- a/assets/2026-07-03-instrument-console-panels-prototype.html +++ /dev/null @@ -1,1359 +0,0 @@ -<!doctype html> -<html lang="en"> -<head> -<meta charset="utf-8"> -<meta name="viewport" content="width=device-width, initial-scale=1"> -<title>Instrument consoles — network + bluetooth</title> -<style> -:root { - --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917; - --gold:#dab53d; --gold-hi:#ffd75f; --silver:#bfc4d0; --cream:#f3e7c5; - --steel:#969385; --dim:#7c838a; --slate:#424f5e; --slate-hi:#54677d; - --wash:#2c2f32; --pass:#74932f; --fail:#cb6b4d; - --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace; -} -*{box-sizing:border-box;margin:0;padding:0} -html{background:var(--ground)} -body{font-family:var(--mono);color:var(--silver);padding:2.4rem 2rem 4rem;line-height:1.45; - background:radial-gradient(1200px 600px at 70% -10%,#1c1915 0%,transparent 60%),var(--ground)} -.masthead{max-width:1280px;margin:0 auto 1.8rem} -.eyebrow{color:var(--steel);font-size:.72rem;letter-spacing:.28em;text-transform:uppercase} -h1{color:var(--gold);font-size:1.5rem;margin:.35rem 0 .4rem} -.masthead p{color:var(--dim);font-size:.86rem;max-width:80ch} -.masthead b{color:var(--silver);font-weight:700} - -.stage{display:flex;gap:2.2rem;flex-wrap:wrap;max-width:1280px;margin:0 auto;align-items:flex-start} -.slot{width:400px} -.slot-label{color:var(--steel);font-size:.7rem;letter-spacing:.22em;text-transform:uppercase;margin:0 0 .55rem .2rem} -.aside{flex:1 1 300px;min-width:280px} -.aside h3{color:var(--steel);font-size:.7rem;letter-spacing:.24em;text-transform:uppercase;margin:1.1rem 0 .5rem} -.aside h3:first-child{margin-top:.2rem} -.aside ul{list-style:none} -.aside li{font-size:.82rem;padding:.22rem 0 .22rem 1.1rem;position:relative} -.aside li::before{content:"·";color:var(--gold);position:absolute;left:.25rem} -.aside li em{color:var(--dim);font-style:normal} -.aside li b{color:var(--cream);font-weight:700} -.demo-box{border:1px dashed var(--wash);border-radius:10px;padding:.8rem 1rem;margin-top:1rem} -.demo-box label{display:flex;gap:.6rem;align-items:center;font-size:.82rem;cursor:pointer;color:var(--silver);margin-top:.45rem} -.demo-box label:first-child{margin-top:0} -.demo-box input{accent-color:#dab53d} -.demo-box .hint{color:var(--dim);font-size:.73rem;margin:.15rem 0 0 1.5rem} -.reset{font:inherit;font-size:.78rem;color:var(--silver);background:transparent;border:1px solid var(--wash); - border-radius:8px;padding:.4rem .9rem;cursor:pointer;margin-top:.8rem} -.reset:hover{background:var(--wash)} - -.panel{background:var(--panel);border:2px solid var(--gold);border-radius:16px;padding:17px 19px; - box-shadow:0 18px 50px rgba(0,0,0,.55);font-size:13.5px;width:380px;position:relative; - transition:opacity .25s,transform .25s} -.panel.closed{opacity:0;transform:translateY(-8px);pointer-events:none} -.reopen{display:none;font:inherit;font-size:.75rem;color:var(--gold);background:transparent; - border:1px dashed var(--gold);border-radius:8px;padding:.5rem 1rem;cursor:pointer;margin-top:.6rem} - -.lamp{width:9px;height:9px;border-radius:50%;background:var(--pass);flex:0 0 auto; - box-shadow:0 0 6px 1px rgba(116,147,47,.55)} -.lamp.gold{background:var(--gold);box-shadow:0 0 6px 1px rgba(218,181,61,.55)} -.lamp.red{background:var(--fail);box-shadow:0 0 6px 1px rgba(203,107,77,.55)} -.lamp.off{background:var(--wash);box-shadow:none} -.lamp.busy{background:var(--gold);animation:pulse .7s ease-in-out infinite} -@keyframes pulse{50%{opacity:.25}} - -.b-face{background:var(--raise);border-radius:12px;border:1px solid #262320;padding:11px 14px} -.b-id{display:flex;align-items:center;gap:9px} -.b-id .state-word{color:var(--gold);font-weight:700;font-size:15px;letter-spacing:.12em} -.b-id .unit{color:var(--steel);font-size:.68rem;letter-spacing:.3em;margin-left:auto} -.badge{font-size:.62rem;letter-spacing:.18em;color:var(--panel);background:var(--gold); - border-radius:4px;padding:1px 6px;display:none} -.badge.show{display:inline-block} -.badge.red{background:var(--fail);color:var(--cream)} -.x-btn{margin-left:6px;color:var(--dim);border:0;background:transparent;font:inherit;font-size:1rem; - cursor:pointer;border-radius:50%;width:26px;height:26px;line-height:1;flex:0 0 auto} -.x-btn:hover{background:var(--wash);color:var(--silver)} -.switch{width:38px;height:20px;border-radius:10px;background:var(--wash); - border:1px solid var(--slate);position:relative;flex:0 0 auto;cursor:pointer} -.switch::after{content:"";position:absolute;top:2px;left:2px;width:14px;height:14px; - border-radius:50%;background:var(--dim);transition:left .15s} -.switch.on{background:var(--slate);border-color:var(--gold)} -.switch.on::after{left:19px;background:var(--gold)} - -.engrave{color:var(--steel);font-size:.64rem;letter-spacing:.32em;text-transform:uppercase; - display:flex;align-items:center;gap:10px;margin:12px 0 6px} -.engrave::before,.engrave::after{content:"";height:1px;background:var(--wash);flex:1} -.engrave::before{max-width:12px} -.engrave .act{color:var(--dim);letter-spacing:.06em;text-transform:none;font-size:.72rem;cursor:pointer} -.engrave .act:hover{color:var(--gold)} - -.chan .line1{display:flex;align-items:baseline;gap:9px} -.chan .ssid{color:var(--cream);font-weight:700;font-size:14.5px} -.chan .line2{color:var(--dim);font-size:11.5px;margin-top:2px} -.chan .chip{color:var(--dim);cursor:pointer;border-bottom:1px dotted var(--wash)} -.chan .chip:hover{color:var(--gold)} -.chan .chip.on{color:var(--gold)} -.ladder{display:inline-flex;gap:2px;align-items:flex-end;height:12px} -.ladder i{width:4px;background:var(--wash);border-radius:1px} -.ladder i:nth-child(1){height:4px}.ladder i:nth-child(2){height:7px} -.ladder i:nth-child(3){height:10px}.ladder i:nth-child(4){height:12px} -.ladder.l1 i:nth-child(-n+1){background:var(--gold)} -.ladder.l2 i:nth-child(-n+2){background:var(--gold)} -.ladder.l3 i:nth-child(-n+3){background:var(--gold)} -.ladder.l4 i{background:var(--gold)} - -/* section row budgets: lists never grow the panel — they scroll inside it, - cut at a half row so the peek says "there's more" */ -.sec-scroll{overflow-y:auto;overscroll-behavior:contain} -#networks.sec-scroll{max-height:160px} -#tunnels.sec-scroll{max-height:131px} -#b-paired.sec-scroll{max-height:160px} -#b-nearby.sec-scroll{max-height:131px} -.engrave .cnt{color:var(--dim);letter-spacing:.12em;margin-left:2px} -.lamp-row{display:flex;align-items:center;gap:9px;padding:5px 6px;border-radius:7px;font-size:12.5px;cursor:pointer;position:relative} -.lamp-row:hover{background:var(--wash)} -.lamp-row .who{color:var(--silver);white-space:nowrap} -.lamp-row .who b{color:var(--cream)} -.lamp-row .what{margin-left:auto;color:var(--dim);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} -.lamp-row.busy{pointer-events:none} -.lamp-row .zap,.lamp-row .pen{display:none;color:var(--dim);border:0;background:transparent;font:inherit;font-size:.85rem; - cursor:pointer;border-radius:5px;padding:0 5px;flex:0 0 auto} -.lamp-row:hover .zap,.lamp-row:hover .pen{display:inline-block} -.lamp-row .zap:hover{color:var(--fail)} -.lamp-row .pen:hover{color:var(--gold)} -.lamp-row.armed-soft{background:rgba(218,181,61,.10)} -.lamp-row.armed-soft .what{color:var(--gold)} -.lamp-row.armed{background:rgba(203,107,77,.12)} -.lamp-row.armed .what{color:var(--fail)} -.lamp-row.armed .zap{display:inline-block;color:var(--fail)} - -.console-btns{display:flex;gap:8px;margin-top:2px} -.c-btn{flex:1;text-align:center;cursor:pointer;font:inherit;font-size:11.5px; - background:linear-gradient(180deg,#23211e,#191715);color:var(--silver); - border:1px solid #33302b;border-bottom-color:#0c0b0a;border-radius:8px;padding:8px 4px; - box-shadow:inset 0 1px 0 rgba(255,255,255,.04),0 2px 3px rgba(0,0,0,.4)} -.c-btn:hover{color:var(--gold);border-color:var(--gold)} -.c-btn:active{transform:translateY(1px)} -.c-btn:disabled{opacity:.4;pointer-events:none} - -.meters{display:flex;gap:12px;margin-top:10px} -.meter{flex:1;background:var(--well);border:1px solid var(--wash);border-radius:10px;padding:9px 10px 7px;cursor:default;position:relative} -.meter.testing{animation:flash 1s ease-in-out infinite;border-color:var(--gold)} -@keyframes flash{50%{box-shadow:0 0 10px 1px rgba(218,181,61,.35)}} -.meter.held{border-color:var(--gold);cursor:pointer} -.meter .hold-tag{display:none;position:absolute;top:6px;right:8px;font-size:.56rem;letter-spacing:.2em; - color:var(--panel);background:var(--gold);border-radius:3px;padding:0 4px} -.meter .mode-tag{position:absolute;top:6px;left:8px;font-size:.56rem;letter-spacing:.2em;color:var(--pass)} -.meter .mode-tag.test{color:var(--gold)} -.meter .mode-tag.off{color:var(--dim)} -.meter.held .hold-tag{display:block} -.meter .dial{position:relative;height:52px;overflow:hidden;margin-top:13px} -.meter .arc{position:absolute;inset:0 0 -52px 0;border:2px solid var(--wash);border-radius:50%} -.meter .tick{position:absolute;left:50%;bottom:0;width:1.5px;height:10px;background:var(--steel);transform-origin:50% 52px} -.meter .needle{position:absolute;left:50%;bottom:0;width:2px;height:44px;background:var(--gold-hi); - transform-origin:50% 100%;transform:rotate(-60deg);border-radius:2px; - box-shadow:0 0 6px rgba(255,215,95,.5);transition:transform .45s cubic-bezier(.3,1.3,.5,1)} -.meter .needle.dead{background:var(--wash);box-shadow:none} -.meter .needle.low{background:var(--fail);box-shadow:0 0 6px rgba(203,107,77,.5)} -.meter .hub{position:absolute;left:50%;bottom:-4px;width:9px;height:9px;margin-left:-4.5px;border-radius:50%;background:var(--gold)} -.meter .m-value{color:var(--cream);font-size:13px;text-align:center;font-weight:700;margin-top:6px;font-variant-numeric:tabular-nums} -.meter .m-value small{color:var(--dim);font-weight:400} -.meter .m-value.low{color:var(--fail)} -.meter .m-label{color:var(--steel);font-size:.62rem;letter-spacing:.26em;text-align:center;margin-top:2px; - white-space:nowrap;overflow:hidden;text-overflow:ellipsis} -.meter-note{color:var(--dim);font-size:10px;text-align:center;margin-top:5px;min-height:1.2em} - -.well{border:1px solid var(--wash);border-radius:10px;background:var(--well)} -.outwrap{position:relative;margin-top:10px} -.outwrap .output{margin-top:0} -.o-clear{display:none;position:absolute;top:3px;right:5px;z-index:2;color:var(--dim); - border:0;background:var(--well);font:inherit;font-size:.8rem;cursor:pointer; - border-radius:50%;width:20px;height:20px;line-height:1} -.o-clear:hover{color:var(--silver);background:var(--wash)} -.outwrap.has .o-clear{display:block} -.output{margin-top:10px;padding:8px 10px;max-height:170px;overflow-y:auto;font-size:11.5px} -.output:empty{padding:4px 10px;min-height:10px} -.o-step{display:flex;gap:8px;align-items:flex-start;padding:2.5px 0} -.o-step .lamp{margin-top:4px;width:7px;height:7px} -.o-step .t b{color:var(--cream);font-weight:700} -.o-step .t .why{color:var(--dim);display:block;font-size:10.5px} -.o-step .t .ev{color:var(--steel);display:block;font-size:11px} -.o-step.repair .t b{color:var(--gold)} -.o-line{padding:2px 0;color:var(--silver)} -.o-line b{color:var(--steel);font-weight:400} -.o-verdict{margin-top:5px;padding-top:5px;border-top:1px solid var(--wash);color:var(--gold);font-weight:700} -.o-verdict.ok{color:var(--pass)} -.o-tip{color:var(--dim);font-size:10.5px;margin-top:4px} - -.toast{margin-top:9px;font-size:11px;color:var(--cream);background:var(--slate);border-radius:7px; - padding:5px 10px;opacity:0;transition:opacity .25s;min-height:1.4em} -.toast.show{opacity:1} -.toast.err{background:transparent;border:1px solid var(--fail);color:var(--fail)} - -.overlay{position:absolute;inset:0;background:rgba(10,12,13,.82);border-radius:14px;display:none; - align-items:center;justify-content:center;z-index:5} -.overlay.show{display:flex} -.dlg{background:var(--panel);border:1px solid var(--gold);border-radius:12px;padding:16px 18px;width:300px} -.dlg h4{color:var(--cream);font-size:13px;margin-bottom:4px} -.dlg .sub{color:var(--dim);font-size:11px;margin-bottom:10px} -.dlg .passkey{color:var(--gold-hi);font-size:22px;font-weight:700;letter-spacing:.18em; - text-align:center;margin:6px 0 12px;font-variant-numeric:tabular-nums} -.dlg input{width:100%;font:inherit;font-size:12.5px;color:var(--silver);background:var(--well); - border:1px solid var(--wash);border-radius:7px;padding:7px 9px;margin-bottom:8px;caret-color:var(--gold)} -.dlg input:focus{outline:none;border-color:var(--gold)} -.dlg .dlg-btns{display:flex;gap:8px;justify-content:flex-end;margin-top:4px} -.btn{font:inherit;font-size:12px;cursor:pointer;background:var(--slate);color:var(--cream); - border:1px solid var(--gold);border-radius:8px;padding:5px 12px} -.btn:hover{background:var(--slate-hi)} -.btn.quiet{background:transparent;border-color:var(--wash);color:var(--silver)} -.btn.quiet:hover{background:var(--wash)} - -*{scrollbar-width:thin;scrollbar-color:var(--slate) transparent} -::-webkit-scrollbar{width:6px;height:6px} -::-webkit-scrollbar-track{background:transparent} -::-webkit-scrollbar-thumb{background:var(--slate);border-radius:4px} -::-webkit-scrollbar-thumb:hover{background:var(--slate-hi)} -@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}} -</style> -</head> -<body> - -<header class="masthead"> - <div class="eyebrow">archsetup · dupre panel family · instrument consoles</div> - <h1>Network + Bluetooth — the pair</h1> - <p>Same faceplate, same idioms: lamp rows act on click, hover reveals ✎ rename and the - arm-to-fire ✕, gauges under the console keys (throughput needles on NET, battery fuel - gauges on BT), doctor streams and repairs in the output well. <b>Try the power switch - on BT·01</b> — everything follows it.</p> -</header> - -<div class="stage"> - - <!-- ============================ NET·01 ============================ --> - <div class="slot"> - <div class="slot-label">net·01 — as iterated</div> - <div class="panel" id="p"> - <div class="overlay" id="ov"> - <div class="dlg"> - <h4 id="dlg-title">Join network</h4> - <div class="sub" id="dlg-sub">WPA2 — password required</div> - <input id="dlg-ssid" placeholder="SSID" style="display:none"> - <input id="dlg-pass" type="password" placeholder="password"> - <div class="dlg-btns"> - <button class="btn quiet" onclick="dlgClose()">Cancel</button> - <button class="btn" id="dlg-go" onclick="dlgGo()">Join</button> - </div> - </div> - </div> - - <div class="b-face"> - <div class="b-id"> - <span class="lamp" id="lamp"></span> - <span class="state-word" id="state">ONLINE</span> - <span class="badge" id="badge">TUNNEL</span> - <span class="badge" id="air-badge">AIRPLANE</span> - <span class="unit">NET·01</span> - <span class="switch on" id="n-power" onclick="wifiPower()" title="WiFi radio"></span> - <button class="x-btn" onclick="closePanel()" title="Close (Esc)">✕</button> - </div> - </div> - - <div class="engrave">channel</div> - <div class="chan"> - <div class="line1"><span class="ssid" id="ch-ssid">@Hyatt_WiFi</span> - <span class="ladder l3" id="ch-ladder"><i></i><i></i><i></i><i></i></span> - <span class="dim" style="font-size:11px;white-space:nowrap" id="ch-dbm">-59 dBm · 44 ms</span></div> - <div class="line2" id="ch-route">172.20.2.108/20 · gw 172.20.0.1 · route wlp170s0</div> - </div> - - <div class="engrave">networks<span class="cnt" id="net-count"></span><span class="act" onclick="dlgHidden()" title="Join a hidden SSID">+ hidden</span></div> - <div id="networks" class="sec-scroll"></div> - - <div class="engrave">tunnels<span class="cnt" id="tun-count"></span></div> - <div id="tunnels" class="sec-scroll"></div> - - <div class="engrave">console</div> - <div class="console-btns"> - <button class="c-btn" id="b-doctor" onclick="runDoctor()">DOCTOR</button> - <button class="c-btn" id="b-speed" onclick="runSpeed()">SPEED TEST</button> - </div> - - <div class="meters"> - <div class="meter" id="m-rx" onclick="release('rx')"> - <span class="mode-tag" id="mt-rx">LIVE</span> - <span class="hold-tag">HOLD</span> - <div class="dial"><div class="arc"></div> - <div class="tick" style="transform:rotate(-60deg)"></div><div class="tick" style="transform:rotate(-30deg)"></div> - <div class="tick" style="transform:rotate(0)"></div><div class="tick" style="transform:rotate(30deg)"></div> - <div class="tick" style="transform:rotate(60deg)"></div> - <div class="needle" id="n-rx"></div><div class="hub"></div></div> - <div class="m-value"><span id="v-rx">0.1</span> <small>Mbps</small></div> - <div class="m-label">RX · DOWN</div> - </div> - <div class="meter" id="m-tx" onclick="release('tx')"> - <span class="mode-tag" id="mt-tx">LIVE</span> - <span class="hold-tag">HOLD</span> - <div class="dial"><div class="arc"></div> - <div class="tick" style="transform:rotate(-60deg)"></div><div class="tick" style="transform:rotate(-30deg)"></div> - <div class="tick" style="transform:rotate(0)"></div><div class="tick" style="transform:rotate(30deg)"></div> - <div class="tick" style="transform:rotate(60deg)"></div> - <div class="needle" id="n-tx"></div><div class="hub"></div></div> - <div class="m-value"><span id="v-tx">0.1</span> <small>Mbps</small></div> - <div class="m-label">TX · UP</div> - </div> - </div> - <div class="meter-note" id="m-note"></div> - - <div class="outwrap" id="outwrap"> - <button class="o-clear" onclick="clearOut('out')" title="Dismiss results">✕</button> - <div class="well output" id="out"></div> - </div> - <div class="toast" id="toastEl"></div> - </div> - <button class="reopen" id="reopen" onclick="openPanel()">reopen NET·01 (bar click)</button> - </div> - - <!-- ============================ BT·01 ============================ --> - <div class="slot"> - <div class="slot-label">bt·01 — new, same idioms</div> - <div class="panel" id="bp"> - <div class="overlay" id="bov"> - <div class="dlg"> - <h4 id="bdlg-title">Pair device</h4> - <div class="sub" id="bdlg-sub">confirm the passkey matches the device</div> - <div class="passkey" id="bdlg-key" style="display:none">847 291</div> - <input id="bdlg-name" placeholder="device name" style="display:none"> - <div class="dlg-btns"> - <button class="btn quiet" onclick="bdlgClose()">Cancel</button> - <button class="btn" id="bdlg-go" onclick="bdlgGo()">Confirm</button> - </div> - </div> - </div> - - <div class="b-face"> - <div class="b-id"> - <span class="lamp" id="b-lamp"></span> - <span class="state-word" id="b-state">POWERED</span> - <span class="badge red" id="b-badge">LOW BATT</span> - <span class="badge" id="b-air-badge">AIRPLANE</span> - <span class="unit">BT·01</span> - <span class="switch on" id="b-power" onclick="btPower()" title="Adapter power"></span> - <button class="x-btn" onclick="closeBt()" title="Close (Esc)">✕</button> - </div> - </div> - - <div class="engrave">adapter</div> - <div class="chan"> - <div class="line1"><span class="ssid">intel ax211</span> - <span class="dim" style="font-size:11px;margin-left:auto">hci0</span></div> - <div class="line2" id="b-adapter-line"> - <span class="chip" id="b-disco" onclick="btDisco()">discoverable off</span> - <span> · </span><span id="b-conn-count">1 device connected</span></div> - </div> - - <div class="engrave">paired<span class="cnt" id="b-paired-count"></span></div> - <div id="b-paired" class="sec-scroll"></div> - - <div class="engrave">nearby<span class="cnt" id="b-nearby-count"></span><span class="act" id="b-scan-note"></span></div> - <div id="b-nearby" class="sec-scroll"></div> - - <div class="engrave">console</div> - <div class="console-btns"> - <button class="c-btn" id="bb-doctor" onclick="btDoctor()">DOCTOR</button> - <button class="c-btn" id="bb-scan" onclick="btScan()">SCAN</button> - </div> - - <div class="meters"> - <div class="meter" id="bm-0"> - <span class="mode-tag" id="bmt-0">LIVE</span> - <div class="dial"><div class="arc"></div> - <div class="tick" style="transform:rotate(-60deg)"></div><div class="tick" style="transform:rotate(-30deg)"></div> - <div class="tick" style="transform:rotate(0)"></div><div class="tick" style="transform:rotate(30deg)"></div> - <div class="tick" style="transform:rotate(60deg)"></div> - <div class="needle" id="bn-0"></div><div class="hub"></div></div> - <div class="m-value" id="bvw-0"><span id="bv-0">72</span> <small>%</small></div> - <div class="m-label" id="bl-0">LOGI M650</div> - </div> - <div class="meter" id="bm-1"> - <span class="mode-tag off" id="bmt-1">—</span> - <div class="dial"><div class="arc"></div> - <div class="tick" style="transform:rotate(-60deg)"></div><div class="tick" style="transform:rotate(-30deg)"></div> - <div class="tick" style="transform:rotate(0)"></div><div class="tick" style="transform:rotate(30deg)"></div> - <div class="tick" style="transform:rotate(60deg)"></div> - <div class="needle dead" id="bn-1"></div><div class="hub"></div></div> - <div class="m-value" id="bvw-1"><span id="bv-1">—</span></div> - <div class="m-label" id="bl-1">NO DEVICE</div> - </div> - </div> - <div class="meter-note">battery · connected devices</div> - - <div class="outwrap" id="b-outwrap"> - <button class="o-clear" onclick="clearOut('b-out')" title="Dismiss results">✕</button> - <div class="well output" id="b-out"></div> - </div> - <div class="toast" id="b-toastEl"></div> - </div> - <button class="reopen" id="b-reopen" onclick="openBt()">reopen BT·01 (bar click)</button> - </div> - - <!-- ============================ NOTES ============================ --> - <div class="aside"> - <h3>The bt mapping</h3> - <ul> - <li><b>Power switch on the faceplate</b> — flip it: devices drop, gauges die, keys disable, state goes OFF. Flip back: the mouse auto-reconnects. <em>(the switch-placement ask, in console form)</em></li> - <li><b>Battery fuel gauges</b> are BT's meters — one per connected device, needle at charge, red under 15% with a LOW BATT badge on the faceplate.</li> - <li><b>Paired rows toggle on click</b> (connect/disconnect), exactly like tunnels. Hover: ✎ rename <em>(the rename ask)</em>, ✕ arm-to-forget.</li> - <li><b>Nearby rows pair on click</b> — passkey confirm dialog, then the device moves up to PAIRED and connects. SCAN refreshes the neighborhood.</li> - <li><b>discoverable off</b> in the adapter line is a click-toggle (gold when on).</li> - <li><b>Disconnect is arm-first on the active row</b> — first click arms in gold ("disconnect? click again"), second fires. Gold, not terracotta: disruptive, not destructive.</li> - <li><b>NET·01 grew the wifi radio switch</b> (faceplate, same spot as BT's). Airplane mode is system-level: both switches drop, AIRPLANE badges light, and a switch flipped under airplane refuses with the way out. A plugged ethernet cable keeps NET·01 online through it.</li> - <li><b>DOCTOR does it all here too</b>: adapter → radio → service → powered → devices → audio profile. Tick the degraded-audio switch and run it: it finds HSP, flips to A2DP, verifies the sink followed.</li> - </ul> - <div class="demo-box"> - <label><input type="checkbox" id="cafe" onchange="setScenario(this.checked)"> net: walk into a new café</label> - <label><input type="checkbox" id="breakdns"> net: broken hotel DNS (then DOCTOR)</label> - <label><input type="checkbox" id="ethercb" onchange="setEther(this.checked)"> net: plug in an ethernet cable</label> - <label><input type="checkbox" id="aircb" onchange="setAirplane(this.checked)"> both: airplane mode (Super+Shift+A)</label> - <label><input type="checkbox" id="airportcb" onchange="setAirport(this.checked)"> both: airport terminal (crowded airspace)</label> - <label><input type="checkbox" id="lowbatt" onchange="btLowBatt(this.checked)"> bt: mouse battery low</label> - <label><input type="checkbox" id="badaudio"> bt: degraded audio profile (then DOCTOR)</label> - <div class="hint">the audio one needs the headphones connected — click WH-1000XM4 first.</div> - <button class="reset" onclick="location.search=''">reset prototypes</button> - </div> - </div> -</div> - -<script> -const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches; -const $ = id => document.getElementById(id); -const T = f => reduced ? Math.max(10, f*0.02) : f; - -/* =========================================================== NET·01 */ -let busy = false; -const HOTEL_NETS = () => [ - {id:'hyatt', ssid:'@Hyatt_WiFi', sec:'WPA2', stored:true, range:true, sig:3, active:true}, - {id:'meeting', ssid:'Hyatt_Meeting', sec:'WPA2', stored:false, range:true, sig:3, active:false}, - {id:'roku', ssid:'DIRECT-roku-882',sec:'WPA2',stored:false, range:true, sig:2, active:false}, - {id:'xfinity', ssid:'xfinitywifi', sec:null, stored:false, range:true, sig:1, active:false}, - {id:'home', ssid:'HomeNet', sec:'WPA2', stored:true, range:false, sig:0, active:false}, -]; -const CAFE_NETS = () => [ - {id:'cafe5g', ssid:'CafeAmore_5G', sec:'WPA2', stored:false, range:true, sig:4, active:false, ip:'10.11.4.27/22 · gw 10.11.4.1'}, - {id:'cafeg', ssid:'CafeAmore_Guest',sec:null, stored:false, range:true, sig:3, active:false, ip:'10.11.8.102/22 · gw 10.11.8.1'}, - {id:'iot', ssid:'Neighbor_IoT', sec:'WPA2', stored:false, range:true, sig:1, active:false}, - {id:'hyatt', ssid:'@Hyatt_WiFi', sec:'WPA2', stored:true, range:false, sig:0, active:false}, - {id:'home', ssid:'HomeNet', sec:'WPA2', stored:true, range:false, sig:0, active:false}, -]; -const AIRPORT_NETS = () => [ - {id:'ord', ssid:'ORD Free Wi-Fi', sec:null, stored:false, range:true, sig:4, active:false, ip:'10.40.2.19/18 · gw 10.40.0.1'}, - {id:'boingo',ssid:'Boingo Hotspot', sec:null, stored:false, range:true, sig:3, active:false}, - {id:'united',ssid:'United_Club', sec:'WPA2', stored:false, range:true, sig:3, active:false}, - {id:'sky', ssid:'SkyClub_5G', sec:'WPA2', stored:false, range:true, sig:3, active:false}, - {id:'aa', ssid:'AmericanAir-Lounge',sec:'WPA2', stored:false, range:true, sig:2, active:false}, - {id:'sbux', ssid:'Starbucks WiFi', sec:null, stored:false, range:true, sig:2, active:false}, - {id:'tom', ssid:"Tom's iPhone", sec:'WPA2', stored:false, range:true, sig:2, active:false}, - {id:'hp', ssid:'HP-Print-88-Kiosk', sec:'WPA2', stored:false, range:true, sig:1, active:false}, - {id:'gate', ssid:'Gate B12 Display', sec:'WPA2', stored:false, range:true, sig:1, active:false}, - {id:'clear', ssid:'CLEAR-Kiosk', sec:'WPA2', stored:false, range:true, sig:1, active:false}, - {id:'tsa', ssid:'TSA-Ops', sec:'WPA2', stored:false, range:true, sig:1, active:false}, - {id:'dfw', ssid:'ORD-Employee', sec:'WPA2', stored:false, range:true, sig:1, active:false}, - {id:'hyatt', ssid:'@Hyatt_WiFi', sec:'WPA2', stored:true, range:false, sig:0, active:false}, - {id:'home', ssid:'HomeNet', sec:'WPA2', stored:true, range:false, sig:0, active:false}, -]; -const AIRPORT_NEARBY = () => [ - {id:'ap1', name:'AirPods Pro', kind:'audio', passkey:'118 402'}, - {id:'ap2', name:'AirPods (3rd gen)', kind:'audio', passkey:'220 981'}, - {id:'gb2', name:'Galaxy Buds2', kind:'audio', passkey:'914 555'}, - {id:'pbp', name:'Pixel Buds Pro', kind:'audio', passkey:'551 902'}, - {id:'xm5', name:'Sony WF-1000XM5', kind:'audio', passkey:'774 210'}, - {id:'jab', name:'Jabra Elite 7', kind:'audio', passkey:'333 190'}, - {id:'aw', name:'Apple Watch', kind:'wearable',passkey:'602 118'}, - {id:'gar', name:'Garmin Fenix 8', kind:'wearable',passkey:'488 007'}, - {id:'tile', name:'Tile Tracker', kind:'tracker', passkey:'150 129'}, - {id:'jbl2', name:'JBL Charge 5', kind:'audio', passkey:'847 291'}, - {id:'bose', name:'Bose QC Ultra', kind:'audio', passkey:'962 340'}, - {id:'gate2',name:'[TV] Gate B12', kind:'display', passkey:'302 118'}, -]; -let NETS = HOTEL_NETS(); -const WG = (id, who) => ({id, who, upWhat:'10.2.0.2/32 · route owner', - downWhat:'wireguard (NM) · down', up:false, ownsRoute:true, dev:'wgpvpn'}); -const TUNNELS = [ - {id:'ts', who:'tailscale · velox', upWhat:'100.127.238.103 · 4/6 peers', downWhat:'down', - up:true, ownsRoute:false, dev:'tailscale0'}, - WG('usny','USNY'), WG('usdc','USDC'), WG('uscala','USCALA'), WG('uscasf','USCASF'), - WG('usgaat','USGAAT'), WG('szur1','switzerlan-zurich1'), WG('szur2','switzerlan-zurich2'), - {id:'proton', who:'Proton VPN CLI', upWhat:'', downWhat:'down', up:false, needsLogin:true}, -]; -const tinit = () => ({ts:true, usny:false, usdc:false, uscala:false, uscasf:false, - usgaat:false, szur1:false, szur2:false, proton:false}); -const tstate = tinit(); -let connected = true; -let routeBase = '172.20.2.108/20 · gw 172.20.0.1'; -let ether = { present:false, routed:false, - ip:'172.20.7.44/20 · gw 172.20.0.1', dev:'enp3s0' }; -let armed = null, armTimer = null; -let wifiOn = true, airplane = false; -let armedDisc = null, armDiscTimer = null; -let lastSsid = '@Hyatt_WiFi'; - -function renderNets(){ - const host = $('networks'); host.innerHTML = ''; - if (ether.present){ - const row = document.createElement('div'); - row.className = 'lamp-row'; - row.innerHTML = `<span class="${ether.routed?'lamp':'lamp gold'}" id="eth-lamp"></span>`+ - `<span class="who">${ether.routed?'<b>'+ether.dev+'</b>':ether.dev}</span>`+ - `<span class="what" id="eth-what">${ether.routed?'active · wired · 1.0 Gbps':'wired · standby'}</span>`; - row.onclick = () => toggleEther(); - host.appendChild(row); - } - if (!wifiOn){ - const note = document.createElement('div'); - note.className = 'lamp-row'; - note.style.cursor = 'default'; - note.innerHTML = `<span class="lamp off"></span><span class="who dim">wifi radio off</span>`+ - `<span class="what">${airplane ? 'airplane mode' : 'flip the switch to scan'}</span>`; - host.appendChild(note); - tips('networks'); - return; - } - const inRange = NETS.filter(n => n.range).sort((a,b) => (b.active-a.active) || (b.sig-a.sig)); - const out = NETS.filter(n => !n.range); - $('net-count').textContent = '· ' + inRange.length + ' in range'; - for (const n of [...inRange, ...out]){ - const row = document.createElement('div'); - row.className = 'lamp-row' + (armed===n.id ? ' armed' : '') - + (armedDisc===n.id ? ' armed-soft' : ''); - const lamp = n.active ? 'lamp' : (n.range ? 'lamp gold' : 'lamp off'); - const what = armed===n.id ? 'forget? click ✕ again' - : armedDisc===n.id ? 'disconnect? click again' - : n.active ? (ether.present && ether.routed ? 'connected · standby · ' : 'active · ') + (n.sec || 'open') - : !n.range ? 'stored · out of range' - : (n.stored ? 'stored · ' : '') + (n.sec || 'open') + ' · ' + [null,'22%','44%','61%','78%'][n.sig]; - row.innerHTML = `<span class="${lamp}"></span>`+ - `<span class="who">${n.active?'<b>'+n.ssid+'</b>':n.ssid}</span>`+ - (n.range && !n.active ? `<span class="ladder l${n.sig}" style="margin-left:6px"><i></i><i></i><i></i><i></i></span>` : '')+ - `<span class="what" id="nw-${n.id}">${what}</span>`+ - (n.stored ? `<button class="zap" title="Forget ${n.ssid}">✕</button>` : ''); - if (n.stored) row.querySelector('.zap').onclick = (e) => { e.stopPropagation(); armForget(n.id); }; - row.onclick = n.active ? () => armDisconnect(n.id) : () => joinNet(n.id); - host.appendChild(row); - } - tips('networks'); -} -function armDisconnect(id){ - if (busy) return; - const n = NETS.find(x=>x.id===id); - if (armedDisc === id){ // second click: disconnect - clearTimeout(armDiscTimer); armedDisc = null; - busy = true; - const what = $('nw-'+id); - if (what) what.textContent = 'disconnecting…'; - setTimeout(() => { - busy = false; - n.active = false; connected = false; - if (!(ether.present && ether.routed)) tstate.ts = false; - netFace(); - renderTunnels(); renderNets(); - toast('disconnected from ' + n.ssid); - }, T(1100)); - return; - } - armedDisc = id; renderNets(); // first click: arm (gold — disruptive, not destructive) - clearTimeout(armDiscTimer); - armDiscTimer = setTimeout(() => { armedDisc = null; renderNets(); }, 3000); -} -function armForget(id){ - const n = NETS.find(x=>x.id===id); - if (armed === id){ - clearTimeout(armTimer); armed = null; - NETS.splice(NETS.indexOf(n), 1); renderNets(); - toast(`${n.ssid} forgotten`); - return; - } - armed = id; renderNets(); - clearTimeout(armTimer); - armTimer = setTimeout(() => { armed = null; renderNets(); }, 3000); -} -let joining = null; -function joinNet(id){ - if (busy) return; - if (!wifiOn){ toast('wifi radio is off', true); return; } - const n = NETS.find(x=>x.id===id); - if (!n.range){ toast(n.ssid + ' is out of range', true); return; } - if (n.sec && !n.stored){ joining = n; dlgJoin(n); return; } - doJoin(n); -} -function doJoin(n){ - busy = true; - const what = $('nw-'+n.id); - if (what) what.textContent = 'joining…'; - lampState('JOINING','gold'); - setTimeout(() => { - NETS.forEach(x => x.active = false); - n.active = true; n.stored = true; - busy = false; - $('ch-ssid').textContent = n.ssid; - $('ch-dbm').textContent = ['','-82 dBm','-74 dBm','-63 dBm','-55 dBm'][n.sig] + ' · 41 ms'; - $('ch-ladder').className = 'ladder l'+n.sig; - connected = true; - lastSsid = n.ssid; - routeBase = n.ip || '172.20.2.108/20 · gw 172.20.0.1'; - netFace(); - if (!tstate.ts) tstate.ts = true; - renderTunnels(); renderNets(); - toast('joined ' + n.ssid + ' — saved for next time'); - }, T(1600)); -} -function dlgJoin(n){ - $('dlg-title').textContent = 'Join ' + n.ssid; - $('dlg-sub').textContent = n.sec + ' — password required'; - $('dlg-ssid').style.display = 'none'; - $('dlg-pass').value = ''; - $('ov').classList.add('show'); - $('dlg-pass').focus(); -} -function dlgHidden(){ - joining = 'hidden'; - $('dlg-title').textContent = 'Join hidden network'; - $('dlg-sub').textContent = 'SSID is not broadcast — enter it exactly'; - $('dlg-ssid').style.display = 'block'; $('dlg-ssid').value = ''; - $('dlg-pass').value = ''; - $('ov').classList.add('show'); - $('dlg-ssid').focus(); -} -function dlgClose(){ $('ov').classList.remove('show'); joining = null; } -function dlgGo(){ - if (joining === 'hidden'){ - const ssid = $('dlg-ssid').value.trim() || 'hidden-net'; - const n = {id:'h'+Date.now(), ssid, sec:'WPA2', stored:true, range:true, sig:2, active:false}; - NETS.splice(0,0,n); renderNets(); dlgClose(); doJoin(n); - return; - } - const n = joining; dlgClose(); if (n) doJoin(n); -} - -function renderTunnels(){ - const host = $('tunnels'); host.innerHTML = ''; - for (const t of TUNNELS){ - const up = tstate[t.id]; - const row = document.createElement('div'); - row.className = 'lamp-row'; - row.innerHTML = `<span class="${up?'lamp':'lamp off'}" id="tl-${t.id}"></span>`+ - `<span class="who">${t.who}</span><span class="what" id="tw-${t.id}">${up?(t.upWhat||'up'):t.downWhat}</span>`; - row.onclick = () => toggleTunnel(t.id); - host.appendChild(row); - } - $('tun-count').textContent = '· ' + TUNNELS.filter(t=>tstate[t.id]).length + ' up of ' + TUNNELS.length; - tips('tunnels'); - updateRoute(); -} -function updateRoute(){ - if (!connected && !(ether.present && ether.routed)){ - $('ch-ssid').textContent = wifiOn ? '— not connected' : '— wifi radio off'; - $('ch-ladder').style.display = 'none'; - $('ch-dbm').textContent = ''; - $('ch-route').textContent = !wifiOn ? (airplane ? 'airplane mode' : 'flip the radio switch to scan') - : 'join a network below'; - $('badge').classList.remove('show'); - return; - } - const owner = TUNNELS.find(t => t.ownsRoute && tstate[t.id]); - const wired = ether.present && ether.routed; - const base = wired ? ether.ip : routeBase; - const dev = wired ? ether.dev : 'wlp170s0'; - $('ch-route').textContent = base + ' · route ' + (owner ? owner.dev + ' (tunnel)' : dev); - $('badge').classList.toggle('show', !!owner); - /* channel headline follows the routed link */ - if (wired){ - $('ch-ssid').textContent = ether.dev; - $('ch-ladder').style.display = 'none'; - $('ch-dbm').textContent = 'wired · 1.0 Gbps full-duplex'; - } else if (connected){ - const act = NETS.find(n => n.active); - if (act){ - $('ch-ssid').textContent = act.ssid; - $('ch-ladder').style.display = ''; - $('ch-ladder').className = 'ladder l' + act.sig; - $('ch-dbm').textContent = ['','-82 dBm','-74 dBm','-63 dBm','-55 dBm'][act.sig] + ' · 44 ms'; - } - } else { - $('ch-ssid').textContent = wifiOn ? '— not connected' : '— wifi radio off'; - $('ch-ladder').style.display = 'none'; - $('ch-dbm').textContent = ''; - } -} -/* the faceplate state word, derived from one place */ -function netFace(){ - const wired = ether.present && ether.routed; - $('air-badge').classList.toggle('show', airplane); - if (wired){ lampState('ONLINE'); return; } - if (connected){ lampState('ONLINE'); return; } - if (airplane){ lampState('AIRPLANE','gold'); return; } - if (!wifiOn){ lampState('OFF','off'); $('lamp').className='lamp off'; return; } - lampState('OFFLINE','red'); -} -function toggleTunnel(id){ - if (busy) return; - const t = TUNNELS.find(x=>x.id===id), up = tstate[id]; - const lamp = $('tl-'+id), what = $('tw-'+id); - busy = true; - lamp.className = 'lamp busy'; - what.textContent = t.needsLogin && !up ? 'connecting…' : up ? 'bringing down…' : 'bringing up…'; - setTimeout(() => { - busy = false; - if (t.needsLogin && !up){ - lamp.className = 'lamp red'; - what.textContent = 'sign in first: protonvpn login'; - toast('Proton: no account signed in — run: protonvpn login', true); - setTimeout(() => { lamp.className = 'lamp off'; what.textContent = t.downWhat; }, 2600); - return; - } - tstate[id] = !up; - renderTunnels(); - toast(tstate[id] ? `${t.who} up` + (t.ownsRoute ? ' — default route moved to '+t.dev : '') - : `${t.who} down` + (t.ownsRoute ? ' — route back on wlp170s0' : '')); - }, T(1500)); -} - -let toastTimer; -function toast(msg, err){ - const el = $('toastEl'); - el.textContent = msg; el.className = 'toast show' + (err ? ' err' : ''); - clearTimeout(toastTimer); - toastTimer = setTimeout(() => el.className = 'toast', err ? 4200 : 2600); -} -function lampState(word, cls){ - $('state').textContent = word; - $('lamp').className = 'lamp' + (cls && cls !== 'off' ? ' '+cls : cls === 'off' ? ' off' : ''); -} - -const held = {rx:false, tx:false}; -let testing = false, amb = 0; -function meter(side, val){ - const deg = -60 + Math.min(1, val/100) * 120; - $('n-'+side).style.transform = `rotate(${deg}deg)`; - $('v-'+side).textContent = val.toFixed(1); -} -function setMeterMode(side, mode){ - const m = $('m-'+side), t = $('mt-'+side); - m.classList.toggle('testing', mode==='test'); - m.classList.toggle('held', mode==='hold'); - held[side] = mode==='hold'; - t.textContent = mode==='live' ? 'LIVE' : 'TEST'; - t.className = 'mode-tag' + (mode==='live' ? '' : ' test'); -} -function release(side){ - if (!held[side]) return; - setMeterMode(side, 'live'); - if (!held.rx && !held.tx) $('m-note').textContent = ''; -} - -function runSpeed(){ - if (busy) return; - busy = true; testing = true; - $('b-doctor').disabled = $('b-speed').disabled = true; - const out = $('out'); out.innerHTML = ''; - setMeterMode('rx','test'); setMeterMode('tx','test'); - $('m-note').textContent = 'measuring — needles follow the live rate'; - const line = (k, v) => { - const el = document.createElement('div'); - el.className = 'o-line'; el.innerHTML = `<b>${k}</b> ${v}`; - out.appendChild(el); out.scrollTop = out.scrollHeight; - }; - setTimeout(() => line('location', 'Tulsa, OK (US) by Encore Communications'), T(900)); - setTimeout(() => line('ping', '44.5 ms (jitter 2.1 ms)'), T(1800)); - let dv = 0; const dT = 25.3; - setTimeout(() => { - const dTick = setInterval(() => { - dv = Math.min(dT, dv + 1.3 + Math.random()*2.1); - meter('rx', dv); - if (dv >= dT){ - clearInterval(dTick); - setMeterMode('rx','hold'); meter('rx', dT); - let uv = 0; const uT = 90.8; - const uTick = setInterval(() => { - uv = Math.min(uT, uv + 4.5 + Math.random()*7); - meter('tx', uv); - if (uv >= uT){ - clearInterval(uTick); - setMeterMode('tx','hold'); meter('tx', uT); - line('final', '25.3↓ 90.8↑ Mbps · 44.5 ms · loss 0.0%'); - const tip = document.createElement('div'); - tip.className = 'o-tip'; - tip.textContent = 'tip: download well below upload — typical of a congested or shaped venue network. Try 5 GHz, move closer, or retest off-peak.'; - out.appendChild(tip); out.scrollTop = out.scrollHeight; - $('m-note').textContent = 'result held — click a meter to go back to live'; - testing = false; busy = false; - $('b-doctor').disabled = $('b-speed').disabled = false; - } - }, T(200)); - } - }, T(185)); - }, T(2100)); -} - -const CHECKS = [ - {t:'Link', why:'is the adapter connected to a network (every later check rides the link)', - ev:'wlp170s0 connected (@Hyatt_WiFi)'}, - {t:'DHCP / IPv4', why:'did the network lease us an IP address (nothing routes without one)', - ev:'172.20.2.108/20'}, - {t:'Gateway', why:'does the router (first hop) answer a ping', ev:'172.20.0.1 [5 ms]'}, - {t:'DNS config', why:'is a DNS resolver configured on the link', ev:'172.20.0.1'}, - {t:'DNS resolution', why:'does a known hostname resolve (catches dead DNS and portal hijacks)', - ev:'names resolve (captive.apple.com) [48 ms]', - evBroken:'no resolution (portal may be stalling DNS)', canBreak:true}, - {t:'Internet', why:'does an HTTP probe reach the open internet (the online/captive verdict)', - ev:'open internet (HTTP 204) [112 ms]', - evBroken:'link up but no clean internet (DNS or egress issue)', canBreak:true}, -]; -const REPAIR = {t:'repair: dns-test', why:'points DNS at 1.1.1.1, tests resolution, then reverts (tells a broken venue resolver from blocked egress)', - ev:'1.1.1.1 resolved captive.apple.com — the hotel resolver is broken, not the link'}; -const REPAIR2 = {t:'repair: dns-override', why:'sets 1.1.1.1 on the link until reconnect (works around the broken venue resolver)', - ev:'DNS set to 1.1.1.1 on wlp170s0'}; - -function addStep(host, step, repair){ - const el = document.createElement('div'); - el.className = 'o-step' + (repair ? ' repair' : ''); - el.innerHTML = `<span class="lamp busy"></span><span class="t"><b>${step.t}</b>`+ - `<span class="why">${step.why}</span><span class="ev">…</span></span>`; - host.appendChild(el); host.scrollTop = host.scrollHeight; - return el; -} -function landStep(el, ev, cls){ - el.querySelector('.lamp').className = 'lamp' + (cls ? ' '+cls : ''); - el.querySelector('.ev').textContent = ev; - el.closest('.output').scrollTop = 1e6; -} -function verdict(host, text, ok){ - const v = document.createElement('div'); - v.className = 'o-verdict' + (ok ? ' ok' : ''); - v.textContent = text; - host.appendChild(v); host.scrollTop = host.scrollHeight; -} - -function runDoctor(){ - if (busy) return; - busy = true; - $('b-doctor').disabled = $('b-speed').disabled = true; - const brokenDNS = $('breakdns').checked; - const out = $('out'); out.innerHTML = ''; - lampState('CHECKING','gold'); - const gap = T(620); - let i = 0; - const next = () => { - if (i >= CHECKS.length){ return brokenDNS ? repairPhase() : finish('Overall — everything checks out'); } - const c = CHECKS[i]; - const el = addStep(out, c); - setTimeout(() => { - const broken = brokenDNS && c.canBreak; - landStep(el, broken ? c.evBroken : c.ev, broken ? 'red' : ''); - i++; setTimeout(next, gap*0.35); - }, gap); - }; - const repairPhase = () => { - lampState('FIXING','gold'); - verdict(out, 'DNS not resolving — trying the lightest repair'); - const r1 = addStep(out, REPAIR, true); - setTimeout(() => { - landStep(r1, REPAIR.ev); - const r2 = addStep(out, REPAIR2, true); - setTimeout(() => { - landStep(r2, REPAIR2.ev); - const re = addStep(out, {t:'re-check: Internet', why:'probe again through the repaired resolver', ev:''}); - setTimeout(() => { - landStep(re, 'open internet (HTTP 204) [96 ms]'); - $('breakdns').checked = false; - finish('fixed — back online after dns-override'); - }, gap); - }, gap*1.2); - }, gap*1.4); - }; - const finish = (text) => { - verdict(out, text, true); - lampState('ONLINE'); - busy = false; - $('b-doctor').disabled = $('b-speed').disabled = false; - }; - next(); -} - -function setEther(present){ - if (busy) return; - ether.present = present; - if (present){ - ether.routed = true; // cable wins the route by metric - lampState('ONLINE'); - toast('link detected on ' + ether.dev + ' — route moved to wired'); - } else { - ether.routed = false; - toast(connected ? 'cable unplugged — route back on wlp170s0' - : 'cable unplugged', !connected); - if (!connected) lampState('OFFLINE','red'); - } - renderTunnels(); renderNets(); -} -function toggleEther(){ - if (busy) return; - const lamp = $('eth-lamp'), what = $('eth-what'); - busy = true; - lamp.className = 'lamp busy'; - what.textContent = ether.routed ? 'standing by…' : 'taking the route…'; - setTimeout(() => { - busy = false; - ether.routed = !ether.routed; - renderTunnels(); renderNets(); - toast(ether.routed ? 'route moved to ' + ether.dev + ' (wired)' - : 'route back on wlp170s0 — ' + ether.dev + ' standing by'); - }, T(1200)); -} - -function wifiPower(){ - if (busy) return; - if (airplane){ toast('airplane mode is on — Super+Shift+A to leave it', true); return; } - setWifi(!wifiOn); -} -function setWifi(on, quiet){ - wifiOn = on; - $('n-power').classList.toggle('on', on); - if (!on){ - const act = NETS.find(n => n.active); - if (act){ lastSsid = act.ssid; act.active = false; } - connected = false; - if (!(ether.present && ether.routed)) tstate.ts = false; - netFace(); renderTunnels(); renderNets(); - if (!quiet) toast('wifi radio off'); - } else { - netFace(); renderTunnels(); renderNets(); - if (!quiet) toast('wifi radio on — rejoining ' + lastSsid); - const n = NETS.find(x => x.ssid === lastSsid && x.range); - if (n) setTimeout(() => doJoin(n), T(700)); - } -} -function setAirplane(on){ - airplane = on; - if (on){ - if (wifiOn) setWifi(false, true); - if (bpower) btPowerSet(false, true); - netFace(); - $('b-air-badge').classList.add('show'); - toast('airplane mode — all radios off'); - } else { - $('b-air-badge').classList.remove('show'); - setWifi(true, true); - btPowerSet(true, true); - netFace(); - toast('airplane mode off — radios back up'); - } -} - -function setAirport(on){ - if (busy || bbusy) return; - if (on && $('cafe').checked){ $('cafe').checked = false; } - NETS = on ? AIRPORT_NETS() : HOTEL_NETS(); - NEARBY.length = 0; - NEARBY.push(...(on ? AIRPORT_NEARBY() - : [{id:'jbl', name:'JBL Flip 6', kind:'audio', passkey:'847 291'}, - {id:'tv', name:'[TV] Samsung Q70', kind:'display', passkey:'302 118'}])); - if (on){ - tstate.ts = false; connected = false; - lampState('OFFLINE','red'); - toast('ORD concourse B — pick a network'); - } else { - tstate.ts = true; connected = true; - routeBase = '172.20.2.108/20 · gw 172.20.0.1'; - NETS.find(n=>n.id==='hyatt').active = true; - lampState('ONLINE'); - } - renderTunnels(); renderNets(); renderBt(); -} - -function setScenario(cafe){ - if (busy) return; - if (cafe && $('airportcb') && $('airportcb').checked){ $('airportcb').checked = false; } - NETS = cafe ? CAFE_NETS() : HOTEL_NETS(); - if (cafe){ - tstate.ts = false; connected = false; - $('ch-ssid').textContent = '— not connected'; - $('ch-dbm').textContent = ''; - $('ch-ladder').className = 'ladder'; - lampState('OFFLINE','red'); - } else { - tstate.ts = true; connected = true; - routeBase = '172.20.2.108/20 · gw 172.20.0.1'; - $('ch-ssid').textContent = '@Hyatt_WiFi'; - $('ch-dbm').textContent = '-59 dBm · 44 ms'; - $('ch-ladder').className = 'ladder l3'; - lampState('ONLINE'); - } - renderTunnels(); renderNets(); -} - -function closePanel(){ $('p').classList.add('closed'); $('reopen').style.display='inline-block'; } -function openPanel(){ $('p').classList.remove('closed'); $('reopen').style.display='none'; } - -/* =========================================================== BT·01 */ -let bbusy = false, bpower = true, bdisco = false, blow = false; -const DEVS = [ - {id:'m650', name:'Logi M650', kind:'mouse', paired:true, conn:true, batt:72, audio:false}, - {id:'xm4', name:'WH-1000XM4', kind:'audio', paired:true, conn:false, batt:58, audio:true}, - {id:'k3', name:'Keychron K3', kind:'keyboard', paired:true, conn:false, batt:34, audio:false}, -]; -const NEARBY = [ - {id:'jbl', name:'JBL Flip 6', kind:'audio', passkey:'847 291'}, - {id:'tv', name:'[TV] Samsung Q70', kind:'display', passkey:'302 118'}, -]; -let barmed = null, barmTimer = null; - -function btConnected(){ return DEVS.filter(d => d.conn); } - -function renderBt(){ - /* paired rows */ - const host = $('b-paired'); host.innerHTML = ''; - for (const d of DEVS){ - const row = document.createElement('div'); - row.className = 'lamp-row' + (barmed===d.id ? ' armed' : ''); - const lamp = !bpower ? 'lamp off' : d.conn ? 'lamp' : 'lamp off'; - const battTxt = d.batt !== null && d.conn ? ` · battery ${d.batt}%` : ''; - const what = barmed===d.id ? 'forget? click ✕ again' - : !bpower ? 'adapter off' - : d.conn ? d.kind + battTxt : d.kind + ' · not connected'; - row.innerHTML = `<span class="${lamp}" id="bl-${d.id}"></span>`+ - `<span class="who">${d.conn && bpower ? '<b>'+d.name+'</b>' : d.name}</span>`+ - `<span class="what" id="bw-${d.id}">${what}</span>`+ - `<button class="pen" title="Rename ${d.name}">✎</button>`+ - `<button class="zap" title="Forget ${d.name}">✕</button>`; - row.querySelector('.pen').onclick = (e) => { e.stopPropagation(); bdlgRename(d.id); }; - row.querySelector('.zap').onclick = (e) => { e.stopPropagation(); btArmForget(d.id); }; - if (bpower) row.onclick = () => btToggleDev(d.id); - host.appendChild(row); - } - /* nearby rows */ - const nb = $('b-nearby'); nb.innerHTML = ''; - for (const n of NEARBY){ - const row = document.createElement('div'); - row.className = 'lamp-row'; - row.innerHTML = `<span class="${bpower?'lamp gold':'lamp off'}" id="bnl-${n.id}"></span>`+ - `<span class="who">${n.name}</span><span class="what" id="bnw-${n.id}">${bpower ? n.kind+' · pairable' : 'adapter off'}</span>`; - if (bpower) row.onclick = () => btPair(n.id); - nb.appendChild(row); - } - /* adapter line + faceplate */ - const c = btConnected().length; - $('b-conn-count').textContent = bpower ? (c === 1 ? '1 device connected' : c + ' devices connected') : 'adapter off'; - $('b-paired-count').textContent = '· ' + DEVS.length; - $('b-nearby-count').textContent = '· ' + NEARBY.length; - $('b-disco').textContent = 'discoverable ' + (bdisco && bpower ? 'on' : 'off'); - $('b-disco').className = 'chip' + (bdisco && bpower ? ' on' : ''); - renderGauges(); - updateBtBadge(); - tips('b-paired'); tips('b-nearby'); - for (const el of document.querySelectorAll('#bp .m-label')) el.title = el.textContent; -} - -function renderGauges(){ - const conns = btConnected(); - for (let i = 0; i < 2; i++){ - const d = bpower ? conns[i] : null; - const needle = $('bn-'+i), val = $('bv-'+i), label = $('bl-'+i), - tag = $('bmt-'+i), wrap = $('bvw-'+i); - if (!d){ - needle.className = 'needle dead'; - needle.style.transform = 'rotate(-60deg)'; - val.textContent = '—'; wrap.className = 'm-value'; - label.textContent = bpower ? 'NO DEVICE' : 'ADAPTER OFF'; - tag.textContent = '—'; tag.className = 'mode-tag off'; - continue; - } - const low = d.batt < 15; - needle.className = 'needle' + (low ? ' low' : ''); - needle.style.transform = `rotate(${-60 + (d.batt/100)*120}deg)`; - val.innerHTML = d.batt; wrap.className = 'm-value' + (low ? ' low' : ''); - wrap.innerHTML = `<span id="bv-${i}">${d.batt}</span> <small>%</small>`; - label.textContent = d.name.toUpperCase(); - tag.textContent = 'LIVE'; tag.className = 'mode-tag'; - } -} - -function updateBtBadge(){ - const low = bpower && btConnected().some(d => d.batt < 15); - $('b-badge').classList.toggle('show', low); -} - -function btToggleDev(id){ - if (bbusy || !bpower) return; - const d = DEVS.find(x=>x.id===id); - const lamp = $('bl-'+id), what = $('bw-'+id); - bbusy = true; - lamp.className = 'lamp busy'; - what.textContent = d.conn ? 'disconnecting…' : 'connecting…'; - setTimeout(() => { - bbusy = false; - d.conn = !d.conn; - renderBt(); - btToast(d.conn ? `${d.name} connected` : `${d.name} disconnected`); - }, T(1300)); -} - -function btArmForget(id){ - const d = DEVS.find(x=>x.id===id); - if (barmed === id){ - clearTimeout(barmTimer); barmed = null; - DEVS.splice(DEVS.indexOf(d), 1); renderBt(); - btToast(`${d.name} forgotten`); - return; - } - barmed = id; renderBt(); - clearTimeout(barmTimer); - barmTimer = setTimeout(() => { barmed = null; renderBt(); }, 3000); -} - -/* pairing + rename dialogs */ -let bdlgMode = null, bdlgTarget = null; -function btPair(id){ - if (bbusy) return; - const n = NEARBY.find(x=>x.id===id); - const lamp = $('bnl-'+id), what = $('bnw-'+id); - bbusy = true; - lamp.className = 'lamp busy'; - what.textContent = 'pairing…'; - setTimeout(() => { - bdlgMode = 'pair'; bdlgTarget = n; - $('bdlg-title').textContent = 'Pair ' + n.name; - $('bdlg-sub').textContent = 'confirm this passkey shows on the device'; - $('bdlg-key').style.display = 'block'; - $('bdlg-key').textContent = n.passkey; - $('bdlg-name').style.display = 'none'; - $('bdlg-go').textContent = 'Confirm'; - $('bov').classList.add('show'); - }, T(1200)); -} -function bdlgRename(id){ - const d = DEVS.find(x=>x.id===id); - bdlgMode = 'rename'; bdlgTarget = d; - $('bdlg-title').textContent = 'Rename device'; - $('bdlg-sub').textContent = 'the alias lives on this machine (bluez set-alias)'; - $('bdlg-key').style.display = 'none'; - $('bdlg-name').style.display = 'block'; - $('bdlg-name').value = d.name; - $('bdlg-go').textContent = 'Rename'; - $('bov').classList.add('show'); - $('bdlg-name').focus(); -} -function bdlgClose(){ - $('bov').classList.remove('show'); - if (bdlgMode === 'pair'){ bbusy = false; renderBt(); btToast('pairing cancelled'); } - bdlgMode = null; bdlgTarget = null; -} -function bdlgGo(){ - if (bdlgMode === 'pair'){ - const n = bdlgTarget; - $('bov').classList.remove('show'); - const what = $('bnw-'+n.id); - if (what) what.textContent = 'connecting…'; - setTimeout(() => { - bbusy = false; - NEARBY.splice(NEARBY.indexOf(n), 1); - DEVS.push({id:n.id, name:n.name, kind:n.kind, paired:true, conn:true, - batt:n.kind==='audio' ? 91 : null, audio:n.kind==='audio'}); - renderBt(); - btToast(`${n.name} paired and connected`); - }, T(1100)); - } else if (bdlgMode === 'rename'){ - const d = bdlgTarget; - const name = $('bdlg-name').value.trim() || d.name; - d.name = name; - $('bov').classList.remove('show'); - renderBt(); - btToast(`renamed to ${name}`); - } - bdlgMode = null; bdlgTarget = null; -} - -/* scan */ -const MORE_NEARBY = [ - {id:'buds', name:'Pixel Buds Pro', kind:'audio', passkey:'551 902'}, -]; -function btScan(){ - if (bbusy || !bpower) return; - $('b-scan-note').textContent = 'scanning…'; - $('bb-scan').disabled = true; - setTimeout(() => { - if (MORE_NEARBY.length){ - NEARBY.push(MORE_NEARBY.shift()); - renderBt(); - } - $('b-scan-note').textContent = ''; - $('bb-scan').disabled = false; - btToast('scan complete — ' + NEARBY.length + ' nearby'); - }, T(2200)); -} - -/* discoverable + power */ -function btDisco(){ - if (!bpower) return; - bdisco = !bdisco; - renderBt(); - btToast(bdisco ? 'discoverable for 2 minutes' : 'discoverable off'); -} -function btPower(){ - if (bbusy) return; - if (airplane){ btToast('airplane mode is on — Super+Shift+A to leave it', true); return; } - btPowerSet(!bpower); -} -function btPowerSet(on, quiet){ - if (bbusy) return; - bpower = on; - $('b-power').classList.toggle('on', bpower); - if (!bpower){ - bdisco = false; - DEVS.forEach(d => d._wasConn = d.conn); - DEVS.forEach(d => d.conn = false); - $('b-state').textContent = airplane ? 'AIRPLANE' : 'OFF'; - $('b-lamp').className = airplane ? 'lamp gold' : 'lamp off'; - $('bb-doctor').disabled = $('bb-scan').disabled = true; - renderBt(); - if (!quiet) btToast('adapter powered off'); - } else { - $('b-state').textContent = 'POWERED'; - $('b-lamp').className = 'lamp'; - $('bb-doctor').disabled = $('bb-scan').disabled = false; - renderBt(); - if (quiet === true) { /* airplane restore: quiet */ } - /* the mouse auto-reconnects, like real life */ - const mouse = DEVS.find(d => d._wasConn); - if (mouse){ - setTimeout(() => { - const lamp = $('bl-'+mouse.id), what = $('bw-'+mouse.id); - if (lamp){ lamp.className = 'lamp busy'; what.textContent = 'reconnecting…'; } - setTimeout(() => { mouse.conn = true; renderBt(); btToast(mouse.name + ' reconnected'); }, T(1200)); - }, T(600)); - } - } -} - -let btoastTimer; -function btToast(msg, err){ - const el = $('b-toastEl'); - el.textContent = msg; el.className = 'toast show' + (err ? ' err' : ''); - clearTimeout(btoastTimer); - btoastTimer = setTimeout(() => el.className = 'toast', err ? 4200 : 2600); -} - -/* bt doctor — the real chain: adapter → radio → service → powered → devices → audio */ -function btChecks(){ - const badAudio = $('badaudio').checked && DEVS.some(d => d.audio && d.conn); - return [ - {t:'Adapter', why:'is a bluetooth adapter visible to the stack', ev:'intel ax211 (hci0)'}, - {t:'Radio', why:'rfkill can block the radio in software or hardware', ev:'unblocked'}, - {t:'Service', why:'is the bluez daemon running', ev:'bluetooth.service active'}, - {t:'Powered', why:'radio on and ready to connect', ev:'powered on'}, - {t:'Devices', why:'are paired devices reachable', - ev: btConnected().length ? btConnected().map(d=>d.name).join(', ') + ' connected' : 'none connected'}, - {t:'Audio profile', why:'is a connected audio device on the high-quality profile (A2DP)', - ev: DEVS.some(d=>d.audio && d.conn) ? 'a2dp-sink' : 'no audio device connected', - evBroken:'stuck on headset-head-unit (HSP) — phone-call-grade audio', canBreak:badAudio}, - ]; -} -const BT_REPAIR = {t:'repair: a2dp-switch', why:'flips the card profile to A2DP and verifies the sink followed', - ev:'card profile set to a2dp-sink — sink followed'}; - -function btDoctor(){ - if (bbusy || !bpower) return; - bbusy = true; - $('bb-doctor').disabled = $('bb-scan').disabled = true; - const checks = btChecks(); - const willRepair = checks.some(c => c.canBreak); - const out = $('b-out'); out.innerHTML = ''; - $('b-state').textContent = 'CHECKING'; $('b-lamp').className = 'lamp gold'; - const gap = T(620); - let i = 0; - const next = () => { - if (i >= checks.length){ return willRepair ? repairPhase() : finish('Overall — everything checks out'); } - const c = checks[i]; - const el = addStep(out, c); - setTimeout(() => { - landStep(el, c.canBreak ? c.evBroken : c.ev, c.canBreak ? 'red' : ''); - i++; setTimeout(next, gap*0.35); - }, gap); - }; - const repairPhase = () => { - $('b-state').textContent = 'FIXING'; - verdict(out, 'audio degraded — trying the lightest repair'); - const r = addStep(out, BT_REPAIR, true); - setTimeout(() => { - landStep(r, BT_REPAIR.ev); - const re = addStep(out, {t:'re-check: Audio profile', why:'read the sink profile again after the switch', ev:''}); - setTimeout(() => { - landStep(re, 'a2dp-sink [verified]'); - $('badaudio').checked = false; - finish('fixed — high-quality audio restored'); - }, gap); - }, gap*1.4); - }; - const finish = (text) => { - verdict(out, text, true); - $('b-state').textContent = 'POWERED'; $('b-lamp').className = 'lamp'; - bbusy = false; - $('bb-doctor').disabled = $('bb-scan').disabled = false; - }; - next(); -} - -function btLowBatt(low){ - blow = low; - const m = DEVS.find(d => d.id === 'm650'); - if (m) m.batt = low ? 9 : 72; - renderBt(); - if (low) btToast('Logi M650 battery low (9%)', true); -} - -function closeBt(){ $('bp').classList.add('closed'); $('b-reopen').style.display='inline-block'; } -function openBt(){ $('bp').classList.remove('closed'); $('b-reopen').style.display='none'; } - -/* shared ambience + keys */ -if (!reduced) setInterval(() => { - amb++; - if (!testing){ - if (!held.rx) meter('rx', 0.1 + Math.abs(Math.sin(amb/3))*0.35); - if (!held.tx) meter('tx', 0.08 + Math.abs(Math.cos(amb/4))*0.25); - } -}, 900); -document.addEventListener('keydown', e => { - if (e.key === 'Escape'){ - if ($('ov').classList.contains('show')) return dlgClose(); - if ($('bov').classList.contains('show')) return bdlgClose(); - closePanel(); closeBt(); - } -}); - -function clearOut(id){ $(id).innerHTML = ''; } -function tips(hostId){ - for (const el of $(hostId).querySelectorAll('.what,.who,.m-label')) - el.title = el.textContent; -} -[['out','outwrap'],['b-out','b-outwrap']].forEach(([oid, wid]) => { - new MutationObserver(() => { - $(wid).classList.toggle('has', $(oid).childElementCount > 0); - }).observe($(oid), {childList: true}); -}); - -renderNets(); renderTunnels(); renderBt(); - -/* headless hooks */ -const auto = new URLSearchParams(location.search).get('auto'); -if (auto === 'btdoctor') btDoctor(); -if (auto === 'btdoctorfix'){ - const xm4 = DEVS.find(d=>d.id==='xm4'); xm4.conn = true; renderBt(); - $('badaudio').checked = true; btDoctor(); -} -if (auto === 'btpair'){ btPair('jbl'); setTimeout(() => bdlgGo(), T(1800)); } -if (auto === 'btpower'){ btPower(); } -if (auto === 'btpoweron'){ btPower(); setTimeout(() => btPower(), T(600)); } -if (auto === 'btlow'){ $('lowbatt').checked = true; btLowBatt(true); } -if (auto === 'speed') runSpeed(); -if (auto === 'ether'){ $('ethercb').checked = true; setEther(true); } -if (auto === 'air'){ $('aircb').checked = true; setAirplane(true); } -if (auto === 'airether'){ $('ethercb').checked = true; setEther(true); $('aircb').checked = true; setAirplane(true); } -if (auto === 'disc'){ armDisconnect('hyatt'); setTimeout(() => armDisconnect('hyatt'), T(600)); } -if (auto === 'wifioff'){ setWifi(false); } -if (auto === 'airport'){ $('airportcb').checked = true; setAirport(true); } -if (auto === 'doctorfix'){ $('breakdns').checked = true; runDoctor(); } -</script> -</body> -</html> |
