@podway/relay 0.2.5 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/FAQ.md +19 -0
- package/README.md +1 -1
- package/dist/main.js +23 -23
- package/dist/status.d.ts +45 -0
- package/dist/status.js +109 -0
- package/package.json +1 -1
package/FAQ.md
CHANGED
|
@@ -115,3 +115,22 @@ rm -rf ~/.npm/_npx
|
|
|
115
115
|
npm i -g @podway/relay@latest
|
|
116
116
|
relay status
|
|
117
117
|
```
|
|
118
|
+
|
|
119
|
+
## How do I see what the relay did without the web dashboard?
|
|
120
|
+
|
|
121
|
+
`relay status` prints it as tables — useful on a NAS where the dashboard's local page is out of reach:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
STATE UPTIME LINK REQ 24h OK% DATA
|
|
125
|
+
running 2d 4h connected 412 98% 61 MB
|
|
126
|
+
|
|
127
|
+
POD REQ OK% LAST
|
|
128
|
+
afisha crawler 301 99% 12 s ago
|
|
129
|
+
crypto sail club 88 93% 3 min ago
|
|
130
|
+
|
|
131
|
+
SITE REQ OK% LAST ERROR
|
|
132
|
+
polymarket.com 145 97% 403 refused, 26 min ago
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`--since 1h|24h|7d` changes the window, `--all` shows every row, `--json` is for scripts. DATA counts
|
|
136
|
+
tunnel traffic only (plain fetches have no size recorded).
|
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ Once started, the relay runs in the background and handles both modes.
|
|
|
46
46
|
| `relay start --code <code> --gateway <url> --accept` | start the relay in the background |
|
|
47
47
|
| `relay login <domain>` | allow signed-in fetches for one domain |
|
|
48
48
|
| `relay dashboard` | open the local command center |
|
|
49
|
-
| `relay status` |
|
|
49
|
+
| `relay status` | the relay at a glance, then requests per pod and per site (OK %, last error); `--since 1h\|24h\|7d`, `--all`, `--json` |
|
|
50
50
|
| `relay stop` | stop it |
|
|
51
51
|
| `relay reset` | remove saved sessions, logins, and pairing |
|
|
52
52
|
|
package/dist/main.js
CHANGED
|
@@ -7,6 +7,7 @@ import { DISCLOSURE } from "./disclosure.js";
|
|
|
7
7
|
import { c, rows } from "./colors.js";
|
|
8
8
|
import { normalizeDomain } from "./domain.js";
|
|
9
9
|
import { stopPid } from "./stop.js";
|
|
10
|
+
import { renderStatus, statusData, WINDOWS } from "./status.js";
|
|
10
11
|
// Only `start` (the daemon) and `dashboard` need the transport, browser and dashboard modules.
|
|
11
12
|
// They load lazily so `relay stop` / `relay status` stay quick on a slow NAS CPU (a DS118 runs the
|
|
12
13
|
// boot task's `relay stop` before every start).
|
|
@@ -367,22 +368,27 @@ async function cmdDashboard(a) {
|
|
|
367
368
|
openBrowser(url);
|
|
368
369
|
await new Promise(() => { }); // stay up until Ctrl-C
|
|
369
370
|
}
|
|
370
|
-
async function cmdStatus() {
|
|
371
|
+
async function cmdStatus(a = []) {
|
|
372
|
+
const flag = (n) => a.includes(n);
|
|
373
|
+
const sinceArg = a[a.indexOf("--since") + 1];
|
|
374
|
+
const window = a.includes("--since") && sinceArg && sinceArg in WINDOWS ? sinceArg : "24h";
|
|
371
375
|
const pid = isRunning();
|
|
372
376
|
const cfg = load();
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
377
|
+
let startedAt;
|
|
378
|
+
try {
|
|
379
|
+
startedAt = JSON.parse(readFileSync(dashboardRuntimeFile(), "utf8")).startedAt;
|
|
380
|
+
}
|
|
381
|
+
catch { }
|
|
382
|
+
// audit.js is fs-only; reload() reads the saved history without pruning or writing.
|
|
383
|
+
const { RelayEventStore } = await import("./audit.js");
|
|
384
|
+
const store = new RelayEventStore({ retentionDays: 30 });
|
|
385
|
+
store.reload();
|
|
386
|
+
const data = statusData(store.query({ limit: 10_000 }), { state: pid ? "running" : "stopped", startedAt, link: pid ? await gatewayLinkState() : undefined }, window);
|
|
387
|
+
if (flag("--json"))
|
|
388
|
+
return log(JSON.stringify({ ...data, pid, gateway: cfg.gatewayUrl ?? null, asYou: cfg.loginDomains }, null, 2));
|
|
389
|
+
log(renderStatus(data, { limit: flag("--all") ? Infinity : 10 }));
|
|
376
390
|
log("");
|
|
377
|
-
log(
|
|
378
|
-
// The PROCESS being alive is not the same as the gateway LINK being up — a slept host
|
|
379
|
-
// leaves a dead link the process can't see. Ask the daemon's own dashboard for the
|
|
380
|
-
// live state so status stops reporting "running" over a dead link.
|
|
381
|
-
if (pid)
|
|
382
|
-
log(`${label("link")}${await gatewayLinkState()}`);
|
|
383
|
-
log(`${label("gateway")}${cfg.gatewayUrl ? c.cyan(cfg.gatewayUrl) : c.dim("(unset)")}`);
|
|
384
|
-
log(`${label("dashboard")}${dashboardUrl() ? c.cyan(dashboardUrl()) : c.dim("relay dashboard opens saved history")}`);
|
|
385
|
-
log(`${label("as-you")}${cfg.loginDomains.length ? cfg.loginDomains.join(", ") : c.dim("no sites signed in — all fetches are clean")}`);
|
|
391
|
+
log(c.dim(`gateway ${cfg.gatewayUrl ?? "(unset)"} · as-you ${cfg.loginDomains.length ? cfg.loginDomains.join(", ") : "none"} · --since 1h|24h|7d · --all · --json`));
|
|
386
392
|
log("");
|
|
387
393
|
}
|
|
388
394
|
/** Live gateway-link state, read from the running daemon's local dashboard (`/data`).
|
|
@@ -392,22 +398,16 @@ async function cmdStatus() {
|
|
|
392
398
|
async function gatewayLinkState() {
|
|
393
399
|
const base = dashboardUrl();
|
|
394
400
|
if (!base)
|
|
395
|
-
return
|
|
401
|
+
return "starting";
|
|
396
402
|
try {
|
|
397
403
|
const ac = new AbortController();
|
|
398
404
|
const timer = setTimeout(() => ac.abort(), 1500);
|
|
399
405
|
const res = await fetch(`${base}/data`, { signal: ac.signal }).finally(() => clearTimeout(timer));
|
|
400
406
|
const data = (await res.json());
|
|
401
|
-
|
|
402
|
-
return c.green("connected");
|
|
403
|
-
if (data.state === "reconnecting")
|
|
404
|
-
return c.yellow("reconnecting… (host asleep or offline)");
|
|
405
|
-
if (data.state === "stopped")
|
|
406
|
-
return c.yellow("stopped");
|
|
407
|
-
return c.dim("unknown");
|
|
407
|
+
return data.state === "connected" || data.state === "reconnecting" || data.state === "stopped" ? data.state : "unknown";
|
|
408
408
|
}
|
|
409
409
|
catch {
|
|
410
|
-
return
|
|
410
|
+
return "unreachable"; // the daemon isn't answering — relay restart
|
|
411
411
|
}
|
|
412
412
|
}
|
|
413
413
|
/** Stop-then-start, reusing the stored gateway + reconnect token. The escape hatch for a
|
|
@@ -437,7 +437,7 @@ async function dispatch(a) {
|
|
|
437
437
|
case "start": return cmdStart(a.slice(1));
|
|
438
438
|
case "login": return cmdLogin(a.slice(1));
|
|
439
439
|
case "dashboard": return cmdDashboard(a.slice(1));
|
|
440
|
-
case "status": return cmdStatus();
|
|
440
|
+
case "status": return cmdStatus(a.slice(1));
|
|
441
441
|
case "stop": return cmdStop();
|
|
442
442
|
case "restart": return cmdRestart(a.slice(1));
|
|
443
443
|
case "reset":
|
package/dist/status.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `relay status` stats as docker-ps style tables (owner, 2026-11-08: the web dashboard is not reachable on
|
|
3
|
+
* a headless NAS, so the terminal has to show what the relay did). Pure: events in, text/JSON out. It reads
|
|
4
|
+
* only the saved event history — no transport, browser or dashboard module — so status stays fast.
|
|
5
|
+
*/
|
|
6
|
+
import type { RelayEventV2 } from "./audit.js";
|
|
7
|
+
export declare const WINDOWS: {
|
|
8
|
+
readonly "1h": 3600000;
|
|
9
|
+
readonly "24h": 86400000;
|
|
10
|
+
readonly "7d": number;
|
|
11
|
+
};
|
|
12
|
+
export type StatusWindow = keyof typeof WINDOWS;
|
|
13
|
+
export interface StatusHead {
|
|
14
|
+
state: "running" | "stopped";
|
|
15
|
+
startedAt?: string;
|
|
16
|
+
link?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface StatusData {
|
|
19
|
+
window: StatusWindow;
|
|
20
|
+
state: StatusHead["state"];
|
|
21
|
+
uptimeMs: number | null;
|
|
22
|
+
link: string | null;
|
|
23
|
+
requests: number;
|
|
24
|
+
okPct: number | null;
|
|
25
|
+
bytes: number | null;
|
|
26
|
+
pods: {
|
|
27
|
+
name: string;
|
|
28
|
+
requests: number;
|
|
29
|
+
okPct: number;
|
|
30
|
+
lastAt: string;
|
|
31
|
+
}[];
|
|
32
|
+
sites: {
|
|
33
|
+
host: string;
|
|
34
|
+
requests: number;
|
|
35
|
+
okPct: number;
|
|
36
|
+
lastError: string | null;
|
|
37
|
+
lastErrorAt: string | null;
|
|
38
|
+
}[];
|
|
39
|
+
}
|
|
40
|
+
export declare function statusData(events: RelayEventV2[], head: StatusHead, window: StatusWindow, now?: number): StatusData;
|
|
41
|
+
export declare function ago(ms: number): string;
|
|
42
|
+
export declare function renderStatus(d: StatusData, opts?: {
|
|
43
|
+
limit: number;
|
|
44
|
+
now?: number;
|
|
45
|
+
}): string;
|
package/dist/status.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { c } from "./colors.js";
|
|
2
|
+
export const WINDOWS = { "1h": 3_600_000, "24h": 86_400_000, "7d": 7 * 86_400_000 };
|
|
3
|
+
const ERROR_WORD = {
|
|
4
|
+
"site-refused": "refused",
|
|
5
|
+
"owner-blocked": "blocked by you",
|
|
6
|
+
"safety-blocked": "blocked (safety)",
|
|
7
|
+
"rate-limited": "rate limited",
|
|
8
|
+
"network-error": "network error",
|
|
9
|
+
};
|
|
10
|
+
function errorText(e) {
|
|
11
|
+
return [e.httpStatus, ERROR_WORD[e.outcome] ?? e.outcome].filter(Boolean).join(" ");
|
|
12
|
+
}
|
|
13
|
+
function podName(e) {
|
|
14
|
+
if (!e.source)
|
|
15
|
+
return "(unknown)";
|
|
16
|
+
if ("system" in e.source)
|
|
17
|
+
return null; // the relay's own health checks are not a pod
|
|
18
|
+
return e.source.podName || e.source.podId;
|
|
19
|
+
}
|
|
20
|
+
function group(events, key) {
|
|
21
|
+
const by = new Map();
|
|
22
|
+
for (const e of events) {
|
|
23
|
+
const k = key(e);
|
|
24
|
+
if (!k)
|
|
25
|
+
continue;
|
|
26
|
+
const t = Date.parse(e.startedAt);
|
|
27
|
+
const r = by.get(k) ?? { name: k, req: 0, ok: 0, last: 0 };
|
|
28
|
+
r.req++;
|
|
29
|
+
if (e.outcome === "ok")
|
|
30
|
+
r.ok++;
|
|
31
|
+
else if (!r.lastError || t > Date.parse(r.lastError.startedAt))
|
|
32
|
+
r.lastError = e;
|
|
33
|
+
r.last = Math.max(r.last, t);
|
|
34
|
+
by.set(k, r);
|
|
35
|
+
}
|
|
36
|
+
return [...by.values()].sort((a, b) => b.req - a.req || b.last - a.last);
|
|
37
|
+
}
|
|
38
|
+
const pct = (ok, n) => (n ? Math.round((ok / n) * 100) : 0);
|
|
39
|
+
export function statusData(events, head, window, now = Date.now()) {
|
|
40
|
+
const inWindow = events.filter((e) => Date.parse(e.startedAt) >= now - WINDOWS[window]);
|
|
41
|
+
const counted = inWindow.filter((e) => podName(e) !== null);
|
|
42
|
+
const ok = counted.filter((e) => e.outcome === "ok").length;
|
|
43
|
+
return {
|
|
44
|
+
window,
|
|
45
|
+
state: head.state,
|
|
46
|
+
uptimeMs: head.state === "running" && head.startedAt ? Math.max(0, now - Date.parse(head.startedAt)) : null,
|
|
47
|
+
link: head.state === "running" ? (head.link ?? "unknown") : null,
|
|
48
|
+
requests: counted.length,
|
|
49
|
+
okPct: counted.length ? pct(ok, counted.length) : null,
|
|
50
|
+
bytes: counted.some((e) => e.mode === "tunnel") ? counted.filter((e) => e.mode === "tunnel").reduce((s, e) => s + (e.bytesUp ?? 0) + (e.bytesDown ?? 0), 0) : null,
|
|
51
|
+
pods: group(counted, podName).map((r) => ({ name: r.name, requests: r.req, okPct: pct(r.ok, r.req), lastAt: new Date(r.last).toISOString() })),
|
|
52
|
+
sites: group(counted, (e) => e.host).map((r) => ({
|
|
53
|
+
host: r.name, requests: r.req, okPct: pct(r.ok, r.req),
|
|
54
|
+
lastError: r.lastError ? errorText(r.lastError) : null,
|
|
55
|
+
lastErrorAt: r.lastError ? r.lastError.startedAt : null,
|
|
56
|
+
})),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export function ago(ms) {
|
|
60
|
+
const s = Math.max(0, Math.round(ms / 1000));
|
|
61
|
+
if (s < 60)
|
|
62
|
+
return `${s} s ago`;
|
|
63
|
+
if (s < 3600)
|
|
64
|
+
return `${Math.round(s / 60)} min ago`;
|
|
65
|
+
if (s < 86_400)
|
|
66
|
+
return `${Math.round(s / 3600)} h ago`;
|
|
67
|
+
return `${Math.round(s / 86_400)} d ago`;
|
|
68
|
+
}
|
|
69
|
+
function duration(ms) {
|
|
70
|
+
const m = Math.floor(ms / 60_000), h = Math.floor(m / 60), d = Math.floor(h / 24);
|
|
71
|
+
return d ? `${d}d ${h % 24}h` : h ? `${h}h ${m % 60}m` : `${m}m`;
|
|
72
|
+
}
|
|
73
|
+
function size(b) {
|
|
74
|
+
if (b < 1024)
|
|
75
|
+
return `${b} B`;
|
|
76
|
+
if (b < 1024 ** 2)
|
|
77
|
+
return `${Math.round(b / 1024)} KB`;
|
|
78
|
+
if (b < 1024 ** 3)
|
|
79
|
+
return `${Math.round(b / 1024 ** 2)} MB`;
|
|
80
|
+
return `${(b / 1024 ** 3).toFixed(1)} GB`;
|
|
81
|
+
}
|
|
82
|
+
const cut = (s, w) => (s.length > w ? s.slice(0, w - 1) + "…" : s);
|
|
83
|
+
/** Columns padded to fixed widths that fit 80 columns; numbers right-aligned. */
|
|
84
|
+
function table(head, widths, right, body) {
|
|
85
|
+
const line = (cells) => cells.map((v, i) => (i === cells.length - 1 ? v : right[i] ? v.padStart(widths[i]) : cut(v, widths[i]).padEnd(widths[i]))).join(" ").trimEnd();
|
|
86
|
+
return [c.dim(line(head)), ...body.map(line)];
|
|
87
|
+
}
|
|
88
|
+
export function renderStatus(d, opts = { limit: 10 }) {
|
|
89
|
+
const now = opts.now ?? Date.now();
|
|
90
|
+
const out = [""];
|
|
91
|
+
const stateWord = d.state === "running" ? c.green("running") : c.yellow("stopped");
|
|
92
|
+
const head = table(["STATE", "UPTIME", "LINK", `REQ ${d.window}`, "OK%", "DATA"], [8, 8, 12, 8, 4, 0], [false, false, false, true, true, false], [[d.state, d.uptimeMs === null ? "—" : duration(d.uptimeMs), d.link ?? "—", String(d.requests), d.okPct === null ? "—" : `${d.okPct}%`, d.bytes === null ? "—" : size(d.bytes)]]);
|
|
93
|
+
head[1] = head[1].replace(/^(running|stopped)/, stateWord);
|
|
94
|
+
out.push(...head);
|
|
95
|
+
if (!d.requests) {
|
|
96
|
+
out.push("", c.dim(`no pod requests in the last ${d.window}`));
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
out.push("");
|
|
100
|
+
out.push(...table(["POD", "REQ", "OK%", "LAST"], [24, 6, 4, 0], [false, true, true, false], d.pods.slice(0, opts.limit).map((p) => [p.name, String(p.requests), `${p.okPct}%`, ago(now - Date.parse(p.lastAt))])));
|
|
101
|
+
out.push("");
|
|
102
|
+
out.push(...table(["SITE", "REQ", "OK%", "LAST ERROR"], [24, 6, 4, 0], [false, true, true, false], d.sites.slice(0, opts.limit).map((s) => [s.host, String(s.requests), `${s.okPct}%`,
|
|
103
|
+
s.lastError ? `${s.lastError}, ${ago(now - Date.parse(s.lastErrorAt))}` : "—"])));
|
|
104
|
+
const hidden = Math.max(0, d.pods.length - opts.limit) + Math.max(0, d.sites.length - opts.limit);
|
|
105
|
+
if (hidden)
|
|
106
|
+
out.push("", c.dim(`${hidden} more rows — relay status --all`));
|
|
107
|
+
}
|
|
108
|
+
return out.join("\n");
|
|
109
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@podway/relay",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "Podway relay \u2014 runs a fetch relay on your machine so a pod can read pages from your network, using sessions you sign into. Install: npm i -g @podway/relay, then `relay start`.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|