Skip to content

Commit 108c43e

Browse files
committed
refactor: tighten netcheck types, share webview CSS, add SDK drift guard
- Split shared netcheck into types.ts / api.ts (Api only) / utils.ts, trim the exported surface, and document provenance. - Add a compile-time drift guard: the parser fails to build if the coder SDK renames or removes a field it reads. - Extract shared base styles into @repo/webview-shared/base.css, consumed by both vanilla webviews; each index.css keeps only page-specific rules. - Simplify connectivity (keyed cases, no nested ternary) and regions (map -> toSorted pipeline); render missing IPv4 as a warning, not an error. - Move the large parser payloads into golden fixtures/ JSON files.
1 parent c7240ae commit 108c43e

19 files changed

Lines changed: 406 additions & 387 deletions

File tree

packages/netcheck/src/connectivity.ts

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -23,23 +23,27 @@ export function buildConnectivityItems(
2323
}
2424

2525
// Tones: bad = real problem, warn = works but suboptimal, neutral = optional.
26-
// So a missing optional capability stays neutral, but blocked UDP is bad.
2726
const items: ConnectivityItem[] = [
28-
boolItem("UDP", probe.UDP, ["Reachable", "good"], ["Blocked", "bad"]),
29-
boolItem("IPv4", probe.IPv4, ["Yes", "good"], ["No", "bad"]),
30-
boolItem("IPv6", probe.IPv6, ["Yes", "good"], ["No", "neutral"]),
31-
boolItem(
32-
"NAT mapping",
33-
probe.MappingVariesByDestIP,
34-
["Varies by destination (hard NAT)", "warn"],
35-
["Consistent (easy NAT)", "good"],
36-
),
37-
boolItem(
38-
"Hairpinning",
39-
probe.HairPinning,
40-
["Supported", "good"],
41-
["Not supported", "neutral"],
42-
),
27+
boolItem("UDP", probe.UDP, {
28+
true: ["Reachable", "good"],
29+
false: ["Blocked", "bad"],
30+
}),
31+
boolItem("IPv4", probe.IPv4, {
32+
true: ["Yes", "good"],
33+
false: ["No", "warn"],
34+
}),
35+
boolItem("IPv6", probe.IPv6, {
36+
true: ["Yes", "good"],
37+
false: ["No", "neutral"],
38+
}),
39+
boolItem("NAT mapping", probe.MappingVariesByDestIP, {
40+
true: ["Varies by destination (hard NAT)", "warn"],
41+
false: ["Consistent (easy NAT)", "good"],
42+
}),
43+
boolItem("Hairpinning", probe.HairPinning, {
44+
true: ["Supported", "good"],
45+
false: ["Not supported", "neutral"],
46+
}),
4347
portMappingItem(probe),
4448
];
4549

@@ -50,19 +54,16 @@ export function buildConnectivityItems(
5054
return items;
5155
}
5256

53-
/** Picks the true/false outcome; null/undefined render as a neutral "Unknown". */
57+
/** Renders a boolean probe field; a missing value is a neutral "Unknown". */
5458
function boolItem(
5559
label: string,
5660
state: boolean | null | undefined,
57-
ifTrue: Outcome,
58-
ifFalse: Outcome,
61+
cases: { true: Outcome; false: Outcome },
5962
): ConnectivityItem {
60-
const [value, tone]: Outcome =
61-
state === true
62-
? ifTrue
63-
: state === false
64-
? ifFalse
65-
: ["Unknown", "neutral"];
63+
if (typeof state !== "boolean") {
64+
return { label, value: "Unknown", tone: "neutral" };
65+
}
66+
const [value, tone] = state ? cases.true : cases.false;
6667
return { label, value, tone };
6768
}
6869

packages/netcheck/src/health.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,31 +10,31 @@ export interface Issue {
1010
message: string;
1111
}
1212

