Skip to content

Commit d3f2c2e

Browse files
hhfrancoisclaude
andcommitted
docs(site): coverage matrix as a themed Angular page (not a raw markdown link)
Ports the coverage-matrix artifact into the doc site as a first-class page at the site theme (violet / GitHub-dark, CSS vars, Material icons) instead of linking to docs/coverage.md raw. Data refreshed to the current state: Windows datapath + single-label DNS (getaddrinfo via .plug/NRPT) validated end-to-end; SYSTEM service coded (no-admin + multicluster groundwork); PID recycle-guard. Wired into the sidebar + header nav with a /coverage route; build + runtime verified in the dev server. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 323121a commit d3f2c2e

4 files changed

Lines changed: 264 additions & 1 deletion

File tree

docs/src/app/app.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ <h1>plug</h1>
1616
<mat-icon>menu_book</mat-icon>
1717
README
1818
</a>
19-
<a href="https://github.com/softwarity/plug/blob/main/docs/coverage.md" target="_blank" rel="noopener" class="nav-button">
19+
<a [routerLink]="'/coverage'" class="nav-button">
2020
<mat-icon>table_chart</mat-icon>
2121
Coverage
2222
</a>

docs/src/app/app.component.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export class AppComponent {
2727
{ path: '/profiles', label: 'Profiles & versions', icon: 'settings' },
2828
{ path: '/agent', label: 'Agent & deployment', icon: 'dns' },
2929
{ path: '/security', label: 'Security model', icon: 'shield' },
30+
{ path: '/coverage', label: 'Coverage matrix', icon: 'table_chart' },
3031
{ path: '/roadmap', label: 'Roadmap', icon: 'map' },
3132
];
3233
}

docs/src/app/app.routes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,9 @@ export const routes: Routes = [
2626
path: 'roadmap',
2727
loadComponent: () => import('./pages/roadmap.component').then((m) => m.RoadmapComponent),
2828
},
29+
{
30+
path: 'coverage',
31+
loadComponent: () => import('./pages/coverage.component').then((m) => m.CoverageComponent),
32+
},
2933
{ path: '**', redirectTo: '' },
3034
];
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
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 &lt;cmd&gt;</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

Comments
 (0)