@feynmanzhang/open-party 0.1.6 → 0.1.8
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/README.md +138 -0
- package/dist/claude-code/{open-party-0.1.6 → open-party-0.1.8}/.claude-plugin/plugin.json +1 -1
- package/dist/claude-code/open-party-0.1.8/BUILD_INFO.json +6 -0
- package/dist/claude-code/open-party-0.1.8/dist/dispatcher.js +187 -0
- package/dist/claude-code/{open-party-0.1.6 → open-party-0.1.8}/dist/hook-handler.js +58 -73
- package/dist/claude-code/{open-party-0.1.6 → open-party-0.1.8}/dist/mcp-server.js +552 -364
- package/dist/claude-code/{open-party-0.1.6 → open-party-0.1.8}/dist/party-server.js +426 -1657
- package/dist/claude-code/{open-party-0.1.6 → open-party-0.1.8}/hooks/hooks.json +39 -50
- package/dist/claude-code/{open-party-0.1.6 → open-party-0.1.8}/package.json +1 -1
- package/dist/claude-code/{open-party-0.1.6 → open-party-0.1.8}/skills/open-party/SKILL.md +39 -21
- package/dist/cli/index.js +1534 -2647
- package/dist/cli/index.js.map +1 -1
- package/dist/party-server.js +426 -1657
- package/dist/party-server.js.map +1 -1
- package/package.json +10 -13
- package/dist/claude-code/open-party-0.1.6/BUILD_INFO.json +0 -6
- package/dist/openclaw/open-party-0.1.5/BUILD_INFO.json +0 -6
- package/dist/openclaw/open-party-0.1.5/SKILL.md +0 -127
- package/dist/openclaw/open-party-0.1.5/dist/index.js +0 -550
- package/dist/openclaw/open-party-0.1.5/dist/party-server.js +0 -5502
- package/dist/openclaw/open-party-0.1.5/openclaw.plugin.json +0 -28
- package/dist/openclaw/open-party-0.1.5/package.json +0 -12
- package/dist/openclaw/open-party-0.1.5/skills/open-party/SKILL.md +0 -90
- package/dist/openclaw/open-party-0.1.6/BUILD_INFO.json +0 -6
- package/dist/openclaw/open-party-0.1.6/SKILL.md +0 -127
- package/dist/openclaw/open-party-0.1.6/dist/index.js +0 -550
- package/dist/openclaw/open-party-0.1.6/dist/party-server.js +0 -5502
- package/dist/openclaw/open-party-0.1.6/openclaw.plugin.json +0 -28
- package/dist/openclaw/open-party-0.1.6/package.json +0 -12
- package/dist/openclaw/open-party-0.1.6/skills/open-party/SKILL.md +0 -90
- /package/dist/claude-code/{open-party-0.1.6 → open-party-0.1.8}/.mcp.json +0 -0
|
@@ -11,11 +11,12 @@ var __export = (target, all) => {
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
// src/server/config.ts
|
|
14
|
-
var PARTY_PORT, HEARTBEAT_TIMEOUT, CLEANUP_INTERVAL, DISCOVERY_INTERVAL, REMOTE_STALE_FACTOR, PROBE_TIMEOUT;
|
|
14
|
+
var PARTY_PORT, STALE_THRESHOLD, HEARTBEAT_TIMEOUT, CLEANUP_INTERVAL, DISCOVERY_INTERVAL, REMOTE_STALE_FACTOR, PROBE_TIMEOUT;
|
|
15
15
|
var init_config = __esm({
|
|
16
16
|
"src/server/config.ts"() {
|
|
17
17
|
"use strict";
|
|
18
18
|
PARTY_PORT = parseInt(process.env.PARTY_PORT || "8000", 10);
|
|
19
|
+
STALE_THRESHOLD = parseInt(process.env.STALE_THRESHOLD || "3", 10);
|
|
19
20
|
HEARTBEAT_TIMEOUT = parseFloat(process.env.HEARTBEAT_TIMEOUT || "60");
|
|
20
21
|
CLEANUP_INTERVAL = parseFloat(process.env.CLEANUP_INTERVAL || "60");
|
|
21
22
|
DISCOVERY_INTERVAL = parseFloat(process.env.DISCOVERY_INTERVAL || "20");
|
|
@@ -57,10 +58,10 @@ function initLogFile() {
|
|
|
57
58
|
}
|
|
58
59
|
function getLogFilePath() {
|
|
59
60
|
const d = /* @__PURE__ */ new Date();
|
|
60
|
-
const
|
|
61
|
+
const yyyy = String(d.getFullYear());
|
|
61
62
|
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
62
63
|
const dd = String(d.getDate()).padStart(2, "0");
|
|
63
|
-
return join(LOG_DIR, `${
|
|
64
|
+
return join(LOG_DIR, `${yyyy}-${mm}-${dd}-open-party.log`);
|
|
64
65
|
}
|
|
65
66
|
function shouldLog(level) {
|
|
66
67
|
return LEVEL_ORDER[level] >= LEVEL_ORDER[effectiveLevel];
|
|
@@ -76,13 +77,13 @@ ${err.stack}` : "";
|
|
|
76
77
|
function format(level, tag, message) {
|
|
77
78
|
const now = /* @__PURE__ */ new Date();
|
|
78
79
|
const levelStr = level.toUpperCase().padEnd(5);
|
|
79
|
-
const
|
|
80
|
+
const yyyy = String(now.getFullYear());
|
|
80
81
|
const mm = String(now.getMonth() + 1).padStart(2, "0");
|
|
81
82
|
const dd = String(now.getDate()).padStart(2, "0");
|
|
82
83
|
const hh = String(now.getHours()).padStart(2, "0");
|
|
83
84
|
const min = String(now.getMinutes()).padStart(2, "0");
|
|
84
85
|
const ss = String(now.getSeconds()).padStart(2, "0");
|
|
85
|
-
const ts = `${
|
|
86
|
+
const ts = `${yyyy}-${mm}-${dd} ${hh}:${min}:${ss}`;
|
|
86
87
|
return `${ts} [${levelStr}] [${tag}] ${message}`;
|
|
87
88
|
}
|
|
88
89
|
function output(consoleFn, level, tag, message) {
|
|
@@ -113,7 +114,8 @@ var init_logger = __esm({
|
|
|
113
114
|
output(console.log, "info", tag, data ? `${message} ${JSON.stringify(data)}` : message);
|
|
114
115
|
},
|
|
115
116
|
warn(tag, message, data) {
|
|
116
|
-
|
|
117
|
+
const detail = data instanceof Error ? `: ${extractError(data)}` : data ? ` ${JSON.stringify(data)}` : "";
|
|
118
|
+
output(console.warn, "warn", tag, message + detail);
|
|
117
119
|
},
|
|
118
120
|
error(tag, message, err) {
|
|
119
121
|
const detail = err ? `: ${extractError(err)}` : "";
|
|
@@ -135,26 +137,6 @@ function dataDirPath() {
|
|
|
135
137
|
if (pluginData) return join2(pluginData, "data");
|
|
136
138
|
return join2(homedir2(), ".open-party", "data");
|
|
137
139
|
}
|
|
138
|
-
function runMigrations(raw2) {
|
|
139
|
-
const data = raw2;
|
|
140
|
-
if (!data || typeof data !== "object") {
|
|
141
|
-
throw new Error("Snapshot is not a valid object");
|
|
142
|
-
}
|
|
143
|
-
const version = typeof data.version === "number" ? data.version : 0;
|
|
144
|
-
let snapshot = {
|
|
145
|
-
version,
|
|
146
|
-
saved_at: typeof data.saved_at === "number" ? data.saved_at : 0,
|
|
147
|
-
agents: Array.isArray(data.agents) ? data.agents : [],
|
|
148
|
-
history: typeof data.history === "object" && data.history !== null && !Array.isArray(data.history) ? data.history : {}
|
|
149
|
-
};
|
|
150
|
-
for (const m of MIGRATORS) {
|
|
151
|
-
if (m.version > snapshot.version) {
|
|
152
|
-
snapshot = m.migrate(snapshot);
|
|
153
|
-
snapshot.version = m.version;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
return snapshot;
|
|
157
|
-
}
|
|
158
140
|
function abortableSleep(ms, signal) {
|
|
159
141
|
return new Promise((resolve, reject) => {
|
|
160
142
|
const timer = setTimeout(resolve, ms);
|
|
@@ -168,24 +150,19 @@ function abortableSleep(ms, signal) {
|
|
|
168
150
|
);
|
|
169
151
|
});
|
|
170
152
|
}
|
|
171
|
-
var CURRENT_SCHEMA_VERSION, SNAPSHOT_FILE, SHUTDOWN_MARKER_FILE, DEFAULT_SNAPSHOT_INTERVAL_MS,
|
|
153
|
+
var CURRENT_SCHEMA_VERSION, SNAPSHOT_FILE, SHUTDOWN_MARKER_FILE, DEFAULT_SNAPSHOT_INTERVAL_MS, SnapshotManager;
|
|
172
154
|
var init_persistence = __esm({
|
|
173
155
|
"src/server/persistence.ts"() {
|
|
174
156
|
"use strict";
|
|
175
157
|
init_logger();
|
|
176
|
-
CURRENT_SCHEMA_VERSION =
|
|
158
|
+
CURRENT_SCHEMA_VERSION = 2;
|
|
177
159
|
SNAPSHOT_FILE = "snapshot.json";
|
|
178
160
|
SHUTDOWN_MARKER_FILE = "shutdown-marker.json";
|
|
179
161
|
DEFAULT_SNAPSHOT_INTERVAL_MS = 6e4;
|
|
180
|
-
DEBOUNCE_MS = 5e3;
|
|
181
|
-
MIGRATORS = [
|
|
182
|
-
// Future: { version: 2, migrate(snapshot) { ... } },
|
|
183
|
-
];
|
|
184
162
|
SnapshotManager = class {
|
|
185
163
|
_dir;
|
|
186
164
|
_snapshotPath;
|
|
187
165
|
_markerPath;
|
|
188
|
-
_debounceTimer = null;
|
|
189
166
|
constructor(dataDir) {
|
|
190
167
|
this._dir = dataDir ?? dataDirPath();
|
|
191
168
|
this._snapshotPath = join2(this._dir, SNAPSHOT_FILE);
|
|
@@ -203,13 +180,13 @@ var init_persistence = __esm({
|
|
|
203
180
|
// ------------------------------------------------------------------
|
|
204
181
|
// Write / Load
|
|
205
182
|
// ------------------------------------------------------------------
|
|
206
|
-
/** Atomically write a snapshot of registry agents and
|
|
207
|
-
writeSnapshot(agents,
|
|
183
|
+
/** Atomically write a snapshot of registry agents and ring buffer state. */
|
|
184
|
+
writeSnapshot(agents, buffers) {
|
|
208
185
|
const snapshot = {
|
|
209
186
|
version: CURRENT_SCHEMA_VERSION,
|
|
210
187
|
saved_at: Date.now(),
|
|
211
188
|
agents,
|
|
212
|
-
|
|
189
|
+
buffers
|
|
213
190
|
};
|
|
214
191
|
const serialized = JSON.stringify(snapshot, null, 2);
|
|
215
192
|
const tmpPath = this._snapshotPath + ".tmp";
|
|
@@ -232,7 +209,15 @@ var init_persistence = __esm({
|
|
|
232
209
|
}
|
|
233
210
|
try {
|
|
234
211
|
const raw2 = JSON.parse(readFileSync(this._snapshotPath, "utf-8"));
|
|
235
|
-
|
|
212
|
+
if (!raw2 || typeof raw2 !== "object") {
|
|
213
|
+
throw new Error("Snapshot is not a valid object");
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
version: typeof raw2.version === "number" ? raw2.version : CURRENT_SCHEMA_VERSION,
|
|
217
|
+
saved_at: typeof raw2.saved_at === "number" ? raw2.saved_at : 0,
|
|
218
|
+
agents: Array.isArray(raw2.agents) ? raw2.agents : [],
|
|
219
|
+
buffers: raw2.buffers && typeof raw2.buffers === "object" && !Array.isArray(raw2.buffers) ? raw2.buffers : {}
|
|
220
|
+
};
|
|
236
221
|
} catch (error) {
|
|
237
222
|
logger.warn("Persistence", "Failed to load snapshot (starting fresh)", error);
|
|
238
223
|
return null;
|
|
@@ -259,22 +244,14 @@ var init_persistence = __esm({
|
|
|
259
244
|
}
|
|
260
245
|
return count;
|
|
261
246
|
}
|
|
262
|
-
/** Restore
|
|
263
|
-
|
|
247
|
+
/** Restore ring buffer state into message queue. */
|
|
248
|
+
hydrateBuffers(queue) {
|
|
264
249
|
const snapshot = this.loadSnapshot();
|
|
265
|
-
if (!snapshot || Object.keys(snapshot.
|
|
250
|
+
if (!snapshot || Object.keys(snapshot.buffers).length === 0) return 0;
|
|
251
|
+
queue.restoreBufferSnapshots(snapshot.buffers);
|
|
266
252
|
let totalEntries = 0;
|
|
267
|
-
for (const
|
|
268
|
-
|
|
269
|
-
queue.logToHistory(agentId, entry.direction, {
|
|
270
|
-
sender_id: entry.sender_id,
|
|
271
|
-
recipient_id: entry.recipient_id,
|
|
272
|
-
summary: entry.summary,
|
|
273
|
-
content: entry.content,
|
|
274
|
-
timestamp: entry.timestamp
|
|
275
|
-
});
|
|
276
|
-
totalEntries++;
|
|
277
|
-
}
|
|
253
|
+
for (const snap of Object.values(snapshot.buffers)) {
|
|
254
|
+
totalEntries += snap.entries.length;
|
|
278
255
|
}
|
|
279
256
|
return totalEntries;
|
|
280
257
|
}
|
|
@@ -308,13 +285,13 @@ var init_persistence = __esm({
|
|
|
308
285
|
return existsSync2(this._markerPath);
|
|
309
286
|
}
|
|
310
287
|
// ------------------------------------------------------------------
|
|
311
|
-
// Snapshot loop
|
|
288
|
+
// Snapshot loop
|
|
312
289
|
// ------------------------------------------------------------------
|
|
313
290
|
/**
|
|
314
291
|
* Start periodic snapshot background loop.
|
|
315
292
|
* Writes snapshot every `intervalMs` milliseconds until signal is aborted.
|
|
316
293
|
*/
|
|
317
|
-
async startSnapshotLoop(signal, getAgents,
|
|
294
|
+
async startSnapshotLoop(signal, getAgents, getBuffers, intervalMs = DEFAULT_SNAPSHOT_INTERVAL_MS) {
|
|
318
295
|
while (!signal.aborted) {
|
|
319
296
|
try {
|
|
320
297
|
await abortableSleep(intervalMs, signal);
|
|
@@ -324,36 +301,12 @@ var init_persistence = __esm({
|
|
|
324
301
|
}
|
|
325
302
|
if (signal.aborted) break;
|
|
326
303
|
try {
|
|
327
|
-
this.writeSnapshot(getAgents(),
|
|
304
|
+
this.writeSnapshot(getAgents(), getBuffers());
|
|
328
305
|
} catch (error) {
|
|
329
306
|
logger.warn("Persistence", "Periodic snapshot failed", error);
|
|
330
307
|
}
|
|
331
308
|
}
|
|
332
309
|
}
|
|
333
|
-
/**
|
|
334
|
-
* Schedule a debounced snapshot write.
|
|
335
|
-
* Multiple calls within DEBOUNCE_MS window are coalesced into one write.
|
|
336
|
-
*/
|
|
337
|
-
scheduleSnapshot(getAgents, getHistory) {
|
|
338
|
-
if (this._debounceTimer !== null) {
|
|
339
|
-
clearTimeout(this._debounceTimer);
|
|
340
|
-
}
|
|
341
|
-
this._debounceTimer = setTimeout(() => {
|
|
342
|
-
this._debounceTimer = null;
|
|
343
|
-
try {
|
|
344
|
-
this.writeSnapshot(getAgents(), getHistory());
|
|
345
|
-
} catch (error) {
|
|
346
|
-
logger.warn("Persistence", "Debounced snapshot failed", error);
|
|
347
|
-
}
|
|
348
|
-
}, DEBOUNCE_MS);
|
|
349
|
-
}
|
|
350
|
-
/** Cancel pending debounced snapshot (called during shutdown). */
|
|
351
|
-
cancelDebounce() {
|
|
352
|
-
if (this._debounceTimer !== null) {
|
|
353
|
-
clearTimeout(this._debounceTimer);
|
|
354
|
-
this._debounceTimer = null;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
310
|
};
|
|
358
311
|
}
|
|
359
312
|
});
|
|
@@ -468,243 +421,293 @@ function getTailscaleIps() {
|
|
|
468
421
|
}
|
|
469
422
|
return [];
|
|
470
423
|
}
|
|
471
|
-
|
|
472
|
-
try {
|
|
473
|
-
return runExec(cmd, timeout);
|
|
474
|
-
} catch (exc) {
|
|
475
|
-
const err = exc;
|
|
476
|
-
const stderrLower = (err.stderr ?? "").toLowerCase();
|
|
477
|
-
if (PERMISSION_KEYWORDS.some((kw) => stderrLower.includes(kw))) {
|
|
478
|
-
try {
|
|
479
|
-
return runExec(["sudo", "-n", ...cmd], timeout);
|
|
480
|
-
} catch {
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
throw exc;
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
function joinTailnet(authKey, timeout = 3e4) {
|
|
487
|
-
const binary = getTailscaleBinary();
|
|
488
|
-
try {
|
|
489
|
-
const output2 = execWithSudoFallback(
|
|
490
|
-
[binary, "up", "--authkey", authKey],
|
|
491
|
-
timeout
|
|
492
|
-
);
|
|
493
|
-
return { success: true, output: output2.trim() };
|
|
494
|
-
} catch (e) {
|
|
495
|
-
const err = e;
|
|
496
|
-
return { success: false, output: (err.stderr ?? err.message ?? "").trim() };
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
function getTailscaleInstallationStatus() {
|
|
500
|
-
const binary = findTailscaleBinary();
|
|
501
|
-
if (!binary) {
|
|
502
|
-
return { state: "not_installed", platform: process.platform };
|
|
503
|
-
}
|
|
504
|
-
try {
|
|
505
|
-
const status = readTailscaleStatus();
|
|
506
|
-
const self = status.Self ?? {};
|
|
507
|
-
const online = self.Online === true;
|
|
508
|
-
if (online) {
|
|
509
|
-
const ips = self.TailscaleIPs;
|
|
510
|
-
const dns = self.DNSName?.replace(/\.$/, "");
|
|
511
|
-
return {
|
|
512
|
-
state: "connected",
|
|
513
|
-
binary,
|
|
514
|
-
tailscale_ip: ips?.[0] ?? "127.0.0.1",
|
|
515
|
-
hostname: dns ?? null
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
return { state: "not_connected", binary };
|
|
519
|
-
} catch (error) {
|
|
520
|
-
console.error("[Tailscale] Failed to get installation status:", error);
|
|
521
|
-
return {
|
|
522
|
-
state: "not_connected",
|
|
523
|
-
binary,
|
|
524
|
-
error: error instanceof Error ? error.message : String(error)
|
|
525
|
-
};
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
function resetTailscaleBinaryCache() {
|
|
529
|
-
cachedBinary = null;
|
|
530
|
-
}
|
|
531
|
-
function logoutTailscale(timeout = 15e3) {
|
|
532
|
-
const binary = getTailscaleBinary();
|
|
533
|
-
try {
|
|
534
|
-
const output2 = runExec([binary, "logout"], timeout);
|
|
535
|
-
resetTailscaleBinaryCache();
|
|
536
|
-
return { success: true, output: output2.trim() };
|
|
537
|
-
} catch (e) {
|
|
538
|
-
const err = e;
|
|
539
|
-
return { success: false, output: (err.stderr ?? err.message ?? "").trim() };
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
function startInteractiveLogin() {
|
|
543
|
-
const binary = getTailscaleBinary();
|
|
544
|
-
const child = nodeSpawn(binary, ["login"], {
|
|
545
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
546
|
-
windowsHide: true
|
|
547
|
-
});
|
|
548
|
-
const urlRegex = /https:\/\/login\.tailscale\.com\/a\/[^\s]+/;
|
|
549
|
-
const promise = new Promise((resolve) => {
|
|
550
|
-
let stdout = "";
|
|
551
|
-
let resolved = false;
|
|
552
|
-
const done = (result) => {
|
|
553
|
-
if (resolved) return;
|
|
554
|
-
resolved = true;
|
|
555
|
-
resolve(result);
|
|
556
|
-
};
|
|
557
|
-
child.stdout?.on("data", (data) => {
|
|
558
|
-
stdout += data.toString();
|
|
559
|
-
const match2 = stdout.match(urlRegex);
|
|
560
|
-
if (match2) {
|
|
561
|
-
done({ success: true, url: match2[0], output: stdout.trim() });
|
|
562
|
-
}
|
|
563
|
-
});
|
|
564
|
-
child.stderr?.on("data", (data) => {
|
|
565
|
-
stdout += data.toString();
|
|
566
|
-
});
|
|
567
|
-
child.on("close", (code) => {
|
|
568
|
-
if (code === 0) {
|
|
569
|
-
done({ success: true, output: stdout.trim() });
|
|
570
|
-
} else {
|
|
571
|
-
done({ success: false, output: stdout.trim() || `Exited with code ${code}` });
|
|
572
|
-
}
|
|
573
|
-
});
|
|
574
|
-
child.on("error", (err) => {
|
|
575
|
-
done({ success: false, output: err.message });
|
|
576
|
-
});
|
|
577
|
-
setTimeout(() => {
|
|
578
|
-
done({ success: false, output: "Timeout waiting for login URL" });
|
|
579
|
-
try {
|
|
580
|
-
child.kill();
|
|
581
|
-
} catch {
|
|
582
|
-
}
|
|
583
|
-
}, 3e4);
|
|
584
|
-
});
|
|
585
|
-
return { promise, process: child };
|
|
586
|
-
}
|
|
587
|
-
function getInstallInstructions(platform) {
|
|
588
|
-
switch (platform) {
|
|
589
|
-
case "linux":
|
|
590
|
-
return {
|
|
591
|
-
os: "linux",
|
|
592
|
-
download_url: "https://tailscale.com/download/linux",
|
|
593
|
-
commands: ["curl -fsSL https://tailscale.com/install.sh | sh"],
|
|
594
|
-
needs_sudo: true
|
|
595
|
-
};
|
|
596
|
-
case "darwin":
|
|
597
|
-
return {
|
|
598
|
-
os: "macOS",
|
|
599
|
-
download_url: "https://tailscale.com/download/mac",
|
|
600
|
-
commands: ["brew install tailscale"],
|
|
601
|
-
needs_sudo: false
|
|
602
|
-
};
|
|
603
|
-
case "win32":
|
|
604
|
-
return {
|
|
605
|
-
os: "windows",
|
|
606
|
-
download_url: "https://tailscale.com/download/windows",
|
|
607
|
-
commands: ["winget install Tailscale.Tailscale"],
|
|
608
|
-
needs_sudo: false
|
|
609
|
-
};
|
|
610
|
-
default:
|
|
611
|
-
return {
|
|
612
|
-
os: platform,
|
|
613
|
-
download_url: "https://tailscale.com/download/",
|
|
614
|
-
commands: [],
|
|
615
|
-
needs_sudo: false
|
|
616
|
-
};
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
var cachedBinary, PERMISSION_KEYWORDS;
|
|
424
|
+
var cachedBinary;
|
|
620
425
|
var init_tailscale = __esm({
|
|
621
426
|
"src/infra/tailscale.ts"() {
|
|
622
427
|
"use strict";
|
|
623
428
|
cachedBinary = null;
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// src/server/ring-buffer.ts
|
|
433
|
+
var DEFAULT_CAPACITY, AgentRingBuffer;
|
|
434
|
+
var init_ring_buffer = __esm({
|
|
435
|
+
"src/server/ring-buffer.ts"() {
|
|
436
|
+
"use strict";
|
|
437
|
+
DEFAULT_CAPACITY = 200;
|
|
438
|
+
AgentRingBuffer = class {
|
|
439
|
+
_buffer;
|
|
440
|
+
_capacity;
|
|
441
|
+
_head = 0;
|
|
442
|
+
// next write position (0 ~ capacity-1)
|
|
443
|
+
_nextSeq = 1;
|
|
444
|
+
// next sequence number to assign
|
|
445
|
+
_count = 0;
|
|
446
|
+
// valid entries currently in buffer
|
|
447
|
+
_cursor = 0;
|
|
448
|
+
// server-side read cursor (last consumed seq)
|
|
449
|
+
constructor(capacity = DEFAULT_CAPACITY) {
|
|
450
|
+
this._capacity = Math.max(1, capacity);
|
|
451
|
+
this._buffer = new Array(this._capacity);
|
|
452
|
+
}
|
|
453
|
+
// ------------------------------------------------------------------
|
|
454
|
+
// Write
|
|
455
|
+
// ------------------------------------------------------------------
|
|
456
|
+
/** Write an entry to the buffer. Returns the assigned sequence number. */
|
|
457
|
+
write(direction, envelope) {
|
|
458
|
+
const seq = this._nextSeq++;
|
|
459
|
+
const entry = {
|
|
460
|
+
seq,
|
|
461
|
+
direction,
|
|
462
|
+
sender_id: envelope.sender_id,
|
|
463
|
+
recipient_id: envelope.recipient_id,
|
|
464
|
+
summary: envelope.summary,
|
|
465
|
+
content: envelope.content,
|
|
466
|
+
timestamp: envelope.timestamp ?? Date.now() / 1e3
|
|
467
|
+
};
|
|
468
|
+
this._buffer[this._head] = entry;
|
|
469
|
+
this._head = (this._head + 1) % this._capacity;
|
|
470
|
+
if (this._count < this._capacity) {
|
|
471
|
+
this._count++;
|
|
472
|
+
}
|
|
473
|
+
return seq;
|
|
474
|
+
}
|
|
475
|
+
// ------------------------------------------------------------------
|
|
476
|
+
// Read (cursor-based — dequeue semantics)
|
|
477
|
+
// ------------------------------------------------------------------
|
|
478
|
+
/**
|
|
479
|
+
* Read up to maxCount entries after the cursor, then advance the cursor.
|
|
480
|
+
* Non-destructive: entries remain in the buffer for history queries.
|
|
481
|
+
*/
|
|
482
|
+
dequeue(maxCount = 50) {
|
|
483
|
+
const unread = this._readSince(this._cursor);
|
|
484
|
+
if (unread.length === 0) return [];
|
|
485
|
+
const taken = unread.slice(0, maxCount);
|
|
486
|
+
this._cursor = taken[taken.length - 1].seq;
|
|
487
|
+
return taken;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Like dequeue(), but only returns 'received' entries.
|
|
491
|
+
* Cursor advances to the last returned 'received' entry's seq.
|
|
492
|
+
* Used for inbox semantics — agents only see incoming messages.
|
|
493
|
+
*/
|
|
494
|
+
dequeueReceived(maxCount = 50) {
|
|
495
|
+
const all = this._readSince(this._cursor);
|
|
496
|
+
if (all.length === 0) return [];
|
|
497
|
+
const received = all.filter((e) => e.direction === "received").slice(0, maxCount);
|
|
498
|
+
if (received.length === 0) return [];
|
|
499
|
+
this._cursor = received[received.length - 1].seq;
|
|
500
|
+
return received;
|
|
501
|
+
}
|
|
502
|
+
/** Number of unread entries (next_seq - cursor). */
|
|
503
|
+
unreadCount() {
|
|
504
|
+
const diff = this._nextSeq - 1 - this._cursor;
|
|
505
|
+
return Math.max(0, diff);
|
|
506
|
+
}
|
|
507
|
+
/** Number of unread 'received' entries after cursor. */
|
|
508
|
+
unreadReceivedCount() {
|
|
509
|
+
const all = this._readSince(this._cursor);
|
|
510
|
+
let count = 0;
|
|
511
|
+
for (const e of all) {
|
|
512
|
+
if (e.direction === "received") count++;
|
|
513
|
+
}
|
|
514
|
+
return count;
|
|
515
|
+
}
|
|
516
|
+
// ------------------------------------------------------------------
|
|
517
|
+
// Read (non-destructive — no cursor advance)
|
|
518
|
+
// ------------------------------------------------------------------
|
|
519
|
+
/** Read all entries with seq > sinceSeq. Does NOT advance cursor. */
|
|
520
|
+
readSince(sinceSeq) {
|
|
521
|
+
return this._readSince(sinceSeq);
|
|
522
|
+
}
|
|
523
|
+
/** Get the most recent N entries regardless of cursor. */
|
|
524
|
+
getRecent(limit = 20) {
|
|
525
|
+
const all = this._allSorted();
|
|
526
|
+
return all.slice(-limit);
|
|
527
|
+
}
|
|
528
|
+
/** Get total number of valid entries in the buffer. */
|
|
529
|
+
get count() {
|
|
530
|
+
return this._count;
|
|
531
|
+
}
|
|
532
|
+
// ------------------------------------------------------------------
|
|
533
|
+
// Lifecycle
|
|
534
|
+
// ------------------------------------------------------------------
|
|
535
|
+
/** Clear the buffer and reset cursor. */
|
|
536
|
+
clear() {
|
|
537
|
+
this._buffer.fill(void 0);
|
|
538
|
+
this._head = 0;
|
|
539
|
+
this._nextSeq = 1;
|
|
540
|
+
this._count = 0;
|
|
541
|
+
this._cursor = 0;
|
|
542
|
+
}
|
|
543
|
+
// ------------------------------------------------------------------
|
|
544
|
+
// Snapshot / Restore
|
|
545
|
+
// ------------------------------------------------------------------
|
|
546
|
+
/** Export buffer state for persistence. */
|
|
547
|
+
getSnapshot() {
|
|
548
|
+
return {
|
|
549
|
+
entries: this._allSorted(),
|
|
550
|
+
next_seq: this._nextSeq,
|
|
551
|
+
cursor: this._cursor
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
/** Restore buffer from a snapshot. */
|
|
555
|
+
restoreFromSnapshot(snap) {
|
|
556
|
+
this.clear();
|
|
557
|
+
for (const entry of snap.entries) {
|
|
558
|
+
this._buffer[this._head] = entry;
|
|
559
|
+
this._head = (this._head + 1) % this._capacity;
|
|
560
|
+
this._count++;
|
|
561
|
+
}
|
|
562
|
+
this._nextSeq = snap.next_seq;
|
|
563
|
+
this._cursor = snap.cursor;
|
|
564
|
+
}
|
|
565
|
+
/** Get the next sequence number that will be assigned. */
|
|
566
|
+
get nextSeq() {
|
|
567
|
+
return this._nextSeq;
|
|
568
|
+
}
|
|
569
|
+
/** Get the current cursor position. */
|
|
570
|
+
get cursor() {
|
|
571
|
+
return this._cursor;
|
|
572
|
+
}
|
|
573
|
+
// ------------------------------------------------------------------
|
|
574
|
+
// Private helpers
|
|
575
|
+
// ------------------------------------------------------------------
|
|
576
|
+
/**
|
|
577
|
+
* Collect all valid entries sorted by seq.
|
|
578
|
+
* O(count) — scans the ring buffer once.
|
|
579
|
+
*/
|
|
580
|
+
_allSorted() {
|
|
581
|
+
const result = [];
|
|
582
|
+
for (let i = 0; i < this._capacity; i++) {
|
|
583
|
+
const entry = this._buffer[i];
|
|
584
|
+
if (entry !== void 0) {
|
|
585
|
+
result.push(entry);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
result.sort((a, b) => a.seq - b.seq);
|
|
589
|
+
return result;
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Read entries with seq > sinceSeq.
|
|
593
|
+
* Uses binary search on sorted entries for efficiency.
|
|
594
|
+
*/
|
|
595
|
+
_readSince(sinceSeq) {
|
|
596
|
+
const all = this._allSorted();
|
|
597
|
+
let lo = 0;
|
|
598
|
+
let hi = all.length;
|
|
599
|
+
while (lo < hi) {
|
|
600
|
+
const mid = lo + hi >>> 1;
|
|
601
|
+
if (all[mid].seq <= sinceSeq) {
|
|
602
|
+
lo = mid + 1;
|
|
603
|
+
} else {
|
|
604
|
+
hi = mid;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return all.slice(lo);
|
|
608
|
+
}
|
|
609
|
+
};
|
|
635
610
|
}
|
|
636
611
|
});
|
|
637
612
|
|
|
638
613
|
// src/server/message-queue.ts
|
|
639
|
-
|
|
614
|
+
function toEnvelope(e) {
|
|
615
|
+
return {
|
|
616
|
+
sender_id: e.sender_id,
|
|
617
|
+
recipient_id: e.recipient_id,
|
|
618
|
+
summary: e.summary,
|
|
619
|
+
content: e.content,
|
|
620
|
+
timestamp: e.timestamp
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
function toHistoryEntry(e) {
|
|
624
|
+
return {
|
|
625
|
+
direction: e.direction,
|
|
626
|
+
sender_id: e.sender_id,
|
|
627
|
+
recipient_id: e.recipient_id,
|
|
628
|
+
summary: e.summary,
|
|
629
|
+
content: e.content,
|
|
630
|
+
timestamp: e.timestamp
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
var DEFAULT_CAPACITY2, MessageQueue;
|
|
640
634
|
var init_message_queue = __esm({
|
|
641
635
|
"src/server/message-queue.ts"() {
|
|
642
636
|
"use strict";
|
|
643
|
-
|
|
637
|
+
init_ring_buffer();
|
|
638
|
+
DEFAULT_CAPACITY2 = 200;
|
|
644
639
|
MessageQueue = class {
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
q = [];
|
|
652
|
-
this._queues.set(agentId, q);
|
|
640
|
+
_buffers = /* @__PURE__ */ new Map();
|
|
641
|
+
_getOrCreate(agentId) {
|
|
642
|
+
let buf = this._buffers.get(agentId);
|
|
643
|
+
if (!buf) {
|
|
644
|
+
buf = new AgentRingBuffer(DEFAULT_CAPACITY2);
|
|
645
|
+
this._buffers.set(agentId, buf);
|
|
653
646
|
}
|
|
654
|
-
|
|
655
|
-
return q.length;
|
|
647
|
+
return buf;
|
|
656
648
|
}
|
|
657
|
-
/**
|
|
649
|
+
/** Enqueue a message for agentId. Returns the unread count after enqueue. */
|
|
650
|
+
enqueue(agentId, envelope) {
|
|
651
|
+
const buf = this._getOrCreate(agentId);
|
|
652
|
+
buf.write("received", envelope);
|
|
653
|
+
return buf.unreadReceivedCount();
|
|
654
|
+
}
|
|
655
|
+
/** Pop up to maxCount received messages for agentId (non-destructive, cursor-based). */
|
|
658
656
|
dequeue(agentId, maxCount = 50) {
|
|
659
|
-
const
|
|
660
|
-
if (!
|
|
661
|
-
return
|
|
657
|
+
const buf = this._buffers.get(agentId);
|
|
658
|
+
if (!buf) return [];
|
|
659
|
+
return buf.dequeueReceived(maxCount).map(toEnvelope);
|
|
662
660
|
}
|
|
663
|
-
/** Return the number of pending messages for agentId. */
|
|
661
|
+
/** Return the number of pending received messages for agentId. */
|
|
664
662
|
pendingCount(agentId) {
|
|
665
|
-
return this.
|
|
663
|
+
return this._buffers.get(agentId)?.unreadReceivedCount() ?? 0;
|
|
666
664
|
}
|
|
667
|
-
/** Clean up
|
|
665
|
+
/** Clean up buffer when agent is removed. */
|
|
668
666
|
removeAgent(agentId) {
|
|
669
|
-
this.
|
|
667
|
+
this._buffers.delete(agentId);
|
|
670
668
|
}
|
|
671
|
-
/** Record a message in an agent's history. */
|
|
669
|
+
/** Record a message in an agent's history (writes to the same ring buffer). */
|
|
672
670
|
logToHistory(agentId, direction, envelope) {
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
h = [];
|
|
676
|
-
this._history.set(agentId, h);
|
|
677
|
-
}
|
|
678
|
-
h.push({
|
|
679
|
-
direction,
|
|
680
|
-
sender_id: envelope.sender_id,
|
|
681
|
-
recipient_id: envelope.recipient_id,
|
|
682
|
-
summary: envelope.summary,
|
|
683
|
-
content: envelope.content,
|
|
684
|
-
timestamp: envelope.timestamp ?? Date.now() / 1e3
|
|
685
|
-
});
|
|
686
|
-
if (h.length > HISTORY_CAP) {
|
|
687
|
-
this._history.set(agentId, h.slice(-HISTORY_CAP));
|
|
688
|
-
}
|
|
671
|
+
const buf = this._getOrCreate(agentId);
|
|
672
|
+
buf.write(direction, envelope);
|
|
689
673
|
}
|
|
690
674
|
/** Get recent N history entries for an agent. */
|
|
691
675
|
getHistory(agentId, limit = 20) {
|
|
692
|
-
const
|
|
693
|
-
if (!
|
|
694
|
-
return
|
|
676
|
+
const buf = this._buffers.get(agentId);
|
|
677
|
+
if (!buf) return [];
|
|
678
|
+
return buf.getRecent(limit).map(toHistoryEntry);
|
|
695
679
|
}
|
|
696
|
-
/** Clean up history when agent is removed. */
|
|
680
|
+
/** Clean up history when agent is removed (same as removeAgent — unified storage). */
|
|
697
681
|
removeAgentHistory(agentId) {
|
|
698
|
-
this.
|
|
682
|
+
this._buffers.delete(agentId);
|
|
699
683
|
}
|
|
700
|
-
/**
|
|
684
|
+
/**
|
|
685
|
+
* Return a shallow copy of the full history map (for persistence snapshots).
|
|
686
|
+
* Kept for backward compatibility with persistence v1 consumers.
|
|
687
|
+
*/
|
|
701
688
|
getHistorySnapshot() {
|
|
702
689
|
const copy = {};
|
|
703
|
-
for (const [agentId,
|
|
704
|
-
copy[agentId] =
|
|
690
|
+
for (const [agentId, buf] of this._buffers) {
|
|
691
|
+
copy[agentId] = buf.getRecent().map(toHistoryEntry);
|
|
705
692
|
}
|
|
706
693
|
return copy;
|
|
707
694
|
}
|
|
695
|
+
/** Return ring buffer snapshots for persistence v2. */
|
|
696
|
+
getBufferSnapshots() {
|
|
697
|
+
const result = {};
|
|
698
|
+
for (const [agentId, buf] of this._buffers) {
|
|
699
|
+
result[agentId] = buf.getSnapshot();
|
|
700
|
+
}
|
|
701
|
+
return result;
|
|
702
|
+
}
|
|
703
|
+
/** Restore buffers from v2 snapshots. */
|
|
704
|
+
restoreBufferSnapshots(snapshots) {
|
|
705
|
+
for (const [agentId, snap] of Object.entries(snapshots)) {
|
|
706
|
+
const buf = new AgentRingBuffer(DEFAULT_CAPACITY2);
|
|
707
|
+
buf.restoreFromSnapshot(snap);
|
|
708
|
+
this._buffers.set(agentId, buf);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
708
711
|
};
|
|
709
712
|
}
|
|
710
713
|
});
|
|
@@ -719,7 +722,7 @@ function classifyFetchError(error) {
|
|
|
719
722
|
if (error instanceof DOMException && error.name === "AbortError") return null;
|
|
720
723
|
return null;
|
|
721
724
|
}
|
|
722
|
-
var UNKNOWN,
|
|
725
|
+
var UNKNOWN, ALIVE, DEAD, MAX_FAILURES, BACKOFF_BASE, BACKOFF_CAP, PeerDiscovery;
|
|
723
726
|
var init_peer_discovery = __esm({
|
|
724
727
|
"src/server/peer-discovery.ts"() {
|
|
725
728
|
"use strict";
|
|
@@ -728,17 +731,11 @@ var init_peer_discovery = __esm({
|
|
|
728
731
|
init_persistence();
|
|
729
732
|
init_logger();
|
|
730
733
|
UNKNOWN = "UNKNOWN";
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
DOWN = "DOWN";
|
|
735
|
-
NOT_SERVER = "NOT_SERVER";
|
|
736
|
-
MAYBE = "MAYBE";
|
|
737
|
-
MAYBE_MAX_RETRIES = 3;
|
|
734
|
+
ALIVE = "ALIVE";
|
|
735
|
+
DEAD = "DEAD";
|
|
736
|
+
MAX_FAILURES = 3;
|
|
738
737
|
BACKOFF_BASE = 60;
|
|
739
738
|
BACKOFF_CAP = 900;
|
|
740
|
-
FAILURE_SUSPECT = 2;
|
|
741
|
-
FAILURE_DOWN = 3;
|
|
742
739
|
PeerDiscovery = class {
|
|
743
740
|
_selfIp;
|
|
744
741
|
_peers = /* @__PURE__ */ new Map();
|
|
@@ -753,10 +750,10 @@ var init_peer_discovery = __esm({
|
|
|
753
750
|
getPeerForAgent(agentId) {
|
|
754
751
|
return this._remoteAgents.get(agentId)?.sourcePeerIp;
|
|
755
752
|
}
|
|
756
|
-
/** Return true if the peer is
|
|
753
|
+
/** Return true if the peer is alive. */
|
|
757
754
|
isPeerReachable(peerIp) {
|
|
758
755
|
const ps = this._peers.get(peerIp);
|
|
759
|
-
return ps !== void 0 &&
|
|
756
|
+
return ps !== void 0 && ps.status === ALIVE;
|
|
760
757
|
}
|
|
761
758
|
/** Return all remote agents (including unreachable ones). */
|
|
762
759
|
getAllRemoteAgents() {
|
|
@@ -766,7 +763,7 @@ var init_peer_discovery = __esm({
|
|
|
766
763
|
getReachableRemoteAgents() {
|
|
767
764
|
return Array.from(this._remoteAgents.values()).filter((e) => e.reachable).map((e) => e.agentInfo);
|
|
768
765
|
}
|
|
769
|
-
/** Return all known peer states
|
|
766
|
+
/** Return all known peer states. */
|
|
770
767
|
getPeerStates() {
|
|
771
768
|
return Array.from(this._peers.values()).map((ps) => ({
|
|
772
769
|
ip: ps.ip,
|
|
@@ -802,23 +799,19 @@ var init_peer_discovery = __esm({
|
|
|
802
799
|
const peerIps = this.getTailscalePeers();
|
|
803
800
|
for (const ip of peerIps) {
|
|
804
801
|
if (!this._peers.has(ip)) {
|
|
805
|
-
this._peers.set(ip, { ip, status: UNKNOWN, consecutiveFailures: 0, lastProbeAt: 0, backoffUntil: null
|
|
802
|
+
this._peers.set(ip, { ip, status: UNKNOWN, consecutiveFailures: 0, lastProbeAt: 0, backoffUntil: null });
|
|
806
803
|
}
|
|
807
804
|
}
|
|
808
805
|
const now = performance.now() / 1e3;
|
|
809
806
|
for (const ip of peerIps) {
|
|
810
807
|
const ps = this._peers.get(ip);
|
|
811
|
-
if (ps.status ===
|
|
808
|
+
if (ps.status === DEAD) {
|
|
812
809
|
if (ps.backoffUntil !== null && now < ps.backoffUntil) continue;
|
|
813
810
|
ps.status = UNKNOWN;
|
|
814
811
|
}
|
|
815
|
-
if (ps.status === MAYBE && ps.maybeRetries >= MAYBE_MAX_RETRIES) {
|
|
816
|
-
this.transition(ps, NOT_SERVER);
|
|
817
|
-
continue;
|
|
818
|
-
}
|
|
819
812
|
await this.probePeer(ps);
|
|
820
813
|
}
|
|
821
|
-
this.
|
|
814
|
+
this.evictDeadAgents();
|
|
822
815
|
this.evictStaleAgents();
|
|
823
816
|
}
|
|
824
817
|
// ------------------------------------------------------------------
|
|
@@ -856,12 +849,10 @@ var init_peer_discovery = __esm({
|
|
|
856
849
|
async probePeer(ps) {
|
|
857
850
|
ps.lastProbeAt = Date.now() / 1e3;
|
|
858
851
|
const healthy = await this.checkHealth(ps.ip);
|
|
859
|
-
if (healthy ===
|
|
860
|
-
this.handleProbeFailure(ps, true);
|
|
861
|
-
} else if (healthy) {
|
|
852
|
+
if (healthy === true) {
|
|
862
853
|
await this.handleProbeSuccess(ps);
|
|
863
|
-
} else {
|
|
864
|
-
this.handleProbeFailure(ps
|
|
854
|
+
} else if (healthy === false) {
|
|
855
|
+
this.handleProbeFailure(ps);
|
|
865
856
|
}
|
|
866
857
|
}
|
|
867
858
|
async checkHealth(ip) {
|
|
@@ -882,55 +873,24 @@ var init_peer_discovery = __esm({
|
|
|
882
873
|
// ------------------------------------------------------------------
|
|
883
874
|
async handleProbeSuccess(ps) {
|
|
884
875
|
ps.consecutiveFailures = 0;
|
|
885
|
-
ps.maybeRetries = 0;
|
|
886
876
|
ps.backoffUntil = null;
|
|
887
|
-
if (ps.status !==
|
|
888
|
-
|
|
877
|
+
if (ps.status !== ALIVE) {
|
|
878
|
+
const old = ps.status;
|
|
879
|
+
ps.status = ALIVE;
|
|
880
|
+
logger.info("Discovery", `Peer ${ps.ip}: ${old} -> ALIVE`);
|
|
889
881
|
}
|
|
890
882
|
await this.syncAgents(ps.ip);
|
|
891
883
|
}
|
|
892
|
-
handleProbeFailure(ps
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
}
|
|
901
|
-
} else {
|
|
902
|
-
this.transition(ps, NOT_SERVER);
|
|
884
|
+
handleProbeFailure(ps) {
|
|
885
|
+
ps.consecutiveFailures++;
|
|
886
|
+
if (ps.consecutiveFailures >= MAX_FAILURES) {
|
|
887
|
+
const old = ps.status;
|
|
888
|
+
ps.status = DEAD;
|
|
889
|
+
if (old !== DEAD) {
|
|
890
|
+
const delay = Math.min(BACKOFF_BASE * Math.pow(2, ps.consecutiveFailures - 1), BACKOFF_CAP);
|
|
891
|
+
ps.backoffUntil = performance.now() / 1e3 + delay;
|
|
892
|
+
logger.info("Discovery", `Peer ${ps.ip}: ${old} -> DEAD (backoff ${delay}s)`);
|
|
903
893
|
}
|
|
904
|
-
} else if (ps.status === PARTY_SERVER) {
|
|
905
|
-
ps.consecutiveFailures++;
|
|
906
|
-
if (ps.consecutiveFailures >= FAILURE_DOWN) {
|
|
907
|
-
this.transition(ps, DOWN);
|
|
908
|
-
} else if (ps.consecutiveFailures >= FAILURE_SUSPECT) {
|
|
909
|
-
this.transition(ps, SUSPECT);
|
|
910
|
-
} else {
|
|
911
|
-
this.transition(ps, DEGRADED);
|
|
912
|
-
}
|
|
913
|
-
} else if (ps.status === DEGRADED || ps.status === SUSPECT) {
|
|
914
|
-
ps.consecutiveFailures++;
|
|
915
|
-
if (ps.consecutiveFailures >= FAILURE_DOWN) {
|
|
916
|
-
this.transition(ps, DOWN);
|
|
917
|
-
} else if (ps.status === DEGRADED && ps.consecutiveFailures >= FAILURE_SUSPECT) {
|
|
918
|
-
this.transition(ps, SUSPECT);
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
transition(ps, newStatus) {
|
|
923
|
-
const old = ps.status;
|
|
924
|
-
ps.status = newStatus;
|
|
925
|
-
if (old !== newStatus) {
|
|
926
|
-
logger.info("Discovery", `Peer ${ps.ip}: ${old} -> ${newStatus}`);
|
|
927
|
-
}
|
|
928
|
-
if (newStatus === NOT_SERVER) {
|
|
929
|
-
const retries = ps.maybeRetries > 0 ? ps.maybeRetries : 1;
|
|
930
|
-
const delay = Math.min(BACKOFF_BASE * Math.pow(2, retries - 1), BACKOFF_CAP);
|
|
931
|
-
ps.backoffUntil = performance.now() / 1e3 + delay;
|
|
932
|
-
}
|
|
933
|
-
if (newStatus === DOWN) {
|
|
934
894
|
for (const entry of this._remoteAgents.values()) {
|
|
935
895
|
if (entry.sourcePeerIp === ps.ip) {
|
|
936
896
|
entry.reachable = false;
|
|
@@ -973,14 +933,14 @@ var init_peer_discovery = __esm({
|
|
|
973
933
|
// ------------------------------------------------------------------
|
|
974
934
|
// Cleanup
|
|
975
935
|
// ------------------------------------------------------------------
|
|
976
|
-
|
|
977
|
-
const
|
|
936
|
+
evictDeadAgents() {
|
|
937
|
+
const deadPeers = /* @__PURE__ */ new Set();
|
|
978
938
|
for (const [ip, ps] of this._peers) {
|
|
979
|
-
if (ps.status ===
|
|
939
|
+
if (ps.status === DEAD) deadPeers.add(ip);
|
|
980
940
|
}
|
|
981
|
-
if (!
|
|
941
|
+
if (!deadPeers.size) return;
|
|
982
942
|
for (const [aid, entry] of this._remoteAgents) {
|
|
983
|
-
if (
|
|
943
|
+
if (deadPeers.has(entry.sourcePeerIp)) {
|
|
984
944
|
this._remoteAgents.delete(aid);
|
|
985
945
|
}
|
|
986
946
|
}
|
|
@@ -1003,8 +963,10 @@ var AgentRegistry;
|
|
|
1003
963
|
var init_registry = __esm({
|
|
1004
964
|
"src/server/registry.ts"() {
|
|
1005
965
|
"use strict";
|
|
966
|
+
init_config();
|
|
1006
967
|
AgentRegistry = class {
|
|
1007
968
|
_agents = /* @__PURE__ */ new Map();
|
|
969
|
+
_staleCounts = /* @__PURE__ */ new Map();
|
|
1008
970
|
_selfIp;
|
|
1009
971
|
constructor(selfIp) {
|
|
1010
972
|
this._selfIp = selfIp;
|
|
@@ -1017,19 +979,23 @@ var init_registry = __esm({
|
|
|
1017
979
|
host_ip: this._selfIp,
|
|
1018
980
|
registered_at: now,
|
|
1019
981
|
last_heartbeat: now,
|
|
1020
|
-
metadata: req.metadata ?? {}
|
|
982
|
+
metadata: req.metadata ?? {},
|
|
983
|
+
callback_url: req.callback_url
|
|
1021
984
|
};
|
|
1022
985
|
this._agents.set(req.agent_id, info);
|
|
986
|
+
this._staleCounts.set(req.agent_id, 0);
|
|
1023
987
|
return info;
|
|
1024
988
|
}
|
|
1025
989
|
remove(agentId) {
|
|
1026
990
|
const existed = this._agents.delete(agentId);
|
|
991
|
+
this._staleCounts.delete(agentId);
|
|
1027
992
|
return existed;
|
|
1028
993
|
}
|
|
1029
994
|
heartbeat(agentId) {
|
|
1030
995
|
const info = this._agents.get(agentId);
|
|
1031
996
|
if (!info) throw new Error(`Agent '${agentId}' not registered`);
|
|
1032
997
|
info.last_heartbeat = Date.now() / 1e3;
|
|
998
|
+
this._staleCounts.set(agentId, 0);
|
|
1033
999
|
return info;
|
|
1034
1000
|
}
|
|
1035
1001
|
get(agentId) {
|
|
@@ -1038,19 +1004,30 @@ var init_registry = __esm({
|
|
|
1038
1004
|
listAll() {
|
|
1039
1005
|
return Array.from(this._agents.values());
|
|
1040
1006
|
}
|
|
1041
|
-
/** Remove agents whose last heartbeat is older than timeout seconds.
|
|
1007
|
+
/** Remove agents whose last heartbeat is older than timeout seconds.
|
|
1008
|
+
* Uses a stale counter: an agent must exceed the timeout STALE_THRESHOLD
|
|
1009
|
+
* consecutive times before being actually removed. Any heartbeat resets
|
|
1010
|
+
* the counter to zero, giving transient network issues room to recover.
|
|
1011
|
+
*/
|
|
1042
1012
|
cleanupStale(timeout) {
|
|
1043
1013
|
const now = Date.now() / 1e3;
|
|
1044
|
-
const
|
|
1014
|
+
const toRemove = [];
|
|
1045
1015
|
for (const [aid, info] of this._agents) {
|
|
1046
1016
|
if (now - info.last_heartbeat > timeout) {
|
|
1047
|
-
|
|
1017
|
+
const count = (this._staleCounts.get(aid) ?? 0) + 1;
|
|
1018
|
+
this._staleCounts.set(aid, count);
|
|
1019
|
+
if (count >= STALE_THRESHOLD) {
|
|
1020
|
+
toRemove.push(aid);
|
|
1021
|
+
}
|
|
1022
|
+
} else {
|
|
1023
|
+
this._staleCounts.set(aid, 0);
|
|
1048
1024
|
}
|
|
1049
1025
|
}
|
|
1050
|
-
for (const aid of
|
|
1026
|
+
for (const aid of toRemove) {
|
|
1051
1027
|
this._agents.delete(aid);
|
|
1028
|
+
this._staleCounts.delete(aid);
|
|
1052
1029
|
}
|
|
1053
|
-
return
|
|
1030
|
+
return toRemove;
|
|
1054
1031
|
}
|
|
1055
1032
|
};
|
|
1056
1033
|
}
|
|
@@ -1068,7 +1045,6 @@ __export(state_exports, {
|
|
|
1068
1045
|
messageQueue: () => messageQueue,
|
|
1069
1046
|
refreshSelfIp: () => refreshSelfIp,
|
|
1070
1047
|
registry: () => registry,
|
|
1071
|
-
scheduleSnapshot: () => scheduleSnapshot,
|
|
1072
1048
|
snapshotManager: () => snapshotManager
|
|
1073
1049
|
});
|
|
1074
1050
|
function resolveSelfIp() {
|
|
@@ -1089,12 +1065,6 @@ function refreshSelfIp() {
|
|
|
1089
1065
|
_selfIp = resolveSelfIp();
|
|
1090
1066
|
return _selfIp;
|
|
1091
1067
|
}
|
|
1092
|
-
function scheduleSnapshot() {
|
|
1093
|
-
snapshotManager?.scheduleSnapshot(
|
|
1094
|
-
() => registry.listAll(),
|
|
1095
|
-
() => messageQueue.getHistorySnapshot()
|
|
1096
|
-
);
|
|
1097
|
-
}
|
|
1098
1068
|
function initSnapshotManager(mgr) {
|
|
1099
1069
|
snapshotManager = mgr;
|
|
1100
1070
|
}
|
|
@@ -1119,58 +1089,6 @@ var init_state = __esm({
|
|
|
1119
1089
|
}
|
|
1120
1090
|
});
|
|
1121
1091
|
|
|
1122
|
-
// src/cli/tailscale-installer.ts
|
|
1123
|
-
var tailscale_installer_exports = {};
|
|
1124
|
-
__export(tailscale_installer_exports, {
|
|
1125
|
-
installTailscale: () => installTailscale
|
|
1126
|
-
});
|
|
1127
|
-
import { spawn } from "child_process";
|
|
1128
|
-
async function installTailscale(platform) {
|
|
1129
|
-
const entry = INSTALL_COMMANDS[platform];
|
|
1130
|
-
if (!entry) {
|
|
1131
|
-
return {
|
|
1132
|
-
success: false,
|
|
1133
|
-
output: `Unsupported platform: ${platform}. Please install manually from https://tailscale.com/download`
|
|
1134
|
-
};
|
|
1135
|
-
}
|
|
1136
|
-
const cmd = entry.needsSudo ? "sudo" : entry.cmd;
|
|
1137
|
-
const args = entry.needsSudo ? [entry.cmd, ...entry.args] : entry.args;
|
|
1138
|
-
console.log(`Running: ${cmd} ${args.join(" ")}
|
|
1139
|
-
`);
|
|
1140
|
-
return new Promise((resolve) => {
|
|
1141
|
-
const child = spawn(cmd, args, {
|
|
1142
|
-
stdio: "inherit",
|
|
1143
|
-
windowsHide: true
|
|
1144
|
-
});
|
|
1145
|
-
let exited = false;
|
|
1146
|
-
child.on("close", (code) => {
|
|
1147
|
-
if (exited) return;
|
|
1148
|
-
exited = true;
|
|
1149
|
-
if (code === 0) {
|
|
1150
|
-
resolve({ success: true, output: "Installation completed." });
|
|
1151
|
-
} else {
|
|
1152
|
-
resolve({ success: false, output: `Installation exited with code ${code}` });
|
|
1153
|
-
}
|
|
1154
|
-
});
|
|
1155
|
-
child.on("error", (err) => {
|
|
1156
|
-
if (exited) return;
|
|
1157
|
-
exited = true;
|
|
1158
|
-
resolve({ success: false, output: err.message });
|
|
1159
|
-
});
|
|
1160
|
-
});
|
|
1161
|
-
}
|
|
1162
|
-
var INSTALL_COMMANDS;
|
|
1163
|
-
var init_tailscale_installer = __esm({
|
|
1164
|
-
"src/cli/tailscale-installer.ts"() {
|
|
1165
|
-
"use strict";
|
|
1166
|
-
INSTALL_COMMANDS = {
|
|
1167
|
-
linux: { cmd: "bash", args: ["-c", "curl -fsSL https://tailscale.com/install.sh | sh"], needsSudo: true },
|
|
1168
|
-
darwin: { cmd: "brew", args: ["install", "tailscale"], needsSudo: false },
|
|
1169
|
-
win32: { cmd: "winget", args: ["install", "Tailscale.Tailscale", "--accept-source-agreements"], needsSudo: false }
|
|
1170
|
-
};
|
|
1171
|
-
}
|
|
1172
|
-
});
|
|
1173
|
-
|
|
1174
1092
|
// node_modules/hono/dist/compose.js
|
|
1175
1093
|
var compose = (middleware, onError, onNotFound) => {
|
|
1176
1094
|
return (context, next) => {
|
|
@@ -3991,12 +3909,34 @@ function sanitizeAgentList(agents) {
|
|
|
3991
3909
|
// src/server/routes/agent.ts
|
|
3992
3910
|
init_config();
|
|
3993
3911
|
init_logger();
|
|
3912
|
+
|
|
3913
|
+
// src/server/callback.ts
|
|
3914
|
+
init_logger();
|
|
3915
|
+
var CALLBACK_TIMEOUT_MS = 5e3;
|
|
3916
|
+
function postCallback(callbackUrl, payload) {
|
|
3917
|
+
fetch(callbackUrl, {
|
|
3918
|
+
method: "POST",
|
|
3919
|
+
headers: { "Content-Type": "application/json" },
|
|
3920
|
+
body: JSON.stringify(payload),
|
|
3921
|
+
signal: AbortSignal.timeout(CALLBACK_TIMEOUT_MS)
|
|
3922
|
+
}).then((resp) => {
|
|
3923
|
+
if (!resp.ok) {
|
|
3924
|
+
logger.warn("Webhook", `Callback HTTP error: url=${callbackUrl}, status=${resp.status}`);
|
|
3925
|
+
}
|
|
3926
|
+
}).catch((err) => {
|
|
3927
|
+
logger.warn("Webhook", `Callback failed: url=${callbackUrl}, error=${err.message}`);
|
|
3928
|
+
});
|
|
3929
|
+
}
|
|
3930
|
+
|
|
3931
|
+
// src/server/routes/agent.ts
|
|
3994
3932
|
var agentRoutes = new Hono2();
|
|
3995
3933
|
async function forwardToPeer(peerIp, envelope) {
|
|
3996
3934
|
const url = `http://${peerIp}:${PARTY_PORT}/proxy/receive`;
|
|
3997
3935
|
const payload = { sender_id: envelope.sender_id, content: envelope.content };
|
|
3998
3936
|
if (envelope.recipient_id) payload.recipient_id = envelope.recipient_id;
|
|
3999
3937
|
if (envelope.group_id) payload.group_id = envelope.group_id;
|
|
3938
|
+
if (envelope.summary) payload.summary = envelope.summary;
|
|
3939
|
+
if (envelope.timestamp) payload.timestamp = envelope.timestamp;
|
|
4000
3940
|
try {
|
|
4001
3941
|
await fetch(url, {
|
|
4002
3942
|
method: "POST",
|
|
@@ -4009,26 +3949,46 @@ async function forwardToPeer(peerIp, envelope) {
|
|
|
4009
3949
|
}
|
|
4010
3950
|
}
|
|
4011
3951
|
agentRoutes.post("/register", async (c) => {
|
|
4012
|
-
const { registry: registry2
|
|
3952
|
+
const { registry: registry2 } = await Promise.resolve().then(() => (init_state(), state_exports));
|
|
4013
3953
|
const req = await c.req.json();
|
|
4014
3954
|
const info = registry2.register(req);
|
|
4015
|
-
|
|
4016
|
-
logger.info("Agent", `Registered: ${info.agent_id} (display: "${info.display_name ?? "N/A"}")`);
|
|
3955
|
+
logger.info("Agent", `Registered: ${info.agent_id} (display: "${info.display_name ?? "N/A"}", callback=${info.callback_url ?? "none"})`);
|
|
4017
3956
|
return c.json(sanitizeAgentInfo(info));
|
|
4018
3957
|
});
|
|
4019
3958
|
agentRoutes.post("/remove", async (c) => {
|
|
4020
|
-
const { registry: registry2,
|
|
3959
|
+
const { registry: registry2, messageQueue: messageQueue2 } = await Promise.resolve().then(() => (init_state(), state_exports));
|
|
4021
3960
|
const req = await c.req.json();
|
|
4022
3961
|
const removed = registry2.remove(req.agent_id);
|
|
4023
|
-
if (removed)
|
|
3962
|
+
if (removed) {
|
|
3963
|
+
messageQueue2.removeAgent(req.agent_id);
|
|
3964
|
+
messageQueue2.removeAgentHistory(req.agent_id);
|
|
3965
|
+
}
|
|
4024
3966
|
logger.info("Agent", removed ? `Removed: ${req.agent_id}` : `Remove failed: ${req.agent_id} (not found)`);
|
|
4025
3967
|
return c.json({ status: removed ? "removed" : "not_found" });
|
|
4026
3968
|
});
|
|
4027
3969
|
agentRoutes.post("/heartbeat", async (c) => {
|
|
4028
3970
|
const { registry: registry2 } = await Promise.resolve().then(() => (init_state(), state_exports));
|
|
4029
3971
|
const req = await c.req.json();
|
|
4030
|
-
|
|
4031
|
-
|
|
3972
|
+
let info;
|
|
3973
|
+
try {
|
|
3974
|
+
info = registry2.heartbeat(req.agent_id);
|
|
3975
|
+
if (req.callback_url) {
|
|
3976
|
+
info.callback_url = req.callback_url;
|
|
3977
|
+
}
|
|
3978
|
+
} catch (err) {
|
|
3979
|
+
if (err instanceof Error && err.message.includes("not registered")) {
|
|
3980
|
+
info = registry2.register({
|
|
3981
|
+
agent_id: req.agent_id,
|
|
3982
|
+
display_name: req.display_name,
|
|
3983
|
+
metadata: req.metadata,
|
|
3984
|
+
callback_url: req.callback_url
|
|
3985
|
+
});
|
|
3986
|
+
logger.info("Agent", `Auto-re-registered: ${req.agent_id} (was cleaned up)`);
|
|
3987
|
+
} else {
|
|
3988
|
+
throw err;
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3991
|
+
logger.info("Agent", `Heartbeat from ${req.agent_id}${req.callback_url ? `, callback=${req.callback_url}` : ""}`);
|
|
4032
3992
|
return c.json({ status: "ok", last_heartbeat: info.last_heartbeat });
|
|
4033
3993
|
});
|
|
4034
3994
|
agentRoutes.get("/list", async (c) => {
|
|
@@ -4039,18 +3999,31 @@ agentRoutes.get("/list", async (c) => {
|
|
|
4039
3999
|
return c.json({ agents: sanitizeAgentList(allAgents), count: allAgents.length });
|
|
4040
4000
|
});
|
|
4041
4001
|
agentRoutes.post("/send", async (c) => {
|
|
4042
|
-
const { registry: registry2, messageQueue: messageQueue2, discovery: discovery2
|
|
4002
|
+
const { registry: registry2, messageQueue: messageQueue2, discovery: discovery2 } = await Promise.resolve().then(() => (init_state(), state_exports));
|
|
4043
4003
|
const envelope = await c.req.json();
|
|
4044
4004
|
const recipient = envelope.recipient_id;
|
|
4045
4005
|
if (!recipient) {
|
|
4046
4006
|
return c.json({ status: "error", error: "recipient_id is required" });
|
|
4047
4007
|
}
|
|
4048
|
-
|
|
4008
|
+
const recipientInfo = registry2.get(recipient);
|
|
4009
|
+
if (recipientInfo) {
|
|
4049
4010
|
const stamped = { ...envelope, timestamp: envelope.timestamp ?? Date.now() / 1e3 };
|
|
4050
4011
|
const count = messageQueue2.enqueue(recipient, stamped);
|
|
4051
4012
|
messageQueue2.logToHistory(envelope.sender_id, "sent", stamped);
|
|
4052
|
-
|
|
4053
|
-
|
|
4013
|
+
if (recipientInfo.callback_url) {
|
|
4014
|
+
const callbackPayload = {
|
|
4015
|
+
type: "message_received",
|
|
4016
|
+
recipient_id: recipient,
|
|
4017
|
+
sender_id: envelope.sender_id,
|
|
4018
|
+
summary: envelope.summary,
|
|
4019
|
+
timestamp: stamped.timestamp,
|
|
4020
|
+
pending_count: count
|
|
4021
|
+
};
|
|
4022
|
+
logger.info("Webhook", `Callback: agent=${recipient}, url=${recipientInfo.callback_url}, sender=${envelope.sender_id}`);
|
|
4023
|
+
postCallback(recipientInfo.callback_url, callbackPayload);
|
|
4024
|
+
} else {
|
|
4025
|
+
logger.debug("Webhook", `No callback_url for agent=${recipient}, skipping webhook`);
|
|
4026
|
+
}
|
|
4054
4027
|
logger.info("Agent", `Send ${envelope.sender_id} -> ${recipient}: delivered_locally`);
|
|
4055
4028
|
return c.json({ status: "delivered_locally", target: recipient });
|
|
4056
4029
|
}
|
|
@@ -4060,7 +4033,6 @@ agentRoutes.post("/send", async (c) => {
|
|
|
4060
4033
|
const result = await forwardToPeer(peerIp, envelope);
|
|
4061
4034
|
if (result.status === "forwarded") {
|
|
4062
4035
|
messageQueue2.logToHistory(envelope.sender_id, "sent", { ...envelope, timestamp: envelope.timestamp ?? Date.now() / 1e3 });
|
|
4063
|
-
scheduleSnapshot3();
|
|
4064
4036
|
logger.info("Agent", `Send ${envelope.sender_id} -> ${recipient}: forwarded (peer ${peerIp})`);
|
|
4065
4037
|
}
|
|
4066
4038
|
return c.json(result);
|
|
@@ -4114,9 +4086,22 @@ proxyRoutes.post("/receive", async (c) => {
|
|
|
4114
4086
|
const envelope = await c.req.json();
|
|
4115
4087
|
const stamped = { ...envelope, timestamp: envelope.timestamp ?? Date.now() / 1e3 };
|
|
4116
4088
|
if (envelope.recipient_id) {
|
|
4117
|
-
messageQueue.enqueue(envelope.recipient_id, stamped);
|
|
4089
|
+
const count = messageQueue.enqueue(envelope.recipient_id, stamped);
|
|
4118
4090
|
messageQueue.logToHistory(envelope.recipient_id, "received", stamped);
|
|
4119
4091
|
logger.info("Proxy", `Received msg ${envelope.sender_id} -> ${envelope.recipient_id}`);
|
|
4092
|
+
const recipientInfo = registry.get(envelope.recipient_id);
|
|
4093
|
+
if (recipientInfo?.callback_url) {
|
|
4094
|
+
const callbackPayload = {
|
|
4095
|
+
type: "message_received",
|
|
4096
|
+
recipient_id: envelope.recipient_id,
|
|
4097
|
+
sender_id: envelope.sender_id,
|
|
4098
|
+
summary: envelope.summary,
|
|
4099
|
+
timestamp: stamped.timestamp,
|
|
4100
|
+
pending_count: count
|
|
4101
|
+
};
|
|
4102
|
+
logger.info("Webhook", `Proxy callback: agent=${envelope.recipient_id}, url=${recipientInfo.callback_url}, sender=${envelope.sender_id}`);
|
|
4103
|
+
postCallback(recipientInfo.callback_url, callbackPayload);
|
|
4104
|
+
}
|
|
4120
4105
|
} else {
|
|
4121
4106
|
for (const agent of registry.listAll()) {
|
|
4122
4107
|
messageQueue.enqueue(agent.agent_id, stamped);
|
|
@@ -4148,1224 +4133,6 @@ proxyRoutes.post("/send/:target_ip", async (c) => {
|
|
|
4148
4133
|
}
|
|
4149
4134
|
});
|
|
4150
4135
|
|
|
4151
|
-
// src/server/dashboard-html.ts
|
|
4152
|
-
var DASHBOARD_HTML = `<!DOCTYPE html>
|
|
4153
|
-
<html lang="zh-CN">
|
|
4154
|
-
<head>
|
|
4155
|
-
<meta charset="UTF-8">
|
|
4156
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
4157
|
-
<title>OPEN PARTY // Dashboard</title>
|
|
4158
|
-
<style>
|
|
4159
|
-
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
4160
|
-
:root{
|
|
4161
|
-
--bg:#0a0a0f;--card:#12121a;--border:#1e1e2e;--border-bright:#2a2a3e;
|
|
4162
|
-
--text:#e0e0e8;--muted:#6a6a8a;
|
|
4163
|
-
--cyan:#00fff0;--magenta:#ff00ff;--green:#00ff88;--red:#ff3366;--yellow:#ffaa00;--orange:#ff8800;
|
|
4164
|
-
--font-mono:'JetBrains Mono','Fira Code','Cascadia Code','Consolas','Courier New',monospace;
|
|
4165
|
-
--font-sans:'Inter','Segoe UI',system-ui,-apple-system,sans-serif;
|
|
4166
|
-
}
|
|
4167
|
-
html{font-size:14px}
|
|
4168
|
-
body{
|
|
4169
|
-
background:var(--bg);color:var(--text);font-family:var(--font-sans);
|
|
4170
|
-
min-height:100vh;overflow-x:hidden;position:relative;
|
|
4171
|
-
}
|
|
4172
|
-
/* Grid background */
|
|
4173
|
-
body::before{
|
|
4174
|
-
content:'';position:fixed;inset:0;z-index:0;pointer-events:none;
|
|
4175
|
-
background-image:
|
|
4176
|
-
linear-gradient(rgba(0,255,240,0.03) 1px,transparent 1px),
|
|
4177
|
-
linear-gradient(90deg,rgba(0,255,240,0.03) 1px,transparent 1px);
|
|
4178
|
-
background-size:40px 40px;
|
|
4179
|
-
}
|
|
4180
|
-
/* Scanline overlay */
|
|
4181
|
-
body::after{
|
|
4182
|
-
content:'';position:fixed;inset:0;z-index:9999;pointer-events:none;
|
|
4183
|
-
background:repeating-linear-gradient(
|
|
4184
|
-
0deg,transparent,transparent 2px,rgba(0,0,0,0.08) 2px,rgba(0,0,0,0.08) 4px
|
|
4185
|
-
);
|
|
4186
|
-
}
|
|
4187
|
-
#app{position:relative;z-index:1;max-width:1400px;margin:0 auto;padding:0 20px 40px}
|
|
4188
|
-
|
|
4189
|
-
/* ===== Header ===== */
|
|
4190
|
-
.header{
|
|
4191
|
-
display:flex;align-items:center;justify-content:space-between;
|
|
4192
|
-
padding:16px 0;border-bottom:1px solid var(--border);
|
|
4193
|
-
margin-bottom:20px;flex-wrap:wrap;gap:12px;
|
|
4194
|
-
}
|
|
4195
|
-
.header-title{
|
|
4196
|
-
font-family:var(--font-mono);font-size:1.4rem;font-weight:700;
|
|
4197
|
-
color:var(--cyan);letter-spacing:3px;
|
|
4198
|
-
text-shadow:0 0 10px rgba(0,255,240,0.5),0 0 30px rgba(0,255,240,0.2);
|
|
4199
|
-
}
|
|
4200
|
-
.header-title span{color:var(--muted);font-weight:400;font-size:0.9rem;margin-left:8px;letter-spacing:1px}
|
|
4201
|
-
.header-center{display:flex;align-items:center;gap:16px;flex-wrap:wrap}
|
|
4202
|
-
.header-status{display:flex;align-items:center;gap:6px;font-family:var(--font-mono);font-size:0.85rem}
|
|
4203
|
-
.status-dot{
|
|
4204
|
-
width:8px;height:8px;border-radius:50%;background:var(--green);
|
|
4205
|
-
box-shadow:0 0 6px var(--green);animation:pulse 2s ease-in-out infinite;
|
|
4206
|
-
}
|
|
4207
|
-
.status-dot.offline{background:var(--red);box-shadow:0 0 6px var(--red)}
|
|
4208
|
-
.status-dot.not-installed{background:var(--red);box-shadow:0 0 6px var(--red)}
|
|
4209
|
-
.status-dot.not-connected{background:var(--yellow);box-shadow:0 0 6px var(--yellow)}
|
|
4210
|
-
@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:0.5;transform:scale(0.8)}}
|
|
4211
|
-
.header-meta{color:var(--muted);font-family:var(--font-mono);font-size:0.8rem}
|
|
4212
|
-
.header-right{font-family:var(--font-mono);font-size:0.85rem;color:var(--muted)}
|
|
4213
|
-
.header-right .value{color:var(--cyan)}
|
|
4214
|
-
|
|
4215
|
-
/* ===== Stats Cards ===== */
|
|
4216
|
-
.stats-row{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:24px}
|
|
4217
|
-
@media(max-width:768px){.stats-row{grid-template-columns:repeat(2,1fr)}}
|
|
4218
|
-
.stat-card{
|
|
4219
|
-
background:var(--card);border:1px solid var(--border);border-radius:8px;
|
|
4220
|
-
padding:16px 20px;position:relative;overflow:hidden;
|
|
4221
|
-
transition:border-color 0.3s;
|
|
4222
|
-
}
|
|
4223
|
-
.stat-card:hover{border-color:var(--border-bright)}
|
|
4224
|
-
.stat-card::before{
|
|
4225
|
-
content:'';position:absolute;top:0;left:0;right:0;height:2px;
|
|
4226
|
-
}
|
|
4227
|
-
.stat-card.cyan::before{background:var(--cyan);box-shadow:0 0 10px var(--cyan)}
|
|
4228
|
-
.stat-card.green::before{background:var(--green);box-shadow:0 0 10px var(--green)}
|
|
4229
|
-
.stat-card.magenta::before{background:var(--magenta);box-shadow:0 0 10px var(--magenta)}
|
|
4230
|
-
.stat-card.yellow::before{background:var(--yellow);box-shadow:0 0 10px var(--yellow)}
|
|
4231
|
-
.stat-value{
|
|
4232
|
-
font-family:var(--font-mono);font-size:2rem;font-weight:700;
|
|
4233
|
-
transition:color 0.3s;
|
|
4234
|
-
}
|
|
4235
|
-
.stat-card.cyan .stat-value{color:var(--cyan)}
|
|
4236
|
-
.stat-card.green .stat-value{color:var(--green)}
|
|
4237
|
-
.stat-card.magenta .stat-value{color:var(--magenta)}
|
|
4238
|
-
.stat-card.yellow .stat-value{color:var(--yellow)}
|
|
4239
|
-
.stat-label{color:var(--muted);font-size:0.8rem;margin-top:4px;text-transform:uppercase;letter-spacing:1px}
|
|
4240
|
-
.stat-value.flash{animation:flash 0.3s ease}
|
|
4241
|
-
@keyframes flash{0%{opacity:1}50%{opacity:0.3}100%{opacity:1}}
|
|
4242
|
-
|
|
4243
|
-
/* ===== Main Grid ===== */
|
|
4244
|
-
.main-grid{display:grid;grid-template-columns:2fr 1fr;gap:20px;margin-bottom:24px}
|
|
4245
|
-
@media(max-width:1024px){.main-grid{grid-template-columns:1fr}}
|
|
4246
|
-
.section{
|
|
4247
|
-
background:var(--card);border:1px solid var(--border);border-radius:8px;
|
|
4248
|
-
overflow:hidden;
|
|
4249
|
-
}
|
|
4250
|
-
.section-header{
|
|
4251
|
-
padding:12px 16px;border-bottom:1px solid var(--border);
|
|
4252
|
-
font-family:var(--font-mono);font-size:0.8rem;font-weight:600;
|
|
4253
|
-
color:var(--muted);letter-spacing:2px;text-transform:uppercase;
|
|
4254
|
-
display:flex;align-items:center;gap:8px;
|
|
4255
|
-
}
|
|
4256
|
-
.section-header .dot{width:6px;height:6px;border-radius:50%;background:var(--cyan)}
|
|
4257
|
-
.section-body{padding:16px}
|
|
4258
|
-
|
|
4259
|
-
/* ===== Topology ===== */
|
|
4260
|
-
.topology-container{display:flex;justify-content:center;align-items:center;min-height:300px}
|
|
4261
|
-
.topology-container svg{width:100%;max-width:500px;height:300px}
|
|
4262
|
-
.topo-node{cursor:pointer;transition:filter 0.3s}
|
|
4263
|
-
.topo-node:hover{filter:brightness(1.3)}
|
|
4264
|
-
.topo-label{font-family:var(--font-mono);font-size:10px;fill:var(--muted)}
|
|
4265
|
-
.topo-badge{
|
|
4266
|
-
font-family:var(--font-mono);font-size:9px;fill:var(--bg);
|
|
4267
|
-
font-weight:700;
|
|
4268
|
-
}
|
|
4269
|
-
|
|
4270
|
-
/* ===== Peer Table ===== */
|
|
4271
|
-
.peer-table{width:100%;border-collapse:collapse;font-size:0.85rem}
|
|
4272
|
-
.peer-table th{
|
|
4273
|
-
text-align:left;padding:8px 10px;color:var(--muted);font-weight:600;
|
|
4274
|
-
font-family:var(--font-mono);font-size:0.75rem;text-transform:uppercase;
|
|
4275
|
-
letter-spacing:1px;border-bottom:1px solid var(--border);
|
|
4276
|
-
}
|
|
4277
|
-
.peer-table td{padding:8px 10px;border-bottom:1px solid var(--border);font-family:var(--font-mono);font-size:0.8rem}
|
|
4278
|
-
.peer-table tr:last-child td{border-bottom:none}
|
|
4279
|
-
.peer-table tr:hover td{background:rgba(255,255,255,0.02)}
|
|
4280
|
-
.badge{
|
|
4281
|
-
display:inline-block;padding:2px 8px;border-radius:4px;font-size:0.7rem;
|
|
4282
|
-
font-weight:600;text-transform:uppercase;letter-spacing:0.5px;
|
|
4283
|
-
}
|
|
4284
|
-
.badge-party{background:rgba(0,255,136,0.15);color:var(--green)}
|
|
4285
|
-
.badge-degraded{background:rgba(255,170,0,0.15);color:var(--yellow)}
|
|
4286
|
-
.badge-suspect{background:rgba(255,136,0,0.15);color:var(--orange)}
|
|
4287
|
-
.badge-down{background:rgba(255,51,102,0.15);color:var(--red)}
|
|
4288
|
-
.badge-unknown{background:rgba(106,106,138,0.15);color:var(--muted)}
|
|
4289
|
-
.badge-not_server{background:rgba(106,106,138,0.1);color:var(--muted)}
|
|
4290
|
-
.badge-maybe{background:rgba(0,255,240,0.1);color:var(--cyan)}
|
|
4291
|
-
.empty-state{text-align:center;color:var(--muted);padding:40px 20px;font-family:var(--font-mono);font-size:0.85rem}
|
|
4292
|
-
|
|
4293
|
-
/* ===== Agent & Message Grid ===== */
|
|
4294
|
-
.bottom-grid{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:24px}
|
|
4295
|
-
@media(max-width:1024px){.bottom-grid{grid-template-columns:1fr}}
|
|
4296
|
-
|
|
4297
|
-
/* Agent cards */
|
|
4298
|
-
.agent-list{display:flex;flex-direction:column;gap:8px;max-height:400px;overflow-y:auto}
|
|
4299
|
-
.agent-card{
|
|
4300
|
-
display:flex;align-items:center;gap:12px;padding:10px 14px;
|
|
4301
|
-
border:1px solid var(--border);border-radius:6px;transition:border-color 0.3s;
|
|
4302
|
-
}
|
|
4303
|
-
.agent-card:hover{border-color:var(--border-bright)}
|
|
4304
|
-
.agent-card.local{border-left:3px solid var(--cyan)}
|
|
4305
|
-
.agent-card.remote{border-left:3px solid var(--green)}
|
|
4306
|
-
.agent-icon{
|
|
4307
|
-
width:36px;height:36px;border-radius:6px;display:flex;align-items:center;
|
|
4308
|
-
justify-content:center;font-family:var(--font-mono);font-size:0.9rem;font-weight:700;
|
|
4309
|
-
flex-shrink:0;
|
|
4310
|
-
}
|
|
4311
|
-
.agent-card.local .agent-icon{background:rgba(0,255,240,0.1);color:var(--cyan)}
|
|
4312
|
-
.agent-card.remote .agent-icon{background:rgba(0,255,136,0.1);color:var(--green)}
|
|
4313
|
-
.agent-info{flex:1;min-width:0}
|
|
4314
|
-
.agent-name{font-weight:600;font-size:0.9rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
4315
|
-
.agent-id{font-family:var(--font-mono);font-size:0.7rem;color:var(--muted);margin-top:2px}
|
|
4316
|
-
.agent-meta{display:flex;gap:6px;flex-shrink:0;align-items:center}
|
|
4317
|
-
.agent-tag{
|
|
4318
|
-
font-family:var(--font-mono);font-size:0.65rem;padding:2px 6px;
|
|
4319
|
-
border-radius:3px;background:rgba(255,255,255,0.05);color:var(--muted);
|
|
4320
|
-
}
|
|
4321
|
-
.agent-tag.unreachable{background:rgba(255,51,102,0.1);color:var(--red)}
|
|
4322
|
-
|
|
4323
|
-
/* Message feed */
|
|
4324
|
-
.msg-feed{display:flex;flex-direction:column;gap:6px;max-height:400px;overflow-y:auto}
|
|
4325
|
-
.msg-item{
|
|
4326
|
-
padding:10px 12px;border:1px solid var(--border);border-radius:6px;
|
|
4327
|
-
border-left:3px solid var(--cyan);font-size:0.8rem;
|
|
4328
|
-
}
|
|
4329
|
-
.msg-item.received{border-left-color:var(--magenta)}
|
|
4330
|
-
.msg-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:4px}
|
|
4331
|
-
.msg-flow{font-family:var(--font-mono);font-size:0.75rem;color:var(--cyan)}
|
|
4332
|
-
.msg-flow .arrow{color:var(--muted);margin:0 4px}
|
|
4333
|
-
.msg-time{font-family:var(--font-mono);font-size:0.65rem;color:var(--muted)}
|
|
4334
|
-
.msg-content{color:var(--text);font-size:0.8rem;line-height:1.4;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
4335
|
-
|
|
4336
|
-
/* Scrollbar */
|
|
4337
|
-
::-webkit-scrollbar{width:4px}
|
|
4338
|
-
::-webkit-scrollbar-track{background:var(--bg)}
|
|
4339
|
-
::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
|
|
4340
|
-
::-webkit-scrollbar-thumb:hover{background:var(--border-bright)}
|
|
4341
|
-
|
|
4342
|
-
/* Footer */
|
|
4343
|
-
.footer{
|
|
4344
|
-
text-align:center;padding:20px 0;border-top:1px solid var(--border);
|
|
4345
|
-
color:var(--muted);font-family:var(--font-mono);font-size:0.75rem;
|
|
4346
|
-
letter-spacing:1px;
|
|
4347
|
-
}
|
|
4348
|
-
|
|
4349
|
-
/* Join Network Button */
|
|
4350
|
-
.btn-join{
|
|
4351
|
-
font-family:var(--font-mono);font-size:0.75rem;letter-spacing:1px;
|
|
4352
|
-
padding:6px 14px;border:1px solid var(--cyan);border-radius:4px;
|
|
4353
|
-
background:rgba(0,255,240,0.08);color:var(--cyan);cursor:pointer;
|
|
4354
|
-
transition:all 0.2s;text-transform:uppercase;margin-left:12px;
|
|
4355
|
-
}
|
|
4356
|
-
.btn-join:hover{background:rgba(0,255,240,0.18);box-shadow:0 0 10px rgba(0,255,240,0.2)}
|
|
4357
|
-
.btn-join:active{transform:scale(0.97)}
|
|
4358
|
-
.btn-logout{border-color:var(--red);color:var(--red);background:rgba(255,51,102,0.08)}
|
|
4359
|
-
.btn-logout:hover{background:rgba(255,51,102,0.18);box-shadow:0 0 10px rgba(255,51,102,0.2)}
|
|
4360
|
-
.btn-install{border-color:var(--yellow);color:var(--yellow);background:rgba(255,170,0,0.08)}
|
|
4361
|
-
.btn-install:hover{background:rgba(255,170,0,0.18);box-shadow:0 0 10px rgba(255,170,0,0.2)}
|
|
4362
|
-
|
|
4363
|
-
/* Tab bar inside modal */
|
|
4364
|
-
.tab-bar{display:flex;gap:0;margin-bottom:18px;border-bottom:1px solid var(--border)}
|
|
4365
|
-
.tab-bar .tab{
|
|
4366
|
-
font-family:var(--font-mono);font-size:0.8rem;padding:8px 16px;cursor:pointer;
|
|
4367
|
-
color:var(--muted);border-bottom:2px solid transparent;transition:all 0.2s;
|
|
4368
|
-
}
|
|
4369
|
-
.tab-bar .tab:hover{color:var(--text)}
|
|
4370
|
-
.tab-bar .tab.active{color:var(--cyan);border-bottom-color:var(--cyan)}
|
|
4371
|
-
.tab-content{display:none}
|
|
4372
|
-
.tab-content.active{display:block}
|
|
4373
|
-
|
|
4374
|
-
/* Modal */
|
|
4375
|
-
.modal-overlay{
|
|
4376
|
-
position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:10000;
|
|
4377
|
-
display:flex;align-items:center;justify-content:center;
|
|
4378
|
-
opacity:0;pointer-events:none;transition:opacity 0.25s;
|
|
4379
|
-
}
|
|
4380
|
-
.modal-overlay.open{opacity:1;pointer-events:all}
|
|
4381
|
-
.modal{
|
|
4382
|
-
background:var(--card);border:1px solid var(--border-bright);border-radius:10px;
|
|
4383
|
-
padding:28px 32px;width:90%;max-width:440px;
|
|
4384
|
-
box-shadow:0 0 40px rgba(0,255,240,0.08),0 8px 32px rgba(0,0,0,0.5);
|
|
4385
|
-
}
|
|
4386
|
-
.modal-title{
|
|
4387
|
-
font-family:var(--font-mono);font-size:1rem;font-weight:700;color:var(--cyan);
|
|
4388
|
-
letter-spacing:2px;margin-bottom:6px;
|
|
4389
|
-
text-shadow:0 0 8px rgba(0,255,240,0.3);
|
|
4390
|
-
}
|
|
4391
|
-
.modal-desc{color:var(--muted);font-size:0.8rem;margin-bottom:20px;line-height:1.5}
|
|
4392
|
-
.modal-input{
|
|
4393
|
-
width:100%;padding:10px 14px;background:var(--bg);border:1px solid var(--border);
|
|
4394
|
-
border-radius:6px;color:var(--text);font-family:var(--font-mono);font-size:0.85rem;
|
|
4395
|
-
outline:none;transition:border-color 0.2s;
|
|
4396
|
-
}
|
|
4397
|
-
.modal-input:focus{border-color:var(--cyan);box-shadow:0 0 8px rgba(0,255,240,0.15)}
|
|
4398
|
-
.modal-input::placeholder{color:var(--muted)}
|
|
4399
|
-
.modal-actions{display:flex;gap:10px;margin-top:18px;justify-content:flex-end}
|
|
4400
|
-
.modal-btn{
|
|
4401
|
-
font-family:var(--font-mono);font-size:0.8rem;padding:8px 18px;
|
|
4402
|
-
border-radius:4px;cursor:pointer;transition:all 0.2s;letter-spacing:0.5px;
|
|
4403
|
-
}
|
|
4404
|
-
.modal-btn-cancel{
|
|
4405
|
-
border:1px solid var(--border);background:transparent;color:var(--muted);
|
|
4406
|
-
}
|
|
4407
|
-
.modal-btn-cancel:hover{border-color:var(--muted);color:var(--text)}
|
|
4408
|
-
.modal-btn-submit{
|
|
4409
|
-
border:1px solid var(--cyan);background:rgba(0,255,240,0.12);color:var(--cyan);
|
|
4410
|
-
}
|
|
4411
|
-
.modal-btn-submit:hover{background:rgba(0,255,240,0.22);box-shadow:0 0 10px rgba(0,255,240,0.2)}
|
|
4412
|
-
.modal-btn-submit:disabled{opacity:0.4;cursor:not-allowed}
|
|
4413
|
-
.modal-status{
|
|
4414
|
-
margin-top:12px;padding:8px 12px;border-radius:4px;font-size:0.78rem;
|
|
4415
|
-
font-family:var(--font-mono);display:none;
|
|
4416
|
-
}
|
|
4417
|
-
.modal-status.success{display:block;background:rgba(0,255,136,0.1);border:1px solid rgba(0,255,136,0.3);color:var(--green)}
|
|
4418
|
-
.modal-status.error{display:block;background:rgba(255,51,102,0.1);border:1px solid rgba(255,51,102,0.3);color:var(--red)}
|
|
4419
|
-
.spinner{
|
|
4420
|
-
display:inline-block;width:12px;height:12px;border:2px solid transparent;
|
|
4421
|
-
border-top-color:var(--cyan);border-radius:50%;animation:spin 0.6s linear infinite;
|
|
4422
|
-
vertical-align:middle;margin-right:6px;
|
|
4423
|
-
}
|
|
4424
|
-
@keyframes spin{to{transform:rotate(360deg)}}
|
|
4425
|
-
|
|
4426
|
-
/* ===== Tailscale Status Panel ===== */
|
|
4427
|
-
.ts-panel{
|
|
4428
|
-
margin-top:12px;padding:16px 20px;background:var(--card);border:1px solid var(--border);
|
|
4429
|
-
border-radius:8px;font-family:var(--font-mono);font-size:0.8rem;
|
|
4430
|
-
}
|
|
4431
|
-
.ts-panel-title{
|
|
4432
|
-
font-weight:700;letter-spacing:2px;text-transform:uppercase;margin-bottom:10px;
|
|
4433
|
-
display:flex;align-items:center;gap:8px;
|
|
4434
|
-
}
|
|
4435
|
-
.ts-panel-title.connected{color:var(--green)}
|
|
4436
|
-
.ts-panel-title.not-installed{color:var(--red)}
|
|
4437
|
-
.ts-panel-title.not-connected{color:var(--yellow)}
|
|
4438
|
-
.ts-info-row{display:flex;gap:8px;margin:4px 0;color:var(--muted)}
|
|
4439
|
-
.ts-info-row .label{min-width:100px;color:var(--muted)}
|
|
4440
|
-
.ts-info-row .value{color:var(--text)}
|
|
4441
|
-
.ts-install-guide{
|
|
4442
|
-
margin-top:12px;padding:12px 16px;background:rgba(0,0,0,0.3);border:1px solid var(--border);
|
|
4443
|
-
border-radius:6px;
|
|
4444
|
-
}
|
|
4445
|
-
.ts-install-guide .cmd{
|
|
4446
|
-
display:flex;align-items:center;justify-content:space-between;
|
|
4447
|
-
padding:6px 10px;background:rgba(255,255,255,0.04);border-radius:4px;
|
|
4448
|
-
margin:6px 0;font-size:0.8rem;cursor:pointer;transition:background 0.2s;
|
|
4449
|
-
}
|
|
4450
|
-
.ts-install-guide .cmd:hover{background:rgba(255,255,255,0.08)}
|
|
4451
|
-
.ts-install-guide .cmd code{color:var(--cyan)}
|
|
4452
|
-
.ts-install-guide .copy-hint{color:var(--muted);font-size:0.7rem}
|
|
4453
|
-
.ts-install-guide a{color:var(--cyan);text-decoration:none}
|
|
4454
|
-
.ts-install-guide a:hover{text-decoration:underline}
|
|
4455
|
-
.ts-setup-hint{
|
|
4456
|
-
margin-top:10px;padding:8px 12px;background:rgba(0,255,240,0.05);border:1px solid rgba(0,255,240,0.15);
|
|
4457
|
-
border-radius:4px;color:var(--cyan);font-size:0.78rem;
|
|
4458
|
-
}
|
|
4459
|
-
.btn-redetect{
|
|
4460
|
-
font-family:var(--font-mono);font-size:0.7rem;padding:4px 12px;
|
|
4461
|
-
border:1px solid var(--border-bright);border-radius:4px;background:transparent;
|
|
4462
|
-
color:var(--muted);cursor:pointer;transition:all 0.2s;margin-top:8px;
|
|
4463
|
-
}
|
|
4464
|
-
.btn-redetect:hover{border-color:var(--cyan);color:var(--cyan)}
|
|
4465
|
-
</style>
|
|
4466
|
-
</head>
|
|
4467
|
-
<body>
|
|
4468
|
-
<div id="app">
|
|
4469
|
-
<header class="header">
|
|
4470
|
-
<div class="header-title">OPEN PARTY<span>// Dashboard</span></div>
|
|
4471
|
-
<div class="header-center">
|
|
4472
|
-
<div class="header-status">
|
|
4473
|
-
<div class="status-dot" id="statusDot"></div>
|
|
4474
|
-
<span id="statusText">CONNECTING</span>
|
|
4475
|
-
</div>
|
|
4476
|
-
<div class="header-meta" id="serverInfo">--</div>
|
|
4477
|
-
</div>
|
|
4478
|
-
<div class="header-right">
|
|
4479
|
-
UPTIME <span class="value" id="uptime">--:--:--</span>
|
|
4480
|
-
<button class="btn-join" id="btnJoinNetwork" title="Join Tailscale Network">Join Network</button>
|
|
4481
|
-
</div>
|
|
4482
|
-
</header>
|
|
4483
|
-
|
|
4484
|
-
<!-- Tailscale status panel (shown when not connected) -->
|
|
4485
|
-
<div id="tsPanel" class="ts-panel" style="display:none"></div>
|
|
4486
|
-
|
|
4487
|
-
<div class="stats-row" id="statsRow">
|
|
4488
|
-
<div class="stat-card cyan">
|
|
4489
|
-
<div class="stat-value" id="localAgentCount">-</div>
|
|
4490
|
-
<div class="stat-label">Local Agents</div>
|
|
4491
|
-
</div>
|
|
4492
|
-
<div class="stat-card green">
|
|
4493
|
-
<div class="stat-value" id="remoteAgentCount">-</div>
|
|
4494
|
-
<div class="stat-label">Remote Agents</div>
|
|
4495
|
-
</div>
|
|
4496
|
-
<div class="stat-card magenta">
|
|
4497
|
-
<div class="stat-value" id="peerCount">-</div>
|
|
4498
|
-
<div class="stat-label">Known Peers</div>
|
|
4499
|
-
</div>
|
|
4500
|
-
<div class="stat-card yellow">
|
|
4501
|
-
<div class="stat-value" id="partyServerCount">-</div>
|
|
4502
|
-
<div class="stat-label">Party Servers</div>
|
|
4503
|
-
</div>
|
|
4504
|
-
</div>
|
|
4505
|
-
|
|
4506
|
-
<div class="main-grid">
|
|
4507
|
-
<div class="section">
|
|
4508
|
-
<div class="section-header"><div class="dot"></div>NETWORK TOPOLOGY</div>
|
|
4509
|
-
<div class="section-body">
|
|
4510
|
-
<div class="topology-container" id="topologyContainer">
|
|
4511
|
-
<svg id="topologySvg" viewBox="0 0 500 300"></svg>
|
|
4512
|
-
</div>
|
|
4513
|
-
</div>
|
|
4514
|
-
</div>
|
|
4515
|
-
<div class="section">
|
|
4516
|
-
<div class="section-header"><div class="dot" style="background:var(--green)"></div>PEER HEALTH</div>
|
|
4517
|
-
<div class="section-body" style="padding:0">
|
|
4518
|
-
<div id="peerTableContainer">
|
|
4519
|
-
<table class="peer-table">
|
|
4520
|
-
<thead><tr><th>IP</th><th>Status</th><th>Fails</th><th>Last Probe</th></tr></thead>
|
|
4521
|
-
<tbody id="peerTableBody"></tbody>
|
|
4522
|
-
</table>
|
|
4523
|
-
</div>
|
|
4524
|
-
</div>
|
|
4525
|
-
</div>
|
|
4526
|
-
</div>
|
|
4527
|
-
|
|
4528
|
-
<div class="bottom-grid">
|
|
4529
|
-
<div class="section">
|
|
4530
|
-
<div class="section-header"><div class="dot" style="background:var(--cyan)"></div>AGENT DIRECTORY</div>
|
|
4531
|
-
<div class="section-body">
|
|
4532
|
-
<div class="agent-list" id="agentList"></div>
|
|
4533
|
-
</div>
|
|
4534
|
-
</div>
|
|
4535
|
-
<div class="section">
|
|
4536
|
-
<div class="section-header"><div class="dot" style="background:var(--magenta)"></div>MESSAGE FLOW</div>
|
|
4537
|
-
<div class="section-body">
|
|
4538
|
-
<div class="msg-feed" id="msgFeed"></div>
|
|
4539
|
-
</div>
|
|
4540
|
-
</div>
|
|
4541
|
-
</div>
|
|
4542
|
-
|
|
4543
|
-
<div class="footer">OPEN PARTY v0.1 // DECENTRALIZED AGENT NETWORK</div>
|
|
4544
|
-
|
|
4545
|
-
<!-- Join Network / Login Modal (two tabs: Interactive + Auth Key) -->
|
|
4546
|
-
<div class="modal-overlay" id="joinModal">
|
|
4547
|
-
<div class="modal">
|
|
4548
|
-
<div class="modal-title">CONNECT TO TAILNET</div>
|
|
4549
|
-
<div class="tab-bar" id="joinTabs">
|
|
4550
|
-
<div class="tab active" data-tab="interactive">Interactive</div>
|
|
4551
|
-
<div class="tab" data-tab="authkey">Auth Key</div>
|
|
4552
|
-
</div>
|
|
4553
|
-
|
|
4554
|
-
<!-- Interactive tab -->
|
|
4555
|
-
<div class="tab-content active" id="tabInteractive">
|
|
4556
|
-
<div class="modal-desc">Click the button below to open a browser authentication page.<br>Your Tailscale connection will be established once you authenticate.</div>
|
|
4557
|
-
<div class="modal-status" id="interactiveStatus"></div>
|
|
4558
|
-
<div class="modal-actions">
|
|
4559
|
-
<button class="modal-btn modal-btn-cancel" id="btnCancelJoin">Cancel</button>
|
|
4560
|
-
<button class="modal-btn modal-btn-submit" id="btnInteractiveLogin">Open Browser Login</button>
|
|
4561
|
-
</div>
|
|
4562
|
-
</div>
|
|
4563
|
-
|
|
4564
|
-
<!-- Auth Key tab -->
|
|
4565
|
-
<div class="tab-content" id="tabAuthkey">
|
|
4566
|
-
<div class="modal-desc">Enter your Tailscale auth key to join the network.<br>You can generate one from the Tailscale admin console.</div>
|
|
4567
|
-
<input type="password" class="modal-input" id="authKeyInput" placeholder="tskey-auth-xxxxx..." autocomplete="off" spellcheck="false" />
|
|
4568
|
-
<div class="modal-status" id="joinStatus"></div>
|
|
4569
|
-
<div class="modal-actions">
|
|
4570
|
-
<button class="modal-btn modal-btn-cancel" id="btnCancelAuthkey">Cancel</button>
|
|
4571
|
-
<button class="modal-btn modal-btn-submit" id="btnSubmitJoin">Connect</button>
|
|
4572
|
-
</div>
|
|
4573
|
-
</div>
|
|
4574
|
-
</div>
|
|
4575
|
-
</div>
|
|
4576
|
-
|
|
4577
|
-
<!-- Logout Confirmation Modal -->
|
|
4578
|
-
<div class="modal-overlay" id="logoutModal">
|
|
4579
|
-
<div class="modal">
|
|
4580
|
-
<div class="modal-title" style="color:var(--red)">LOG OUT OF TAILNET</div>
|
|
4581
|
-
<div class="modal-desc">This will disconnect from Tailscale and remove your credentials.<br>You will need to re-authenticate to reconnect.</div>
|
|
4582
|
-
<div class="modal-status" id="logoutStatus"></div>
|
|
4583
|
-
<div class="modal-actions">
|
|
4584
|
-
<button class="modal-btn modal-btn-cancel" id="btnCancelLogout">Cancel</button>
|
|
4585
|
-
<button class="modal-btn modal-btn-submit" style="border-color:var(--red);color:var(--red);background:rgba(255,51,102,0.12)" id="btnConfirmLogout">Log Out</button>
|
|
4586
|
-
</div>
|
|
4587
|
-
</div>
|
|
4588
|
-
</div>
|
|
4589
|
-
</div>
|
|
4590
|
-
|
|
4591
|
-
<script>
|
|
4592
|
-
(function() {
|
|
4593
|
-
'use strict';
|
|
4594
|
-
|
|
4595
|
-
// ---- Helpers ----
|
|
4596
|
-
const $ = (s) => document.querySelector(s);
|
|
4597
|
-
const $$ = (s) => document.querySelectorAll(s);
|
|
4598
|
-
|
|
4599
|
-
function formatUptime(seconds) {
|
|
4600
|
-
const h = Math.floor(seconds / 3600);
|
|
4601
|
-
const m = Math.floor((seconds % 3600) / 60);
|
|
4602
|
-
const s = seconds % 60;
|
|
4603
|
-
return String(h).padStart(2,'0') + ':' + String(m).padStart(2,'0') + ':' + String(s).padStart(2,'0');
|
|
4604
|
-
}
|
|
4605
|
-
|
|
4606
|
-
function timeAgo(ts) {
|
|
4607
|
-
if (!ts) return '--';
|
|
4608
|
-
const diff = Math.floor(Date.now() / 1000 - ts);
|
|
4609
|
-
if (diff < 0) return 'now';
|
|
4610
|
-
if (diff < 60) return diff + 's ago';
|
|
4611
|
-
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
|
|
4612
|
-
return Math.floor(diff / 3600) + 'h ago';
|
|
4613
|
-
}
|
|
4614
|
-
|
|
4615
|
-
function flashEl(el) {
|
|
4616
|
-
el.classList.remove('flash');
|
|
4617
|
-
void el.offsetWidth;
|
|
4618
|
-
el.classList.add('flash');
|
|
4619
|
-
}
|
|
4620
|
-
|
|
4621
|
-
const statusColors = {
|
|
4622
|
-
PARTY_SERVER: '#00ff88',
|
|
4623
|
-
DEGRADED: '#ffaa00',
|
|
4624
|
-
SUSPECT: '#ff8800',
|
|
4625
|
-
DOWN: '#ff3366',
|
|
4626
|
-
UNKNOWN: '#6a6a8a',
|
|
4627
|
-
NOT_SERVER: '#6a6a8a',
|
|
4628
|
-
MAYBE: '#00fff0',
|
|
4629
|
-
};
|
|
4630
|
-
|
|
4631
|
-
const statusBadgeClass = {
|
|
4632
|
-
PARTY_SERVER: 'badge-party',
|
|
4633
|
-
DEGRADED: 'badge-degraded',
|
|
4634
|
-
SUSPECT: 'badge-suspect',
|
|
4635
|
-
DOWN: 'badge-down',
|
|
4636
|
-
UNKNOWN: 'badge-unknown',
|
|
4637
|
-
NOT_SERVER: 'badge-not_server',
|
|
4638
|
-
MAYBE: 'badge-maybe',
|
|
4639
|
-
};
|
|
4640
|
-
|
|
4641
|
-
// ---- State ----
|
|
4642
|
-
let overview = null;
|
|
4643
|
-
let prevStats = null;
|
|
4644
|
-
|
|
4645
|
-
// ---- Fetch helpers ----
|
|
4646
|
-
async function fetchStats() {
|
|
4647
|
-
try {
|
|
4648
|
-
const r = await fetch('/dashboard/api/stats');
|
|
4649
|
-
return await r.json();
|
|
4650
|
-
} catch { return null; }
|
|
4651
|
-
}
|
|
4652
|
-
|
|
4653
|
-
async function fetchOverview() {
|
|
4654
|
-
try {
|
|
4655
|
-
const r = await fetch('/dashboard/api/overview');
|
|
4656
|
-
return await r.json();
|
|
4657
|
-
} catch { return null; }
|
|
4658
|
-
}
|
|
4659
|
-
|
|
4660
|
-
// ---- Render functions ----
|
|
4661
|
-
function renderHeader(data) {
|
|
4662
|
-
const s = data.server;
|
|
4663
|
-
if (tsState && tsState.state === 'connected') {
|
|
4664
|
-
$('#statusDot').className = 'status-dot';
|
|
4665
|
-
$('#statusText').textContent = 'ONLINE';
|
|
4666
|
-
$('#serverInfo').textContent = s.tailscale_ip + ' // ' + s.hostname;
|
|
4667
|
-
} else if (tsState && tsState.state === 'not_connected') {
|
|
4668
|
-
$('#statusDot').className = 'status-dot not-connected';
|
|
4669
|
-
$('#statusText').textContent = 'NOT CONNECTED';
|
|
4670
|
-
$('#serverInfo').textContent = 'Tailscale installed but not authenticated';
|
|
4671
|
-
} else if (tsState && tsState.state === 'not_installed') {
|
|
4672
|
-
$('#statusDot').className = 'status-dot not-installed';
|
|
4673
|
-
$('#statusText').textContent = 'NO TAILSCALE';
|
|
4674
|
-
$('#serverInfo').textContent = 'Tailscale not installed - local mode';
|
|
4675
|
-
} else {
|
|
4676
|
-
$('#statusDot').className = 'status-dot';
|
|
4677
|
-
$('#statusText').textContent = 'ONLINE';
|
|
4678
|
-
$('#serverInfo').textContent = s.tailscale_ip + ' // ' + s.hostname;
|
|
4679
|
-
}
|
|
4680
|
-
$('#uptime').textContent = formatUptime(s.uptime_seconds);
|
|
4681
|
-
}
|
|
4682
|
-
|
|
4683
|
-
function renderStats(data) {
|
|
4684
|
-
const prev = {
|
|
4685
|
-
local: parseInt($('#localAgentCount').textContent) || 0,
|
|
4686
|
-
remote: parseInt($('#remoteAgentCount').textContent) || 0,
|
|
4687
|
-
peer: parseInt($('#peerCount').textContent) || 0,
|
|
4688
|
-
party: parseInt($('#partyServerCount').textContent) || 0,
|
|
4689
|
-
};
|
|
4690
|
-
const vals = {
|
|
4691
|
-
local: data.agents.local_count,
|
|
4692
|
-
remote: data.agents.remote_count,
|
|
4693
|
-
peer: data.peers.total,
|
|
4694
|
-
party: data.peers.party_servers,
|
|
4695
|
-
};
|
|
4696
|
-
if (vals.local !== prev.local) { $('#localAgentCount').textContent = vals.local; flashEl($('#localAgentCount')); }
|
|
4697
|
-
if (vals.remote !== prev.remote) { $('#remoteAgentCount').textContent = vals.remote; flashEl($('#remoteAgentCount')); }
|
|
4698
|
-
if (vals.peer !== prev.peer) { $('#peerCount').textContent = vals.peer; flashEl($('#peerCount')); }
|
|
4699
|
-
if (vals.party !== prev.party) { $('#partyServerCount').textContent = vals.party; flashEl($('#partyServerCount')); }
|
|
4700
|
-
}
|
|
4701
|
-
|
|
4702
|
-
function renderTopology(data) {
|
|
4703
|
-
const svg = $('#topologySvg');
|
|
4704
|
-
const peers = data.peers.details || [];
|
|
4705
|
-
const cx = 250, cy = 150, radius = 100;
|
|
4706
|
-
|
|
4707
|
-
let html = '';
|
|
4708
|
-
|
|
4709
|
-
// Center node
|
|
4710
|
-
html += '<circle cx="' + cx + '" cy="' + cy + '" r="24" fill="rgba(0,255,240,0.15)" stroke="#00fff0" stroke-width="2">';
|
|
4711
|
-
html += '<animate attributeName="r" values="24;26;24" dur="3s" repeatCount="indefinite"/>';
|
|
4712
|
-
html += '</circle>';
|
|
4713
|
-
html += '<text x="' + cx + '" y="' + cy + '" text-anchor="middle" dominant-baseline="central" fill="#00fff0" font-family="var(--font-mono)" font-size="10" font-weight="700">SELF</text>';
|
|
4714
|
-
html += '<text x="' + cx + '" y="' + (cy + 38) + '" text-anchor="middle" class="topo-label">' + data.server.tailscale_ip + '</text>';
|
|
4715
|
-
|
|
4716
|
-
if (peers.length === 0) {
|
|
4717
|
-
html += '<text x="' + cx + '" y="' + (cy + 60) + '" text-anchor="middle" fill="#6a6a8a" font-family="var(--font-mono)" font-size="11">No peers discovered</text>';
|
|
4718
|
-
}
|
|
4719
|
-
|
|
4720
|
-
peers.forEach(function(p, i) {
|
|
4721
|
-
const angle = (2 * Math.PI * i / Math.max(peers.length, 1)) - Math.PI / 2;
|
|
4722
|
-
const px = cx + radius * Math.cos(angle);
|
|
4723
|
-
const py = cy + radius * Math.sin(angle);
|
|
4724
|
-
const color = statusColors[p.status] || '#6a6a8a';
|
|
4725
|
-
const opacity = (p.status === 'PARTY_SERVER' || p.status === 'DEGRADED' || p.status === 'SUSPECT') ? 1 : 0.4;
|
|
4726
|
-
|
|
4727
|
-
// Connection line
|
|
4728
|
-
html += '<line x1="' + cx + '" y1="' + cy + '" x2="' + px + '" y2="' + py + '" stroke="' + color + '" stroke-width="1" opacity="' + (opacity * 0.3) + '"/>';
|
|
4729
|
-
|
|
4730
|
-
// Peer node
|
|
4731
|
-
html += '<g class="topo-node"><circle cx="' + px + '" cy="' + py + '" r="16" fill="rgba(255,255,255,0.03)" stroke="' + color + '" stroke-width="1.5" opacity="' + opacity + '"/>';
|
|
4732
|
-
html += '<text x="' + px + '" y="' + py + '" text-anchor="middle" dominant-baseline="central" fill="' + color + '" font-family="var(--font-mono)" font-size="8" opacity="' + opacity + '">' + p.ip.split('.').slice(-1)[0] + '</text></g>';
|
|
4733
|
-
|
|
4734
|
-
// IP label
|
|
4735
|
-
html += '<text x="' + px + '" y="' + (py + 26) + '" text-anchor="middle" class="topo-label">' + p.ip + '</text>';
|
|
4736
|
-
});
|
|
4737
|
-
|
|
4738
|
-
svg.innerHTML = html;
|
|
4739
|
-
}
|
|
4740
|
-
|
|
4741
|
-
function renderPeerTable(data) {
|
|
4742
|
-
const tbody = $('#peerTableBody');
|
|
4743
|
-
const peers = data.peers.details || [];
|
|
4744
|
-
|
|
4745
|
-
if (peers.length === 0) {
|
|
4746
|
-
tbody.innerHTML = '<tr><td colspan="4" class="empty-state">No peers discovered</td></tr>';
|
|
4747
|
-
return;
|
|
4748
|
-
}
|
|
4749
|
-
|
|
4750
|
-
// Sort: PARTY_SERVER first, then by severity
|
|
4751
|
-
const order = { PARTY_SERVER: 0, DEGRADED: 1, SUSPECT: 2, MAYBE: 3, UNKNOWN: 4, NOT_SERVER: 5, DOWN: 6 };
|
|
4752
|
-
const sorted = [...peers].sort(function(a, b) { return (order[a.status] || 99) - (order[b.status] || 99); });
|
|
4753
|
-
|
|
4754
|
-
tbody.innerHTML = sorted.map(function(p) {
|
|
4755
|
-
const badge = statusBadgeClass[p.status] || 'badge-unknown';
|
|
4756
|
-
const label = p.status === 'PARTY_SERVER' ? 'SERVER' : p.status === 'NOT_SERVER' ? 'NOT_SVR' : p.status;
|
|
4757
|
-
return '<tr>'
|
|
4758
|
-
+ '<td>' + p.ip + '</td>'
|
|
4759
|
-
+ '<td><span class="badge ' + badge + '">' + label + '</span></td>'
|
|
4760
|
-
+ '<td>' + p.consecutiveFailures + '</td>'
|
|
4761
|
-
+ '<td>' + timeAgo(p.lastProbeAt) + '</td>'
|
|
4762
|
-
+ '</tr>';
|
|
4763
|
-
}).join('');
|
|
4764
|
-
}
|
|
4765
|
-
|
|
4766
|
-
function renderAgents(data) {
|
|
4767
|
-
const container = $('#agentList');
|
|
4768
|
-
const local = data.agents.local_agents || [];
|
|
4769
|
-
const remote = data.agents.remote_agents || [];
|
|
4770
|
-
const all = [
|
|
4771
|
-
...local.map(function(a) { return { ...a, type: 'local' }; }),
|
|
4772
|
-
...remote.map(function(a) { return { ...a, type: 'remote' }; }),
|
|
4773
|
-
];
|
|
4774
|
-
|
|
4775
|
-
if (all.length === 0) {
|
|
4776
|
-
container.innerHTML = '<div class="empty-state">No agents registered</div>';
|
|
4777
|
-
return;
|
|
4778
|
-
}
|
|
4779
|
-
|
|
4780
|
-
container.innerHTML = all.map(function(a) {
|
|
4781
|
-
const initials = (a.display_name || a.agent_id).substring(0, 2).toUpperCase();
|
|
4782
|
-
const tags = [];
|
|
4783
|
-
if (a.type === 'remote') {
|
|
4784
|
-
tags.push('<span class="agent-tag">' + a.source_peer_ip + '</span>');
|
|
4785
|
-
if (!a.reachable) tags.push('<span class="agent-tag unreachable">offline</span>');
|
|
4786
|
-
} else {
|
|
4787
|
-
tags.push('<span class="agent-tag">local</span>');
|
|
4788
|
-
}
|
|
4789
|
-
return '<div class="agent-card ' + a.type + '">'
|
|
4790
|
-
+ '<div class="agent-icon">' + initials + '</div>'
|
|
4791
|
-
+ '<div class="agent-info">'
|
|
4792
|
-
+ '<div class="agent-name">' + (a.display_name || a.agent_id) + '</div>'
|
|
4793
|
-
+ '<div class="agent-id">' + a.agent_id + '</div>'
|
|
4794
|
-
+ '</div>'
|
|
4795
|
-
+ '<div class="agent-meta">' + tags.join('') + '</div>'
|
|
4796
|
-
+ '</div>';
|
|
4797
|
-
}).join('');
|
|
4798
|
-
}
|
|
4799
|
-
|
|
4800
|
-
// Build agent_id \u2192 display_name lookup from local + remote agents
|
|
4801
|
-
function buildNameMap(data) {
|
|
4802
|
-
const map = {};
|
|
4803
|
-
const all = [...(data.agents.local_agents || []), ...(data.agents.remote_agents || [])];
|
|
4804
|
-
for (const a of all) { map[a.agent_id] = a.display_name || a.agent_id; }
|
|
4805
|
-
return map;
|
|
4806
|
-
}
|
|
4807
|
-
|
|
4808
|
-
function resolveName(map, id) { return map[id] || id; }
|
|
4809
|
-
|
|
4810
|
-
function renderMessages(data) {
|
|
4811
|
-
const container = $('#msgFeed');
|
|
4812
|
-
const msgs = data.messages.recent || [];
|
|
4813
|
-
|
|
4814
|
-
if (msgs.length === 0) {
|
|
4815
|
-
container.innerHTML = '<div class="empty-state">No recent messages</div>';
|
|
4816
|
-
return;
|
|
4817
|
-
}
|
|
4818
|
-
|
|
4819
|
-
const names = buildNameMap(data);
|
|
4820
|
-
|
|
4821
|
-
container.innerHTML = msgs.map(function(m) {
|
|
4822
|
-
const dir = m.direction === 'received' ? 'received' : '';
|
|
4823
|
-
const arrow = m.direction === 'received' ? '←' : '→';
|
|
4824
|
-
const flow = m.direction === 'received'
|
|
4825
|
-
? resolveName(names, m.sender_id) + ' <span class="arrow">' + arrow + '</span> ' + resolveName(names, m.agent_id)
|
|
4826
|
-
: resolveName(names, m.agent_id) + ' <span class="arrow">' + arrow + '</span> ' + resolveName(names, m.recipient_id) || 'broadcast';
|
|
4827
|
-
return '<div class="msg-item ' + dir + '">'
|
|
4828
|
-
+ '<div class="msg-top">'
|
|
4829
|
-
+ '<div class="msg-flow">' + flow + '</div>'
|
|
4830
|
-
+ '<div class="msg-time">' + timeAgo(m.timestamp) + '</div>'
|
|
4831
|
-
+ '</div>'
|
|
4832
|
-
+ '<div class="msg-content">' + (m.summary || m.content) + '</div>'
|
|
4833
|
-
+ '</div>';
|
|
4834
|
-
}).join('');
|
|
4835
|
-
}
|
|
4836
|
-
|
|
4837
|
-
function renderAll(data) {
|
|
4838
|
-
renderHeader(data);
|
|
4839
|
-
renderStats(data);
|
|
4840
|
-
renderTopology(data);
|
|
4841
|
-
renderPeerTable(data);
|
|
4842
|
-
renderAgents(data);
|
|
4843
|
-
renderMessages(data);
|
|
4844
|
-
}
|
|
4845
|
-
|
|
4846
|
-
// ---- Polling ----
|
|
4847
|
-
let fullTimer = null;
|
|
4848
|
-
let fastTimer = null;
|
|
4849
|
-
|
|
4850
|
-
async function fullRefresh() {
|
|
4851
|
-
const data = await fetchOverview();
|
|
4852
|
-
if (!data) {
|
|
4853
|
-
$('#statusDot').className = 'status-dot offline';
|
|
4854
|
-
$('#statusText').textContent = 'OFFLINE';
|
|
4855
|
-
return;
|
|
4856
|
-
}
|
|
4857
|
-
overview = data;
|
|
4858
|
-
renderAll(data);
|
|
4859
|
-
}
|
|
4860
|
-
|
|
4861
|
-
async function fastRefresh() {
|
|
4862
|
-
const stats = await fetchStats();
|
|
4863
|
-
if (!stats) return;
|
|
4864
|
-
const changed = JSON.stringify(stats) !== JSON.stringify(prevStats);
|
|
4865
|
-
prevStats = stats;
|
|
4866
|
-
if (changed) {
|
|
4867
|
-
fullRefresh();
|
|
4868
|
-
}
|
|
4869
|
-
}
|
|
4870
|
-
|
|
4871
|
-
// ---- Init ----
|
|
4872
|
-
fullRefresh();
|
|
4873
|
-
fastTimer = setInterval(fastRefresh, 3000);
|
|
4874
|
-
fullTimer = setInterval(fullRefresh, 5000);
|
|
4875
|
-
|
|
4876
|
-
// Clipboard click delegation for .cmd elements
|
|
4877
|
-
document.addEventListener('click', function(e) {
|
|
4878
|
-
var cmd = e.target.closest('.cmd');
|
|
4879
|
-
if (!cmd) return;
|
|
4880
|
-
var text = cmd.getAttribute('data-clipboard');
|
|
4881
|
-
if (text) {
|
|
4882
|
-
navigator.clipboard.writeText(text).then(function() {
|
|
4883
|
-
var hint = cmd.querySelector('.copy-hint');
|
|
4884
|
-
if (hint) hint.textContent = 'Copied!';
|
|
4885
|
-
});
|
|
4886
|
-
}
|
|
4887
|
-
});
|
|
4888
|
-
|
|
4889
|
-
// Update uptime display every second
|
|
4890
|
-
setInterval(function() {
|
|
4891
|
-
if (overview && overview.server) {
|
|
4892
|
-
overview.server.uptime_seconds++;
|
|
4893
|
-
$('#uptime').textContent = formatUptime(overview.server.uptime_seconds);
|
|
4894
|
-
}
|
|
4895
|
-
}, 1000);
|
|
4896
|
-
|
|
4897
|
-
// ---- Join Modal Tabs ----
|
|
4898
|
-
const joinTabs = $$('#joinTabs .tab');
|
|
4899
|
-
joinTabs.forEach(function(tab) {
|
|
4900
|
-
tab.addEventListener('click', function() {
|
|
4901
|
-
joinTabs.forEach(function(t) { t.classList.remove('active'); });
|
|
4902
|
-
tab.classList.add('active');
|
|
4903
|
-
// Toggle tab contents
|
|
4904
|
-
const target = tab.getAttribute('data-tab');
|
|
4905
|
-
$$('.tab-content').forEach(function(tc) { tc.classList.remove('active'); });
|
|
4906
|
-
if (target === 'interactive') {
|
|
4907
|
-
$('#tabInteractive').classList.add('active');
|
|
4908
|
-
} else {
|
|
4909
|
-
$('#tabAuthkey').classList.add('active');
|
|
4910
|
-
}
|
|
4911
|
-
});
|
|
4912
|
-
});
|
|
4913
|
-
|
|
4914
|
-
// ---- Join Network Modal (open/close) ----
|
|
4915
|
-
const joinModal = $('#joinModal');
|
|
4916
|
-
const btnJoin = $('#btnJoinNetwork');
|
|
4917
|
-
const btnCancel = $('#btnCancelJoin');
|
|
4918
|
-
const btnCancelAuthkey = $('#btnCancelAuthkey');
|
|
4919
|
-
const btnSubmit = $('#btnSubmitJoin');
|
|
4920
|
-
const authKeyInput = $('#authKeyInput');
|
|
4921
|
-
const joinStatus = $('#joinStatus');
|
|
4922
|
-
|
|
4923
|
-
function openJoinModal() {
|
|
4924
|
-
// Reset both tabs
|
|
4925
|
-
joinStatus.className = 'modal-status';
|
|
4926
|
-
joinStatus.textContent = '';
|
|
4927
|
-
authKeyInput.value = '';
|
|
4928
|
-
$('#interactiveStatus').className = 'modal-status';
|
|
4929
|
-
$('#interactiveStatus').textContent = '';
|
|
4930
|
-
// Default to Interactive tab
|
|
4931
|
-
$$('#joinTabs .tab').forEach(function(t) { t.classList.remove('active'); });
|
|
4932
|
-
$$('#joinTabs .tab')[0].classList.add('active');
|
|
4933
|
-
$$('.tab-content').forEach(function(tc) { tc.classList.remove('active'); });
|
|
4934
|
-
$('#tabInteractive').classList.add('active');
|
|
4935
|
-
joinModal.classList.add('open');
|
|
4936
|
-
}
|
|
4937
|
-
|
|
4938
|
-
function closeJoinModal() {
|
|
4939
|
-
joinModal.classList.remove('open');
|
|
4940
|
-
}
|
|
4941
|
-
|
|
4942
|
-
btnJoin.addEventListener('click', function() {
|
|
4943
|
-
// Decide action based on Tailscale state
|
|
4944
|
-
if (tsState && tsState.state === 'connected') {
|
|
4945
|
-
openLogoutModal();
|
|
4946
|
-
} else if (tsState && tsState.state === 'not_installed') {
|
|
4947
|
-
doInstallTailscale();
|
|
4948
|
-
} else {
|
|
4949
|
-
openJoinModal();
|
|
4950
|
-
}
|
|
4951
|
-
});
|
|
4952
|
-
btnCancel.addEventListener('click', closeJoinModal);
|
|
4953
|
-
btnCancelAuthkey.addEventListener('click', closeJoinModal);
|
|
4954
|
-
joinModal.addEventListener('click', function(e) {
|
|
4955
|
-
if (e.target === joinModal) closeJoinModal();
|
|
4956
|
-
});
|
|
4957
|
-
authKeyInput.addEventListener('keydown', function(e) {
|
|
4958
|
-
if (e.key === 'Enter') btnSubmit.click();
|
|
4959
|
-
if (e.key === 'Escape') closeJoinModal();
|
|
4960
|
-
});
|
|
4961
|
-
|
|
4962
|
-
// ---- Auth Key submit ----
|
|
4963
|
-
btnSubmit.addEventListener('click', async function() {
|
|
4964
|
-
const key = authKeyInput.value.trim();
|
|
4965
|
-
if (!key) {
|
|
4966
|
-
authKeyInput.focus();
|
|
4967
|
-
return;
|
|
4968
|
-
}
|
|
4969
|
-
btnSubmit.disabled = true;
|
|
4970
|
-
btnSubmit.innerHTML = '<span class="spinner"></span>Connecting...';
|
|
4971
|
-
joinStatus.className = 'modal-status';
|
|
4972
|
-
joinStatus.textContent = '';
|
|
4973
|
-
|
|
4974
|
-
try {
|
|
4975
|
-
const r = await fetch('/dashboard/api/join-network', {
|
|
4976
|
-
method: 'POST',
|
|
4977
|
-
headers: { 'Content-Type': 'application/json' },
|
|
4978
|
-
body: JSON.stringify({ auth_key: key }),
|
|
4979
|
-
});
|
|
4980
|
-
const data = await r.json();
|
|
4981
|
-
if (data.success) {
|
|
4982
|
-
joinStatus.className = 'modal-status success';
|
|
4983
|
-
joinStatus.textContent = 'Successfully joined network!';
|
|
4984
|
-
btnJoin.textContent = 'Logout';
|
|
4985
|
-
btnJoin.className = 'btn-join btn-logout';
|
|
4986
|
-
setTimeout(function() { closeJoinModal(); checkTailscaleStatus(); fullRefresh(); }, 1500);
|
|
4987
|
-
} else {
|
|
4988
|
-
joinStatus.className = 'modal-status error';
|
|
4989
|
-
joinStatus.textContent = data.output || 'Failed to join network';
|
|
4990
|
-
}
|
|
4991
|
-
} catch (e) {
|
|
4992
|
-
joinStatus.className = 'modal-status error';
|
|
4993
|
-
joinStatus.textContent = 'Network error: ' + (e.message || 'unknown');
|
|
4994
|
-
}
|
|
4995
|
-
btnSubmit.disabled = false;
|
|
4996
|
-
btnSubmit.textContent = 'Connect';
|
|
4997
|
-
});
|
|
4998
|
-
|
|
4999
|
-
// ---- Interactive Login ----
|
|
5000
|
-
const btnInteractiveLogin = $('#btnInteractiveLogin');
|
|
5001
|
-
btnInteractiveLogin.addEventListener('click', async function() {
|
|
5002
|
-
const statusEl = $('#interactiveStatus');
|
|
5003
|
-
statusEl.className = 'modal-status';
|
|
5004
|
-
statusEl.textContent = '';
|
|
5005
|
-
btnInteractiveLogin.disabled = true;
|
|
5006
|
-
btnInteractiveLogin.innerHTML = '<span class="spinner"></span>Opening browser...';
|
|
5007
|
-
|
|
5008
|
-
try {
|
|
5009
|
-
const r = await fetch('/dashboard/api/tailscale-login', { method: 'POST' });
|
|
5010
|
-
const data = await r.json();
|
|
5011
|
-
|
|
5012
|
-
if (data.success && data.url) {
|
|
5013
|
-
// Open the auth URL in a new tab
|
|
5014
|
-
window.open(data.url, '_blank');
|
|
5015
|
-
statusEl.className = 'modal-status success';
|
|
5016
|
-
statusEl.textContent = 'Authentication page opened in your browser. Waiting for connection...';
|
|
5017
|
-
|
|
5018
|
-
// Poll for connection
|
|
5019
|
-
var pollCount = 0;
|
|
5020
|
-
var pollInterval = setInterval(async function() {
|
|
5021
|
-
pollCount++;
|
|
5022
|
-
if (pollCount > 40) { // 2 minutes timeout
|
|
5023
|
-
clearInterval(pollInterval);
|
|
5024
|
-
statusEl.className = 'modal-status error';
|
|
5025
|
-
statusEl.textContent = 'Timed out waiting for authentication. Please try again.';
|
|
5026
|
-
btnInteractiveLogin.disabled = false;
|
|
5027
|
-
btnInteractiveLogin.textContent = 'Open Browser Login';
|
|
5028
|
-
return;
|
|
5029
|
-
}
|
|
5030
|
-
try {
|
|
5031
|
-
var sr = await fetch('/dashboard/api/tailscale-status');
|
|
5032
|
-
var sd = await sr.json();
|
|
5033
|
-
if (sd.state === 'connected') {
|
|
5034
|
-
clearInterval(pollInterval);
|
|
5035
|
-
btnJoin.textContent = 'Logout';
|
|
5036
|
-
btnJoin.className = 'btn-join btn-logout';
|
|
5037
|
-
closeJoinModal();
|
|
5038
|
-
checkTailscaleStatus();
|
|
5039
|
-
fullRefresh();
|
|
5040
|
-
return;
|
|
5041
|
-
}
|
|
5042
|
-
} catch { /* poll error, continue */ }
|
|
5043
|
-
}, 3000);
|
|
5044
|
-
} else {
|
|
5045
|
-
statusEl.className = 'modal-status error';
|
|
5046
|
-
statusEl.textContent = data.output || 'Failed to start interactive login';
|
|
5047
|
-
btnInteractiveLogin.disabled = false;
|
|
5048
|
-
btnInteractiveLogin.textContent = 'Open Browser Login';
|
|
5049
|
-
}
|
|
5050
|
-
} catch (e) {
|
|
5051
|
-
statusEl.className = 'modal-status error';
|
|
5052
|
-
statusEl.textContent = 'Network error: ' + (e.message || 'unknown');
|
|
5053
|
-
btnInteractiveLogin.disabled = false;
|
|
5054
|
-
btnInteractiveLogin.textContent = 'Open Browser Login';
|
|
5055
|
-
}
|
|
5056
|
-
});
|
|
5057
|
-
|
|
5058
|
-
// ---- Logout Modal ----
|
|
5059
|
-
const logoutModal = $('#logoutModal');
|
|
5060
|
-
const btnConfirmLogout = $('#btnConfirmLogout');
|
|
5061
|
-
const btnCancelLogout = $('#btnCancelLogout');
|
|
5062
|
-
const logoutStatus = $('#logoutStatus');
|
|
5063
|
-
|
|
5064
|
-
function openLogoutModal() {
|
|
5065
|
-
logoutStatus.className = 'modal-status';
|
|
5066
|
-
logoutStatus.textContent = '';
|
|
5067
|
-
logoutModal.classList.add('open');
|
|
5068
|
-
}
|
|
5069
|
-
|
|
5070
|
-
btnCancelLogout.addEventListener('click', function() { logoutModal.classList.remove('open'); });
|
|
5071
|
-
logoutModal.addEventListener('click', function(e) { if (e.target === logoutModal) logoutModal.classList.remove('open'); });
|
|
5072
|
-
|
|
5073
|
-
btnConfirmLogout.addEventListener('click', async function() {
|
|
5074
|
-
btnConfirmLogout.disabled = true;
|
|
5075
|
-
btnConfirmLogout.innerHTML = '<span class="spinner"></span>Logging out...';
|
|
5076
|
-
logoutStatus.className = 'modal-status';
|
|
5077
|
-
logoutStatus.textContent = '';
|
|
5078
|
-
|
|
5079
|
-
try {
|
|
5080
|
-
const r = await fetch('/dashboard/api/logout', { method: 'POST' });
|
|
5081
|
-
const data = await r.json();
|
|
5082
|
-
logoutModal.classList.remove('open');
|
|
5083
|
-
if (data.success) {
|
|
5084
|
-
checkTailscaleStatus();
|
|
5085
|
-
fullRefresh();
|
|
5086
|
-
} else {
|
|
5087
|
-
alert('Logout failed: ' + (data.output || 'unknown error'));
|
|
5088
|
-
}
|
|
5089
|
-
} catch (e) {
|
|
5090
|
-
logoutModal.classList.remove('open');
|
|
5091
|
-
alert('Network error: ' + (e.message || 'unknown'));
|
|
5092
|
-
}
|
|
5093
|
-
btnConfirmLogout.disabled = false;
|
|
5094
|
-
btnConfirmLogout.textContent = 'Log Out';
|
|
5095
|
-
});
|
|
5096
|
-
|
|
5097
|
-
// ---- Install Tailscale ----
|
|
5098
|
-
async function doInstallTailscale() {
|
|
5099
|
-
if (!confirm('Install Tailscale on this machine?')) return;
|
|
5100
|
-
|
|
5101
|
-
btnJoin.disabled = true;
|
|
5102
|
-
btnJoin.innerHTML = '<span class="spinner"></span>Installing...';
|
|
5103
|
-
|
|
5104
|
-
try {
|
|
5105
|
-
const r = await fetch('/dashboard/api/install-tailscale', { method: 'POST' });
|
|
5106
|
-
const data = await r.json();
|
|
5107
|
-
if (data.success) {
|
|
5108
|
-
btnJoin.textContent = 'Installed';
|
|
5109
|
-
btnJoin.disabled = false;
|
|
5110
|
-
checkTailscaleStatus();
|
|
5111
|
-
fullRefresh();
|
|
5112
|
-
} else {
|
|
5113
|
-
alert('Installation failed: ' + (data.output || 'unknown error'));
|
|
5114
|
-
btnJoin.textContent = 'Install Tailscale';
|
|
5115
|
-
btnJoin.className = 'btn-join btn-install';
|
|
5116
|
-
btnJoin.disabled = false;
|
|
5117
|
-
}
|
|
5118
|
-
} catch (e) {
|
|
5119
|
-
alert('Network error: ' + (e.message || 'unknown'));
|
|
5120
|
-
btnJoin.textContent = 'Install Tailscale';
|
|
5121
|
-
btnJoin.className = 'btn-join btn-install';
|
|
5122
|
-
btnJoin.disabled = false;
|
|
5123
|
-
}
|
|
5124
|
-
}
|
|
5125
|
-
|
|
5126
|
-
// Check initial Tailscale status (tri-state)
|
|
5127
|
-
let tsState = null;
|
|
5128
|
-
let tsInstallInfo = null;
|
|
5129
|
-
|
|
5130
|
-
async function checkTailscaleStatus() {
|
|
5131
|
-
try {
|
|
5132
|
-
const r = await fetch('/dashboard/api/tailscale-status');
|
|
5133
|
-
tsState = await r.json();
|
|
5134
|
-
} catch { tsState = { state: 'not_installed', platform: 'unknown' }; }
|
|
5135
|
-
|
|
5136
|
-
const dot = $('#statusDot');
|
|
5137
|
-
const text = $('#statusText');
|
|
5138
|
-
const btnJoin = $('#btnJoinNetwork');
|
|
5139
|
-
const panel = $('#tsPanel');
|
|
5140
|
-
|
|
5141
|
-
if (tsState.state === 'connected') {
|
|
5142
|
-
dot.className = 'status-dot';
|
|
5143
|
-
text.textContent = 'ONLINE';
|
|
5144
|
-
btnJoin.textContent = 'Logout';
|
|
5145
|
-
btnJoin.className = 'btn-join btn-logout';
|
|
5146
|
-
btnJoin.style.display = '';
|
|
5147
|
-
panel.style.display = 'none';
|
|
5148
|
-
} else if (tsState.state === 'not_installed') {
|
|
5149
|
-
dot.className = 'status-dot not-installed';
|
|
5150
|
-
text.textContent = 'NOT INSTALLED';
|
|
5151
|
-
btnJoin.textContent = 'Install Tailscale';
|
|
5152
|
-
btnJoin.className = 'btn-join btn-install';
|
|
5153
|
-
btnJoin.style.display = '';
|
|
5154
|
-
await renderNotInstalledPanel();
|
|
5155
|
-
} else {
|
|
5156
|
-
dot.className = 'status-dot not-connected';
|
|
5157
|
-
text.textContent = 'NOT CONNECTED';
|
|
5158
|
-
btnJoin.textContent = 'Join Network';
|
|
5159
|
-
btnJoin.className = 'btn-join';
|
|
5160
|
-
btnJoin.style.display = '';
|
|
5161
|
-
await renderNotConnectedPanel();
|
|
5162
|
-
}
|
|
5163
|
-
}
|
|
5164
|
-
|
|
5165
|
-
async function fetchInstallInfo() {
|
|
5166
|
-
if (tsInstallInfo) return tsInstallInfo;
|
|
5167
|
-
try {
|
|
5168
|
-
const r = await fetch('/dashboard/api/tailscale-install-info');
|
|
5169
|
-
tsInstallInfo = await r.json();
|
|
5170
|
-
} catch { tsInstallInfo = null; }
|
|
5171
|
-
return tsInstallInfo;
|
|
5172
|
-
}
|
|
5173
|
-
|
|
5174
|
-
async function renderNotInstalledPanel() {
|
|
5175
|
-
const info = await fetchInstallInfo();
|
|
5176
|
-
const panel = $('#tsPanel');
|
|
5177
|
-
let html = '<div class="ts-panel-title not-installed">Tailscale Not Installed</div>';
|
|
5178
|
-
html += '<div class="ts-info-row"><span class="label">Status:</span><span class="value" style="color:var(--red)">Tailscale is not detected on this system</span></div>';
|
|
5179
|
-
|
|
5180
|
-
if (info && info.commands && info.commands.length > 0) {
|
|
5181
|
-
html += '<div class="ts-install-guide">';
|
|
5182
|
-
html += '<div style="color:var(--muted);margin-bottom:6px">Install for ' + info.os + ':</div>';
|
|
5183
|
-
info.commands.forEach(function(cmd) {
|
|
5184
|
-
const display = info.needs_sudo ? 'sudo ' + cmd : cmd;
|
|
5185
|
-
html += '<div class="cmd" data-clipboard="' + display.replace(/"/g, '"') + '">';
|
|
5186
|
-
html += '<code>' + display + '</code><span class="copy-hint">Click to copy</span></div>';
|
|
5187
|
-
});
|
|
5188
|
-
if (info.download_url) {
|
|
5189
|
-
html += '<div style="margin-top:8px">Download: <a href="' + info.download_url + '" target="_blank">' + info.download_url + '</a></div>';
|
|
5190
|
-
}
|
|
5191
|
-
html += '</div>';
|
|
5192
|
-
}
|
|
5193
|
-
|
|
5194
|
-
html += '<div class="ts-setup-hint">Or click the <strong>Install Tailscale</strong> button above, or run <code style="color:var(--cyan)">npx open-party setup</code></div>';
|
|
5195
|
-
html += '<button class="btn-redetect" onclick="window.__redetectTailscale()">Re-detect</button>';
|
|
5196
|
-
panel.innerHTML = html;
|
|
5197
|
-
panel.style.display = 'block';
|
|
5198
|
-
}
|
|
5199
|
-
|
|
5200
|
-
async function renderNotConnectedPanel() {
|
|
5201
|
-
const panel = $('#tsPanel');
|
|
5202
|
-
|
|
5203
|
-
let html = '<div class="ts-panel-title not-connected">Tailscale Not Connected</div>';
|
|
5204
|
-
html += '<div class="ts-info-row"><span class="label">Status:</span><span class="value" style="color:var(--yellow)">Installed but not authenticated</span></div>';
|
|
5205
|
-
html += '<div class="ts-setup-hint">Use the <strong>Join Network</strong> button above to log in</div>';
|
|
5206
|
-
html += '<button class="btn-redetect" onclick="window.__redetectTailscale()">Re-detect</button>';
|
|
5207
|
-
panel.innerHTML = html;
|
|
5208
|
-
panel.style.display = 'block';
|
|
5209
|
-
}
|
|
5210
|
-
|
|
5211
|
-
window.__redetectTailscale = async function() {
|
|
5212
|
-
const panel = $('#tsPanel');
|
|
5213
|
-
panel.innerHTML = '<div style="color:var(--muted);padding:12px"><span class="spinner"></span> Re-detecting Tailscale...</div>';
|
|
5214
|
-
try {
|
|
5215
|
-
await fetch('/dashboard/api/tailscale-detect', { method: 'POST' });
|
|
5216
|
-
} catch { /* ignore */ }
|
|
5217
|
-
await checkTailscaleStatus();
|
|
5218
|
-
fullRefresh();
|
|
5219
|
-
};
|
|
5220
|
-
|
|
5221
|
-
checkTailscaleStatus();
|
|
5222
|
-
|
|
5223
|
-
})();
|
|
5224
|
-
</script>
|
|
5225
|
-
</body>
|
|
5226
|
-
</html>`;
|
|
5227
|
-
|
|
5228
|
-
// src/server/routes/dashboard.ts
|
|
5229
|
-
init_tailscale();
|
|
5230
|
-
init_state();
|
|
5231
|
-
init_logger();
|
|
5232
|
-
var dashboardRoutes = new Hono2();
|
|
5233
|
-
dashboardRoutes.get("/", (c) => {
|
|
5234
|
-
return c.html(DASHBOARD_HTML);
|
|
5235
|
-
});
|
|
5236
|
-
dashboardRoutes.get("/api/stats", async (c) => {
|
|
5237
|
-
const localAgents = registry.listAll();
|
|
5238
|
-
const remoteAgents = discovery.getReachableRemoteAgents();
|
|
5239
|
-
const peerStates = discovery.getPeerStates();
|
|
5240
|
-
const partyServers = peerStates.filter((p) => p.status === "PARTY_SERVER" || p.status === "DEGRADED" || p.status === "SUSPECT");
|
|
5241
|
-
return c.json({
|
|
5242
|
-
local_agent_count: localAgents.length,
|
|
5243
|
-
remote_agent_count: remoteAgents.length,
|
|
5244
|
-
peer_count: peerStates.length,
|
|
5245
|
-
party_server_count: partyServers.length
|
|
5246
|
-
});
|
|
5247
|
-
});
|
|
5248
|
-
dashboardRoutes.get("/api/overview", async (c) => {
|
|
5249
|
-
let hostname = "127.0.0.1";
|
|
5250
|
-
try {
|
|
5251
|
-
hostname = getTailnetHostname();
|
|
5252
|
-
} catch {
|
|
5253
|
-
}
|
|
5254
|
-
const localAgents = sanitizeAgentList(registry.listAll());
|
|
5255
|
-
const remoteEntries = discovery.getRemoteAgentEntries();
|
|
5256
|
-
const peerStates = discovery.getPeerStates();
|
|
5257
|
-
const seen = /* @__PURE__ */ new Set();
|
|
5258
|
-
const recentMessages = [];
|
|
5259
|
-
for (const agent of localAgents) {
|
|
5260
|
-
const history = messageQueue.getHistory(agent.agent_id, 5);
|
|
5261
|
-
for (const entry of history) {
|
|
5262
|
-
const key = `${entry.sender_id}:${entry.recipient_id ?? ""}:${Math.floor(entry.timestamp)}`;
|
|
5263
|
-
if (seen.has(key)) continue;
|
|
5264
|
-
seen.add(key);
|
|
5265
|
-
recentMessages.push({ agent_id: agent.agent_id, ...entry });
|
|
5266
|
-
}
|
|
5267
|
-
}
|
|
5268
|
-
recentMessages.sort((a, b) => b.timestamp - a.timestamp);
|
|
5269
|
-
if (recentMessages.length > 20) recentMessages.length = 20;
|
|
5270
|
-
const partyServers = peerStates.filter(
|
|
5271
|
-
(p) => p.status === "PARTY_SERVER" || p.status === "DEGRADED" || p.status === "SUSPECT"
|
|
5272
|
-
);
|
|
5273
|
-
return c.json({
|
|
5274
|
-
server: {
|
|
5275
|
-
status: "ok",
|
|
5276
|
-
tailscale_ip: getSelfIp(),
|
|
5277
|
-
hostname,
|
|
5278
|
-
uptime_seconds: Math.floor((Date.now() - STARTED_AT) / 1e3)
|
|
5279
|
-
},
|
|
5280
|
-
agents: {
|
|
5281
|
-
local_count: localAgents.length,
|
|
5282
|
-
remote_count: remoteEntries.length,
|
|
5283
|
-
local_agents: localAgents,
|
|
5284
|
-
remote_agents: remoteEntries.map((e) => ({
|
|
5285
|
-
...sanitizeAgentList([e.agentInfo])[0],
|
|
5286
|
-
source_peer_ip: e.sourcePeerIp,
|
|
5287
|
-
reachable: e.reachable
|
|
5288
|
-
}))
|
|
5289
|
-
},
|
|
5290
|
-
peers: {
|
|
5291
|
-
total: peerStates.length,
|
|
5292
|
-
party_servers: partyServers.length,
|
|
5293
|
-
down: peerStates.filter((p) => p.status === "DOWN").length,
|
|
5294
|
-
unknown: peerStates.filter((p) => p.status === "UNKNOWN" || p.status === "MAYBE").length,
|
|
5295
|
-
details: peerStates
|
|
5296
|
-
},
|
|
5297
|
-
messages: {
|
|
5298
|
-
recent: recentMessages
|
|
5299
|
-
}
|
|
5300
|
-
});
|
|
5301
|
-
});
|
|
5302
|
-
dashboardRoutes.get("/api/tailscale-status", async (c) => {
|
|
5303
|
-
try {
|
|
5304
|
-
return c.json(getTailscaleInstallationStatus());
|
|
5305
|
-
} catch (e) {
|
|
5306
|
-
return c.json({ state: "not_installed", platform: process.platform, error: e.message });
|
|
5307
|
-
}
|
|
5308
|
-
});
|
|
5309
|
-
dashboardRoutes.post("/api/tailscale-detect", async (c) => {
|
|
5310
|
-
resetTailscaleBinaryCache();
|
|
5311
|
-
const state = getTailscaleInstallationStatus();
|
|
5312
|
-
if (state.state === "connected") {
|
|
5313
|
-
refreshSelfIp();
|
|
5314
|
-
}
|
|
5315
|
-
return c.json(state);
|
|
5316
|
-
});
|
|
5317
|
-
dashboardRoutes.get("/api/tailscale-install-info", async (c) => {
|
|
5318
|
-
return c.json(getInstallInstructions(process.platform));
|
|
5319
|
-
});
|
|
5320
|
-
dashboardRoutes.post("/api/join-network", async (c) => {
|
|
5321
|
-
try {
|
|
5322
|
-
const body = await c.req.json();
|
|
5323
|
-
const authKey = (body.auth_key ?? "").trim();
|
|
5324
|
-
if (!authKey) {
|
|
5325
|
-
return c.json({ success: false, output: "auth_key is required" }, 400);
|
|
5326
|
-
}
|
|
5327
|
-
const result = joinTailnet(authKey);
|
|
5328
|
-
logger.info("Dashboard", `Join network: ${result.success ? "success" : "failed"}`);
|
|
5329
|
-
return c.json(result, result.success ? 200 : 500);
|
|
5330
|
-
} catch (e) {
|
|
5331
|
-
return c.json({ success: false, output: e.message }, 500);
|
|
5332
|
-
}
|
|
5333
|
-
});
|
|
5334
|
-
var activeLogin = null;
|
|
5335
|
-
dashboardRoutes.post("/api/logout", async (c) => {
|
|
5336
|
-
const result = logoutTailscale();
|
|
5337
|
-
logger.info("Dashboard", `Logout: ${result.success ? "success" : "failed"}`);
|
|
5338
|
-
if (result.success) {
|
|
5339
|
-
resetTailscaleBinaryCache();
|
|
5340
|
-
refreshSelfIp();
|
|
5341
|
-
}
|
|
5342
|
-
return c.json(result, result.success ? 200 : 500);
|
|
5343
|
-
});
|
|
5344
|
-
dashboardRoutes.post("/api/tailscale-login", async (c) => {
|
|
5345
|
-
if (activeLogin?.url) {
|
|
5346
|
-
return c.json({ success: true, url: activeLogin.url });
|
|
5347
|
-
}
|
|
5348
|
-
const { promise, process: process2 } = startInteractiveLogin();
|
|
5349
|
-
activeLogin = { process: process2 };
|
|
5350
|
-
logger.info("Dashboard", "Tailscale login initiated");
|
|
5351
|
-
const result = await promise;
|
|
5352
|
-
if (result.success && result.url) {
|
|
5353
|
-
activeLogin.url = result.url;
|
|
5354
|
-
return c.json({ success: true, url: result.url });
|
|
5355
|
-
}
|
|
5356
|
-
activeLogin = null;
|
|
5357
|
-
return c.json({ success: false, output: result.output }, 500);
|
|
5358
|
-
});
|
|
5359
|
-
dashboardRoutes.post("/api/install-tailscale", async (c) => {
|
|
5360
|
-
const { installTailscale: installTailscale2 } = await Promise.resolve().then(() => (init_tailscale_installer(), tailscale_installer_exports));
|
|
5361
|
-
const result = await installTailscale2(process.platform);
|
|
5362
|
-
logger.info("Dashboard", `Install Tailscale: ${result.success ? "success" : "failed"}`);
|
|
5363
|
-
if (result.success) {
|
|
5364
|
-
resetTailscaleBinaryCache();
|
|
5365
|
-
}
|
|
5366
|
-
return c.json(result, result.success ? 200 : 500);
|
|
5367
|
-
});
|
|
5368
|
-
|
|
5369
4136
|
// src/server/index.ts
|
|
5370
4137
|
async function periodicCleanup() {
|
|
5371
4138
|
while (!lifecycleController.signal.aborted) {
|
|
@@ -5373,6 +4140,10 @@ async function periodicCleanup() {
|
|
|
5373
4140
|
const removed = registry.cleanupStale(HEARTBEAT_TIMEOUT);
|
|
5374
4141
|
if (removed.length > 0) {
|
|
5375
4142
|
logger.info("Cleanup", `Removed ${removed.length} stale agent(s): ${removed.join(", ")}`);
|
|
4143
|
+
for (const aid of removed) {
|
|
4144
|
+
messageQueue.removeAgent(aid);
|
|
4145
|
+
messageQueue.removeAgentHistory(aid);
|
|
4146
|
+
}
|
|
5376
4147
|
}
|
|
5377
4148
|
} catch (e) {
|
|
5378
4149
|
logger.error("Cleanup", "Error during cleanup", e);
|
|
@@ -5403,7 +4174,6 @@ app.onError((err, c) => {
|
|
|
5403
4174
|
});
|
|
5404
4175
|
app.route("/agent", agentRoutes);
|
|
5405
4176
|
app.route("/proxy", proxyRoutes);
|
|
5406
|
-
app.route("/dashboard", dashboardRoutes);
|
|
5407
4177
|
function pidFilePath() {
|
|
5408
4178
|
const pluginData = process.env.CLAUDE_PLUGIN_DATA || "";
|
|
5409
4179
|
if (pluginData) return join4(pluginData, "server.pid");
|
|
@@ -5416,7 +4186,6 @@ async function performShutdown(server, pidPath) {
|
|
|
5416
4186
|
logger.info("Shutdown", "Shutting down Party Server...");
|
|
5417
4187
|
try {
|
|
5418
4188
|
lifecycleController.abort();
|
|
5419
|
-
getSnapshotManager()?.cancelDebounce();
|
|
5420
4189
|
try {
|
|
5421
4190
|
getSnapshotManager()?.writeSnapshot(registry.listAll(), messageQueue.getHistorySnapshot());
|
|
5422
4191
|
logger.info("Shutdown", "Final snapshot written.");
|
|
@@ -5461,7 +4230,7 @@ async function main() {
|
|
|
5461
4230
|
const savedSnapshot = sm.loadSnapshot();
|
|
5462
4231
|
if (savedSnapshot) {
|
|
5463
4232
|
recoveredAgents = sm.hydrateAgents(registry, getSelfIp());
|
|
5464
|
-
recoveredHistoryEntries = sm.
|
|
4233
|
+
recoveredHistoryEntries = sm.hydrateBuffers(messageQueue);
|
|
5465
4234
|
if (recoveredAgents > 0 || recoveredHistoryEntries > 0) {
|
|
5466
4235
|
logger.info(
|
|
5467
4236
|
"Recovery",
|
|
@@ -5478,7 +4247,7 @@ async function main() {
|
|
|
5478
4247
|
const snapshotLoopPromise = sm.startSnapshotLoop(
|
|
5479
4248
|
lifecycleController.signal,
|
|
5480
4249
|
() => registry.listAll(),
|
|
5481
|
-
() => messageQueue.
|
|
4250
|
+
() => messageQueue.getBufferSnapshots()
|
|
5482
4251
|
);
|
|
5483
4252
|
const shutdownHandler = () => void performShutdown(server, pidPath);
|
|
5484
4253
|
process.on("SIGINT", shutdownHandler);
|