13-
const SEVERITY_LABEL: Readonly<Record<NetcheckSeverity, string>> = {
13+
const SEVERITY_LABEL = {
1414
ok: "Healthy",
1515
warning: "Warning",
1616
error: "Error",
17-
};
17+
} as const satisfies Record<NetcheckSeverity, string>;
1818

19-
export function severityLabel(severity: NetcheckSeverity): string {
20-
return SEVERITY_LABEL[severity];
21-
}
22-
23-
const BANNER_TITLE: Readonly<Record<NetcheckSeverity, string>> = {
19+
const BANNER_TITLE = {
2420
ok: "Network is healthy",
2521
warning: "Network has warnings",
2622
error: "Network problems detected",
27-
};
28-
29-
export function bannerTitle(severity: NetcheckSeverity): string {
30-
return BANNER_TITLE[severity];
31-
}
23+
} as const satisfies Record<NetcheckSeverity, string>;
3224

33-
const SECTION_STATUS: Readonly<Record<NetcheckSeverity, string>> = {
25+
const SECTION_STATUS = {
3426
ok: "healthy",
3527
warning: "warning",
3628
error: "error",
37-
};
29+
} as const satisfies Record<NetcheckSeverity, string>;
30+
31+
export function severityLabel(severity: NetcheckSeverity): string {
32+
return SEVERITY_LABEL[severity];
33+
}
34+
35+
export function bannerTitle(severity: NetcheckSeverity): string {
36+
return BANNER_TITLE[severity];
37+
}
3838

3939
/** One-line status for a report section, e.g. "2 warnings" or "healthy". */
4040
export function sectionSummary(section: NetcheckSectionHealth): string {

packages/netcheck/src/index.css

Lines changed: 1 addition & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
*,
2-
*::before,
3-
*::after {
4-
box-sizing: border-box;
5-
}
1+
/* Base reset, body, and button styles come from @repo/webview-shared/base.css. */
62

73
/* Local aliases composed from VS Code theme tokens so every theme (light,
84
* dark, high contrast) renders consistently. */
@@ -15,15 +11,6 @@
1511
--card-bg: var(--vscode-editorWidget-background, transparent);
1612
}
1713

18-
body {
19-
margin: 0;
20-
padding: 1.5em;
21-
background: var(--vscode-editor-background);
22-
color: var(--vscode-foreground, var(--vscode-editor-foreground));
23-
font-family: var(--vscode-font-family);
24-
font-size: var(--vscode-font-size);
25-
}
26-
2714
#root {
2815
max-width: 56em;
2916
margin: 0 auto;
@@ -286,39 +273,14 @@ body {
286273
}
287274

288275
.actions {
289-
display: flex;
290-
justify-content: center;
291276
margin-top: 1.5em;
292277
}
293278

294-
button {
295-
padding: 0.4em 1em;
296-
border: 1px solid var(--vscode-button-border, transparent);
297-
border-radius: 2px;
298-
background: var(--vscode-button-secondaryBackground);
299-
color: var(--vscode-button-secondaryForeground);
300-
font: inherit;
301-
cursor: pointer;
302-
}
303-
304-
button:hover {
305-
background: var(--vscode-button-secondaryHoverBackground);
306-
}
307-
308-
button:focus-visible {
309-
outline: 1px solid var(--vscode-focusBorder);
310-
outline-offset: 2px;
311-
}
312-
313279
.error,
314280
.empty {
315281
margin: 0.5em 0;
316282
}
317283

318-
.error {
319-
color: var(--vscode-errorForeground);
320-
}
321-
322284
.empty {
323285
color: var(--muted);
324286
}
@@ -339,8 +301,4 @@ button:focus-visible {
339301
.section-body {
340302
padding: 0.75em;
341303
}
342-
343-
.section-body-flush {
344-
padding: 0;
345-
}
346304
}

packages/netcheck/src/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { NetcheckApi, toError, type NetcheckData } from "@repo/shared";
22
import { sendCommand, subscribeNotifications } from "@repo/webview-shared";
3+
import "@repo/webview-shared/base.css";
34

