|
| 1 | +import { Component } from '@angular/core'; |
| 2 | +import { RouterLink } from '@angular/router'; |
| 3 | + |
| 4 | +type St = 'ok' | 'warn' | 'no' | 'na'; |
| 5 | + |
| 6 | +interface Row { |
| 7 | + feat: string; |
| 8 | + os?: St[]; // [Linux, macOS, Windows] for OS sections |
| 9 | + st?: St; // single status for flat sections |
| 10 | + note?: string; |
| 11 | + sub?: boolean; |
| 12 | +} |
| 13 | +interface Section { |
| 14 | + title: string; |
| 15 | + flat?: boolean; |
| 16 | + rows: Row[]; |
| 17 | +} |
| 18 | +interface Hole { |
| 19 | + sev: 'no' | 'warn'; |
| 20 | + t: string; |
| 21 | + d: string; |
| 22 | +} |
| 23 | + |
| 24 | +@Component({ |
| 25 | + selector: 'app-coverage', |
| 26 | + imports: [RouterLink], |
| 27 | + preserveWhitespaces: true, |
| 28 | + styles: [ |
| 29 | + ` |
| 30 | + .lead { max-width: 66ch; } |
| 31 | + .snap { font-family: 'Courier New', Consolas, monospace; font-size: 0.8rem; color: var(--text-muted); margin: 4px 0 0; } |
| 32 | +
|
| 33 | + .legend { display: flex; flex-wrap: wrap; gap: 16px; margin: 18px 0 6px; font-size: 0.82rem; } |
| 34 | + .legend span { display: inline-flex; align-items: center; gap: 7px; color: var(--text-muted); } |
| 35 | + .dot { width: 15px; height: 15px; border-radius: 50%; display: inline-grid; place-items: center; |
| 36 | + font-size: 10px; font-weight: 700; color: #0d1117; } |
| 37 | + .d-ok { background: var(--cov-ok); } .d-warn { background: var(--cov-warn); } |
| 38 | + .d-no { background: var(--cov-no); } .d-na { background: var(--cov-na); color: var(--text-primary); } |
| 39 | +
|
| 40 | + .holes { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 12px; margin: 12px 0 8px; } |
| 41 | + .hole { background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 10px; |
| 42 | + padding: 14px 16px; position: relative; overflow: hidden; } |
| 43 | + .hole::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: var(--sev); } |
| 44 | + .hole .n { font-family: 'Courier New', Consolas, monospace; font-size: 0.7rem; color: var(--text-muted); } |
| 45 | + .hole .t { font-weight: 600; margin: 3px 0 6px; color: var(--text-primary); font-size: 0.92rem; } |
| 46 | + .hole .d { font-size: 0.83rem; color: var(--text-secondary); line-height: 1.45; } |
| 47 | +
|
| 48 | + section.cov { margin-top: 30px; } |
| 49 | + .s-title { font-size: 0.82rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--accent-purple); |
| 50 | + margin: 0 0 10px; display: flex; align-items: center; gap: 12px; font-weight: 600; } |
| 51 | + .s-title::after { content: ''; flex: 1; height: 1px; background: var(--border-color); } |
| 52 | +
|
| 53 | + .scroll { overflow-x: auto; border: 1px solid var(--border-color); border-radius: 10px; } |
| 54 | + table.cov { border-collapse: collapse; width: 100%; min-width: 640px; margin: 0; font-size: 0.88rem; } |
| 55 | + table.cov th, table.cov td { padding: 9px 13px; border-bottom: 1px solid var(--border-color); text-align: left; vertical-align: middle; } |
| 56 | + table.cov tbody tr:last-child td { border-bottom: none; } |
| 57 | + table.cov thead th { background: var(--bg-secondary); font-size: 0.7rem; letter-spacing: 0.06em; |
| 58 | + text-transform: uppercase; color: var(--text-muted); font-weight: 600; } |
| 59 | + table.cov th.c, table.cov td.c { text-align: center; width: 92px; } |
| 60 | + td.feat { font-family: 'Courier New', Consolas, monospace; font-size: 0.83rem; color: var(--text-primary); white-space: nowrap; } |
| 61 | + td.feat.sub { color: var(--text-secondary); padding-left: 22px; position: relative; } |
| 62 | + td.feat.sub::before { content: '↳'; position: absolute; left: 8px; color: var(--text-muted); } |
| 63 | + td.note { color: var(--text-secondary); font-size: 0.83rem; white-space: normal; } |
| 64 | + table.cov tbody tr:hover td { background: rgba(163, 113, 247, 0.05); } |
| 65 | +
|
| 66 | + .cell { display: inline-grid; place-items: center; width: 26px; height: 26px; border-radius: 7px; |
| 67 | + font-family: 'Courier New', Consolas, monospace; font-weight: 700; font-size: 0.95rem; } |
| 68 | + .st-ok .cell { background: rgba(63, 185, 80, 0.14); color: var(--cov-ok); } |
| 69 | + .st-warn .cell { background: rgba(210, 153, 34, 0.16); color: var(--cov-warn); } |
| 70 | + .st-no .cell { background: rgba(248, 81, 73, 0.15); color: var(--cov-no); } |
| 71 | + .st-na .cell { background: rgba(139, 148, 158, 0.12); color: var(--cov-na); } |
| 72 | +
|
| 73 | + :host { --cov-ok: #3fb950; --cov-warn: var(--accent-yellow); --cov-no: var(--accent-red); --cov-na: var(--text-muted); } |
| 74 | + `, |
| 75 | + ], |
| 76 | + template: ` |
| 77 | + <h2>Coverage matrix</h2> |
| 78 | + <p class="lead"> |
| 79 | + What works where, and where the holes are — features × OS. One process run |
| 80 | + <strong>as if it were inside the cluster</strong>, via a userspace TUN over an SSH tunnel. |
| 81 | + </p> |
| 82 | + <p class="snap">snapshot {{ snapshot }} · Windows datapath validated end-to-end, SYSTEM service coded</p> |
| 83 | +
|
| 84 | + <div class="legend"> |
| 85 | + <span><i class="dot d-ok">✓</i> works (proven at runtime)</span> |
| 86 | + <span><i class="dot d-warn">!</i> coded / partial — not yet e2e-validated</span> |
| 87 | + <span><i class="dot d-no">✕</i> not yet</span> |
| 88 | + <span><i class="dot d-na">–</i> n/a by design</span> |
| 89 | + </div> |
| 90 | +
|
| 91 | + <h3>Biggest holes — priority order</h3> |
| 92 | + <div class="holes"> |
| 93 | + @for (h of holes; track h.t; let i = $index) { |
| 94 | + <div class="hole" [style.--sev]="h.sev === 'no' ? 'var(--cov-no)' : 'var(--cov-warn)'"> |
| 95 | + <div class="n">0{{ i + 1 }}</div> |
| 96 | + <div class="t">{{ h.t }}</div> |
| 97 | + <div class="d">{{ h.d }}</div> |
| 98 | + </div> |
| 99 | + } |
| 100 | + </div> |
| 101 | +
|
| 102 | + @for (sec of sections; track sec.title) { |
| 103 | + <section class="cov"> |
| 104 | + <h3 class="s-title">{{ sec.title }}</h3> |
| 105 | + <div class="scroll"> |
| 106 | + <table class="cov"> |
| 107 | + <thead> |
| 108 | + @if (sec.flat) { |
| 109 | + <tr><th>Item</th><th class="c">Support</th><th>Notes</th></tr> |
| 110 | + } @else { |
| 111 | + <tr> |
| 112 | + <th>Feature</th> |
| 113 | + @for (o of os; track o) { <th class="c">{{ o }}</th> } |
| 114 | + <th>Notes</th> |
| 115 | + </tr> |
| 116 | + } |
| 117 | + </thead> |
| 118 | + <tbody> |
| 119 | + @for (r of sec.rows; track r.feat) { |
| 120 | + <tr> |
| 121 | + <td class="feat" [class.sub]="r.sub" [innerHTML]="r.feat"></td> |
| 122 | + @if (sec.flat) { |
| 123 | + <td class="c st-{{ r.st }}"><span class="cell">{{ glyph(r.st!) }}</span></td> |
| 124 | + } @else { |
| 125 | + @for (s of r.os!; track $index) { |
| 126 | + <td class="c st-{{ s }}"><span class="cell">{{ glyph(s) }}</span></td> |
| 127 | + } |
| 128 | + } |
| 129 | + <td class="note" [innerHTML]="r.note || ''"></td> |
| 130 | + </tr> |
| 131 | + } |
| 132 | + </tbody> |
| 133 | + </table> |
| 134 | + </div> |
| 135 | + </section> |
| 136 | + } |
| 137 | +
|
| 138 | + <div class="callout"> |
| 139 | + <strong>Read this alongside the code.</strong> The Windows datapath (WinTUN + name resolution) |
| 140 | + is proven on a real machine; the SYSTEM service that removes the per-run admin and enables |
| 141 | + multicluster is written and build-validated on all three OSes but not yet runtime-validated — |
| 142 | + see <a href="https://github.com/softwarity/plug/blob/main/docs/windows-service.md" target="_blank" rel="noopener">docs/windows-service.md</a> |
| 143 | + and the <a routerLink="/roadmap">roadmap</a>. |
| 144 | + </div> |
| 145 | + `, |
| 146 | +}) |
| 147 | +export class CoverageComponent { |
| 148 | + protected readonly snapshot = '2026-07-08'; |
| 149 | + protected readonly os = ['Linux', 'macOS', 'Windows']; |
| 150 | + |
| 151 | + protected glyph(s: St): string { |
| 152 | + return { ok: '✓', warn: '!', no: '✕', na: '–' }[s]; |
| 153 | + } |
| 154 | + |
| 155 | + protected readonly holes: Hole[] = [ |
| 156 | + { |
| 157 | + sev: 'warn', |
| 158 | + t: 'Windows SYSTEM service — runtime', |
| 159 | + d: 'The service (no-admin + multicluster) is written and build-validated on all three OSes. Needs a real Windows run: SCM lifecycle, service ACL for non-admin start, session-0 DNS.', |
| 160 | + }, |
| 161 | + { |
| 162 | + sev: 'warn', |
| 163 | + t: 'Windows e2e in CI', |
| 164 | + d: 'Lock down install + service + name resolution (getaddrinfo) with an all-Windows-container e2e on a hosted runner; the full protocol grid needs a self-hosted (WSL2) runner.', |
| 165 | + }, |
| 166 | + { |
| 167 | + sev: 'warn', |
| 168 | + t: 'Windows real-app + VPN', |
| 169 | + d: 'A plain cluster is now proven end-to-end on Windows. Corporate-VPN behaviour and self-heal there are still unproven.', |
| 170 | + }, |
| 171 | + { |
| 172 | + sev: 'warn', |
| 173 | + t: 'self-update on Linux', |
| 174 | + d: 'Loses file-caps on update (pre-existing). macOS already re-applies its setuid bit.', |
| 175 | + }, |
| 176 | + ]; |
| 177 | + |
| 178 | + protected readonly sections: Section[] = [ |
| 179 | + { |
| 180 | + title: 'Install & privilege', |
| 181 | + rows: [ |
| 182 | + { feat: 'Install from the cluster (one-liner)', os: ['ok', 'ok', 'ok'], note: '<code>install | sh</code> · <code>install-windows | powershell</code> (Git Bash: <code>PLUG_HOST=</code>)' }, |
| 183 | + { feat: 'No sudo/admin to install', os: ['ok', 'ok', 'ok'], note: 'binaries + PATH; one UAC only to create the service' }, |
| 184 | + { feat: 'One-time privilege grant at install', os: ['ok', 'ok', 'warn'], note: 'setcap · setuid helper · SCM service (coded)' }, |
| 185 | + { feat: '<b>plug <cmd></b> without sudo/admin', os: ['ok', 'ok', 'warn'], note: 'Windows: via the service (coded) → no admin; else falls back to an elevated terminal' }, |
| 186 | + { feat: 'Child runs as you (privilege drop)', os: ['ok', 'ok', 'na'], note: "caps don't cross exec · Credential drop · service is SYSTEM" }, |
| 187 | + { feat: 'self-update preserves privilege', os: ['warn', 'ok', 'na'], note: 'setcap lost on update · macOS re-applies setuid' }, |
| 188 | + { feat: 'Pre-create profile at install', os: ['ok', 'ok', 'ok'], note: 'reads host/port off the live ssh' }, |
| 189 | + { feat: 'Uninstall', os: ['ok', 'ok', 'warn'], note: 'unix covered; Windows path unverified' }, |
| 190 | + ], |
| 191 | + }, |
| 192 | + { |
| 193 | + title: 'Data path', |
| 194 | + rows: [ |
| 195 | + { feat: 'Userspace TUN (IP-layer capture)', os: ['ok', 'ok', 'ok'], note: '/dev/net/tun · utun · WinTUN' }, |
| 196 | + { feat: 'Cluster-name DNS (real apps)', os: ['ok', 'ok', 'ok'], note: 'private resolv.conf · scutil store · WinTUN search-suffix + NRPT' }, |
| 197 | + { feat: 'Single-label name via <code>getaddrinfo</code>', os: ['ok', 'ok', 'ok'], note: 'the real app path; Windows needs the <code>.plug</code> search suffix to issue a DNS query' }, |
| 198 | + { feat: 'Works under a corporate VPN', os: ['ok', 'ok', 'warn'], note: 'macOS proven w/ GlobalProtect; Windows unproven' }, |
| 199 | + { feat: 'Every runtime (Node/JVM/Py/Go/gRPC)', os: ['ok', 'ok', 'ok'], note: 'IP-level capture, socket never touched' }, |
| 200 | + { feat: 'Native selftest (datapath proof)', os: ['ok', 'ok', 'ok'], note: 'green on all three in CI' }, |
| 201 | + { feat: 'e2e protocol matrix (8 protos × 4 langs)', os: ['ok', 'na', 'na'], note: 'Docker→Linux; mac/win via native selftest (Windows e2e planned)' }, |
| 202 | + { feat: 'Split-horizon (short→cluster, FQDN→direct)', os: ['ok', 'ok', 'ok'], note: '+ <code>PLUG_DIRECT</code> overrides' }, |
| 203 | + { feat: 'Self-heal (VPN / sleep / agent restart)', os: ['ok', 'ok', 'warn'], note: 'keepalive+reconnect; Windows unproven' }, |
| 204 | + ], |
| 205 | + }, |
| 206 | + { |
| 207 | + title: 'Daemon / persistence', |
| 208 | + rows: [ |
| 209 | + { feat: 'Persistent global datapath', os: ['na', 'ok', 'warn'], note: 'Linux autonomous · macOS daemon · Windows SCM service (coded)' }, |
| 210 | + { feat: 'Survives process restarts', os: ['ok', 'ok', 'warn'], note: 'per-launch · daemon · service (coded)' }, |
| 211 | + { feat: 'Graft (multi-process, same cluster)', os: ['ok', 'ok', 'warn'], note: 'flock leader/graft · service is the single owner' }, |
| 212 | + { feat: '<b>plug down</b>', os: ['na', 'ok', 'warn'], note: 'stops the daemon (macOS) / service (Windows, coded)' }, |
| 213 | + ], |
| 214 | + }, |
| 215 | + { |
| 216 | + title: 'Multicluster — different clusters at once', |
| 217 | + rows: [ |
| 218 | + { feat: 'Simultaneous different clusters', os: ['ok', 'ok', 'warn'], note: 'Linux mount-ns · <b>macOS validated e2e</b> · Windows service (coded)' }, |
| 219 | + { feat: 'PID-at-connect attribution', os: ['na', 'ok', 'warn'], note: '<code>multiDial</code> shared; macOS proven on 2 live clusters · Windows service coded' }, |
| 220 | + { feat: 'ppidOf', os: ['ok', 'ok', 'warn'], note: '/proc · ps · ToolHelp (coded, unit-tested)', sub: true }, |
| 221 | + { feat: 'pidForLocalPort', os: ['ok', 'ok', 'warn'], note: '/proc/net · lsof · GetExtendedTcpTable (coded, unit-tested)', sub: true }, |
| 222 | + { feat: 'procStart (recycle guard)', os: ['ok', 'ok', 'warn'], note: 'ps lstart · /proc stat · GetProcessTimes — rejects a reused PID', sub: true }, |
| 223 | + { feat: 'N-tunnel global daemon', os: ['na', 'ok', 'warn'], note: 'macOS done + validated · Windows = SCM service (coded)' }, |
| 224 | + ], |
| 225 | + }, |
| 226 | + { |
| 227 | + title: 'Profiles & CLI', |
| 228 | + rows: [ |
| 229 | + { feat: 'Profiles ~/.plug/*.conf', os: ['ok', 'ok', 'ok'], note: 'create by naming · ls · rm · rn/mv · test' }, |
| 230 | + { feat: '-p profile / --host / --port / env', os: ['ok', 'ok', 'ok'], note: 'env = <code>$PLUG_HOST $PLUG_PORT</code>' }, |
| 231 | + { feat: 'Launcher versions (per-cluster)', os: ['ok', 'ok', 'ok'], note: 'versions · self-update · <code>.exe</code> + wintun.dll handled on Windows' }, |
| 232 | + { feat: 'Host-key TOFU pin', os: ['ok', 'ok', 'ok'], note: 'localhost skipped; pin chowned (macOS)' }, |
| 233 | + { feat: 'Port-forward escape hatch (forward=)', os: ['ok', 'ok', 'ok'], note: 'rewrites an env var to a local port' }, |
| 234 | + ], |
| 235 | + }, |
| 236 | + { |
| 237 | + title: 'Agent deployment', |
| 238 | + flat: true, |
| 239 | + rows: [ |
| 240 | + { feat: 'Docker Compose / Swarm', st: 'ok', note: 'agent image joins the stack network' }, |
| 241 | + { feat: 'Kubernetes — NodePort', st: 'ok', note: '<code>deploy/plug-k8s.yaml</code>, --port 32222' }, |
| 242 | + { feat: 'Kubernetes — kubectl port-forward', st: 'ok', note: 'zero exposed port, API-server RBAC' }, |
| 243 | + { feat: 'Cross-namespace', st: 'ok', note: 'via FQDN <code>svc.othernamespace</code>' }, |
| 244 | + { feat: 'kubectl exec transport', st: 'no', note: 'dropped — port-forward already covers it' }, |
| 245 | + ], |
| 246 | + }, |
| 247 | + { |
| 248 | + title: 'Design limits (all OS)', |
| 249 | + flat: true, |
| 250 | + rows: [ |
| 251 | + { feat: 'UDP / QUIC / ping', st: 'no', note: 'TCP only (SSH tunnel); most clients fall back to TCP' }, |
| 252 | + { feat: 'IPv6 literal (hard-coded)', st: 'no', note: 'fake IPs are IPv4; by-name cluster service is fine' }, |
| 253 | + { feat: 'Root / helper required', st: 'na', note: 'by design — the price of covering every runtime' }, |
| 254 | + { feat: 'Authentication', st: 'na', note: 'none by design — trusted dev clusters only' }, |
| 255 | + ], |
| 256 | + }, |
| 257 | + ]; |
| 258 | +} |
0 commit comments