45
import "./index.css";
56
import { renderError, renderPage } from "./page";
67

78
function main(): void {
8-
// The extension re-sends `data` on visibility/theme changes, so each render
9-
// replaces the whole root, clearing any prior error or report.
9+
// The extension re-sends `data` on visibility/theme changes; each render
10+
// replaces the root, clearing any prior error.
1011
subscribeNotifications(NetcheckApi, {
1112
data: (data) => render(data),
1213
});

packages/netcheck/src/regions.ts

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -26,25 +26,35 @@ export function regionName(
2626

2727
/** Rows for the regions table: preferred first, then by latency, then name. */
2828
export function buildRegionRows(report: NetcheckReport): RegionRow[] {
29-
const latencies = report.derp.netcheck?.RegionLatency ?? {};
30-
const preferredId = report.derp.netcheck?.PreferredDERP;
31-
const rows = Object.entries(report.derp.regions).map(([key, region]) => {
32-
const id = Number(key);
33-
const nodes = region.node_reports;
34-
const stunNodes = nodes.filter((n) => n.stun.Enabled);
35-
const relayNodes = nodes.filter((n) => !(n.node?.STUNOnly ?? false));
36-
return {
37-
name: regionName(region, id),
38-
severity: region.severity,
39-
latencyMs: regionLatencyMs(latencies[key], relayNodes),
40-
preferred: id === preferredId,
41-
embeddedRelay: region.region?.EmbeddedRelay ?? false,
42-
stun: anyTriState(stunNodes, (n) => n.stun.CanSTUN),
43-
relay: anyTriState(relayNodes, (n) => n.can_exchange_messages),
44-
error: region.error ?? undefined,
45-
};
46-
});
47-
return rows.sort(compareRegionRows);
29+
const probe = report.derp.netcheck;
30+
return Object.entries(report.derp.regions)
31+
.map(([key, region]) =>
32+
toRegionRow(region, Number(key), probe?.PreferredDERP, probe?.RegionLatency[key]),
33+
)
34+
.toSorted(compareRegionRows);
35+
}
36+
37+
function toRegionRow(
38+
region: NetcheckRegionReport,
39+
id: number,
40+
preferredId: number | undefined,
41+
latencyNanos: number | undefined,
42+
): RegionRow {
43+
// STUN and relay capability come from different node sets.
44+
const relayNodes = region.node_reports.filter(
45+
(n) => !(n.node?.STUNOnly ?? false),
46+
);
47+
const stunNodes = region.node_reports.filter((n) => n.stun.Enabled);
48+
return {
49+
name: regionName(region, id),
50+
severity: region.severity,
51+
latencyMs: regionLatencyMs(latencyNanos, relayNodes),
52+
preferred: id === preferredId,
53+
embeddedRelay: region.region?.EmbeddedRelay ?? false,
54+
stun: anyTriState(stunNodes, (n) => n.stun.CanSTUN),
55+
relay: anyTriState(relayNodes, (n) => n.can_exchange_messages),
56+
error: region.error ?? undefined,
57+
};
4858
}
4959

5060
function regionLatencyMs(

packages/shared/src/index.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,17 @@ export {
1818
} from "./speedtest/api";
1919

2020
// Netcheck API
21-
export {
22-
NetcheckApi,
23-
overallNetcheckSeverity,
24-
type NetcheckConnectivity,
25-
type NetcheckData,
26-
type NetcheckHealthMessage,
27-
type NetcheckInterface,
28-
type NetcheckNodeReport,
29-
type NetcheckRegionReport,
30-
type NetcheckReport,
31-
type NetcheckSectionHealth,
32-
type NetcheckSeverity,
33-
} from "./netcheck/api";
21+
export { NetcheckApi } from "./netcheck/api";
22+
export { overallNetcheckSeverity } from "./netcheck/utils";
23+
export type {
24+
NetcheckConnectivity,
25+
NetcheckData,
26+
NetcheckInterface,
27+
NetcheckRegionReport,
28+
NetcheckReport,
29+
NetcheckSectionHealth,
30+
NetcheckSeverity,
31+
} from "./netcheck/types";
3432

3533
// Workspaces API
3634
export { WorkspacesApi } from "./workspaces/api";

packages/shared/src/netcheck/api.ts

Lines changed: 1 addition & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,95 +1,6 @@
11
import { defineCommand, defineNotification } from "../ipc/protocol";
22

3-
export type NetcheckSeverity = "ok" | "warning" | "error";
4-
5-
export interface NetcheckHealthMessage {
6-
code: string;
7-
message: string;
8-
}
9-
10-
/** Health fields shared by the DERP and interfaces sections of the report. */
11-
export interface NetcheckSectionHealth {
12-
severity: NetcheckSeverity;
13-
warnings: NetcheckHealthMessage[];
14-
error?: string | null;
15-
}
16-
17-
export interface NetcheckNodeReport {
18-
can_exchange_messages: boolean;
19-
round_trip_ping_ms: number;
20-
/** Field names match the CLI's Go JSON output, which has no tags here. */
21-
stun: { Enabled: boolean; CanSTUN: boolean };
22-
/** Field names match tailscale's DERP map JSON. */
23-
node?: { STUNOnly?: boolean | null } | null;
24-
}
25-
26-
export interface NetcheckRegionReport {
27-
severity: NetcheckSeverity;
28-
error?: string | null;
29-
/** Field names match tailscale's DERP map JSON. */
30-
region?: {
31-
RegionID: number;
32-
RegionName: string;
33-
EmbeddedRelay: boolean;
34-
} | null;
35-
node_reports: NetcheckNodeReport[];
36-
}
37-
38-
/** Subset of tailscale's netcheck report; field names match its JSON output. */
39-
export interface NetcheckConnectivity {
40-
UDP: boolean;
41-
IPv4: boolean;
42-
IPv6: boolean;
43-
MappingVariesByDestIP?: boolean | null;
44-
HairPinning?: boolean | null;
45-
UPnP?: boolean | null;
46-
PMP?: boolean | null;
47-
PCP?: boolean | null;
48-
/** Region ID of the preferred DERP region; 0 when undetermined. */
49-
PreferredDERP: number;
50-
/** Latency per DERP region ID, in nanoseconds. */
51-
RegionLatency: Record<string, number>;
52-
}
53-
54-
export interface NetcheckInterface {
55-
name: string;
56-
mtu: number;
57-
addresses: string[];
58-
}
59-
60-
/** Subset of the CLI's ClientNetcheckReport that the extension renders. */
61-
export interface NetcheckReport {
62-
derp: NetcheckSectionHealth & {
63-
regions: Record<string, NetcheckRegionReport>;
64-
netcheck?: NetcheckConnectivity | null;
65-
netcheck_err?: string | null;
66-
};
67-
interfaces: NetcheckSectionHealth & {
68-
interfaces: NetcheckInterface[];
69-
};
70-
}
71-
72-
export interface NetcheckData {
73-
/** Hostname of the deployment the report was generated against. */
74-
host: string;
75-
report: NetcheckReport;
76-
}
77-
78-
const SEVERITY_RANK: Record<NetcheckSeverity, number> = {
79-
ok: 0,
80-
warning: 1,
81-
error: 2,
82-
};
83-
84-
/** Worst severity across the DERP and interfaces sections. */
85-
export function overallNetcheckSeverity(
86-
report: NetcheckReport,
87-
): NetcheckSeverity {
88-
const { derp, interfaces } = report;
89-
return SEVERITY_RANK[derp.severity] >= SEVERITY_RANK[interfaces.severity]
90-
? derp.severity
91-
: interfaces.severity;
92-
}
3+
import type { NetcheckData } from "./types";
934

945
export const NetcheckApi = {
956
/** Extension pushes the parsed report to the webview */

0 commit comments

Comments
 (0)