@monster-orange/den 0.4.16 → 0.4.18
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/CHANGELOG.md +101 -0
- package/README.md +7 -7
- package/docs/distributions.md +94 -1
- package/package.json +1 -1
- package/src/ui/cli/agent-ops.ts +50 -0
- package/src/ui/cli/agents-command.ts +5 -5
- package/src/ui/server/agent-routes.ts +13 -1
- package/src/ui/server/check-pool.ts +72 -0
- package/src/ui/server/index.ts +105 -10
- package/src/ui/server/public/agents-page.js +98 -16
- package/src/ui/server/public/agents.html +22 -0
- package/src/ui/server/public/environments.html +223 -140
- package/src/ui/server/public/graph.css +40 -0
- package/src/ui/server/public/graph.js +47 -6
- package/src/ui/server/public/header.js +7 -0
- package/src/ui/server/public/i18n/es.js +46 -0
- package/src/ui/server/public/i18n/ja.js +46 -0
- package/src/ui/server/public/index.html +6 -0
- package/src/ui/server/public/local-dev.html +199 -27
- package/src/ui/server/public/log-source.js +26 -0
- package/src/ui/server/public/logs.html +7 -20
- package/src/ui/server/public/panel.js +1 -1
- package/src/ui/server/public/release.html +19 -1
- package/src/ui/server/public/services.html +18 -3
- package/src/ui/server/public/shell.js +360 -32
- package/src/ui/server/public/svc-panel.js +672 -0
- package/src/ui/server/public/topbar.js +34 -1
- package/src/ui/server/public/view-scope.js +61 -0
- package/src/ui/server/public/workspace-picker.js +2 -0
- package/src/ui/server/route-classes.ts +2 -0
- package/src/ui/server/run-history.ts +181 -0
- package/src/ui/server/session-routes.ts +94 -17
- package/src/ui/server/ui-assets.ts +6 -0
|
@@ -50,6 +50,43 @@
|
|
|
50
50
|
"b.down": "{n} 件停止",
|
|
51
51
|
"act.pin": "ピン留め",
|
|
52
52
|
"act.unpin": "ピン留めを外す",
|
|
53
|
+
"act.visit": "開く",
|
|
54
|
+
"act.visitX": "{s} を開く(新しいタブ)",
|
|
55
|
+
// 0.4.18: the service panel (svc-panel.js) and Start's Folder.
|
|
56
|
+
"svp.label": "{s}: クイックパネル",
|
|
57
|
+
"svp.more": "{s} のクイックパネル",
|
|
58
|
+
"svp.notRunning": "停止中",
|
|
59
|
+
"svp.up": "稼働 {d}",
|
|
60
|
+
"svp.upNow": "稼働中",
|
|
61
|
+
"svp.down": "停止: {c}",
|
|
62
|
+
"svp.downNone": "停止",
|
|
63
|
+
"svp.starting": "起動中…",
|
|
64
|
+
"svp.stopped": "停止",
|
|
65
|
+
"svp.noUrl": "URL なし",
|
|
66
|
+
"svp.start": "開始",
|
|
67
|
+
"svp.stop": "停止",
|
|
68
|
+
"svp.restart": "再起動",
|
|
69
|
+
"svp.logs": "ログ",
|
|
70
|
+
"svp.stopping": "{s} を停止中",
|
|
71
|
+
"svp.tests": "前回のテスト",
|
|
72
|
+
"svp.testsOk": "✓ {t}",
|
|
73
|
+
"svp.testsBad": "✗ 失敗 {t}",
|
|
74
|
+
"svp.testsRun": "実行中…",
|
|
75
|
+
"svp.testsNone": "—",
|
|
76
|
+
"svp.runTests": "テストを実行",
|
|
77
|
+
"svp.testsDone": "テスト成功: {s}",
|
|
78
|
+
"svp.testsFailed": "テスト失敗: {s}",
|
|
79
|
+
"svp.agents": "エージェント",
|
|
80
|
+
"svp.noAgents": "このリポジトリにエージェントはいません。",
|
|
81
|
+
"svp.open": "開く",
|
|
82
|
+
"svp.openAg": "{s} を開く",
|
|
83
|
+
"svp.startHere": "ここでエージェントを開始",
|
|
84
|
+
"svp.openSvc": "サービスで開く",
|
|
85
|
+
"svp.openGraph": "グラフで開く",
|
|
86
|
+
"svp.close": "閉じる",
|
|
87
|
+
"ag.new.folder": "フォルダー(任意)",
|
|
88
|
+
"ag.new.folderHelp":
|
|
89
|
+
"ワークスペースのフォルダー内。空欄ならワークスペースのフォルダー。",
|
|
53
90
|
"act.pinX": "{s} をピン留め",
|
|
54
91
|
"act.unpinX": "{s} のピン留めを外す",
|
|
55
92
|
"toast.pinned": "{s} をピン留めしました",
|
|
@@ -101,6 +138,8 @@
|
|
|
101
138
|
"kind.app": "アプリ",
|
|
102
139
|
"kind.action": "操作",
|
|
103
140
|
"kind.setting": "設定",
|
|
141
|
+
"kind.workspace": "ワークスペース",
|
|
142
|
+
"pal.goWs": "ワークスペース {ws} へ移動",
|
|
104
143
|
"pal.repos": "{n} 個のリポジトリ",
|
|
105
144
|
"pal.restart": "{s} を再起動",
|
|
106
145
|
"pal.results": "結果",
|
|
@@ -500,6 +539,10 @@
|
|
|
500
539
|
"作業中です。このターンの後に変更しますか、それとも今すぐ停止しますか?",
|
|
501
540
|
"ag.pc.after": "このターンの後",
|
|
502
541
|
"ag.pc.now": "今すぐ停止",
|
|
542
|
+
"ag.pc.fresh":
|
|
543
|
+
"{id} は {p} になりました。まだ再開するものがないため、新しく開始しました(会話 ID、フォルダー、範囲は同じ)",
|
|
544
|
+
"ag.freshLine":
|
|
545
|
+
"まだ再開するものがないため、新しく開始しました(会話 ID、フォルダー、範囲は同じ)",
|
|
503
546
|
"ag.pc.done": "{id} は {p} になりました",
|
|
504
547
|
"ag.pc.pending": "このターンが終わると {id} は {p} になります",
|
|
505
548
|
"ag.more": "{id} のその他の操作",
|
|
@@ -653,6 +696,9 @@
|
|
|
653
696
|
"ag.ended": "このセッションは終了しました。記録は再開か削除まで残ります。",
|
|
654
697
|
"ag.switched": "{p} に切り替えました",
|
|
655
698
|
"ag.exitedLine": "[セッションが終了しました{code}]",
|
|
699
|
+
"ag.refusedUnknown": "サーバーがターミナルを拒否しました",
|
|
700
|
+
"ag.refusedStatus": "サーバーがターミナルを拒否しました({status})",
|
|
701
|
+
"ag.goWs": "ワークスペースで {ws} を開く",
|
|
656
702
|
"ag.lost": "[切断: {why}]",
|
|
657
703
|
"ag.reconnect": "再接続",
|
|
658
704
|
"ag.noBackend":
|
|
@@ -290,6 +290,7 @@
|
|
|
290
290
|
}
|
|
291
291
|
.module-picker-dropdown {
|
|
292
292
|
display: none;
|
|
293
|
+
padding: 0;
|
|
293
294
|
position: absolute;
|
|
294
295
|
top: calc(100% + 4px);
|
|
295
296
|
left: 0;
|
|
@@ -1568,6 +1569,11 @@
|
|
|
1568
1569
|
e.stopPropagation();
|
|
1569
1570
|
const open = pickerDropdown.classList.toggle("open");
|
|
1570
1571
|
pickerBtn.classList.toggle("open", open);
|
|
1572
|
+
// 0.4.17: in the top layer, so the output pane can't cover it.
|
|
1573
|
+
if (open)
|
|
1574
|
+
window.denShell?.topLayer?.show(pickerDropdown, pickerBtn, {
|
|
1575
|
+
gap: 4,
|
|
1576
|
+
});
|
|
1571
1577
|
});
|
|
1572
1578
|
document.addEventListener("click", () => {
|
|
1573
1579
|
pickerDropdown.classList.remove("open");
|
|
@@ -1168,6 +1168,11 @@
|
|
|
1168
1168
|
}
|
|
1169
1169
|
.tablewrap {
|
|
1170
1170
|
overflow-x: auto;
|
|
1171
|
+
/* 0.4.17: in a short window (under ~620 px tall) the column's other
|
|
1172
|
+
parts squeezed it to nothing; it keeps room for rows and scrolls. */
|
|
1173
|
+
min-height: 200px;
|
|
1174
|
+
flex-shrink: 0;
|
|
1175
|
+
overflow-y: auto;
|
|
1171
1176
|
border: 1px solid var(--border);
|
|
1172
1177
|
border-radius: var(--radius);
|
|
1173
1178
|
background: var(--surface);
|
|
@@ -1285,6 +1290,9 @@
|
|
|
1285
1290
|
.tbl-health.ok {
|
|
1286
1291
|
color: var(--ok);
|
|
1287
1292
|
}
|
|
1293
|
+
.tbl-up:not(:empty) {
|
|
1294
|
+
margin-left: 8px;
|
|
1295
|
+
}
|
|
1288
1296
|
.tbl-muted {
|
|
1289
1297
|
color: var(--muted);
|
|
1290
1298
|
font-size: 12.5px;
|
|
@@ -1330,6 +1338,46 @@
|
|
|
1330
1338
|
.tbl-acts .tbl-more-sp {
|
|
1331
1339
|
display: inline-block;
|
|
1332
1340
|
}
|
|
1341
|
+
/* Visit ↗ (0.4.17): before Start, so Start/Stop/Restart/Logs and ⋯
|
|
1342
|
+
still line up across rows whether a service has a URL or not. */
|
|
1343
|
+
.tbl-acts .tbl-visit {
|
|
1344
|
+
flex: none;
|
|
1345
|
+
padding-left: 6px;
|
|
1346
|
+
padding-right: 6px;
|
|
1347
|
+
text-decoration: none;
|
|
1348
|
+
}
|
|
1349
|
+
/* The table never scrolls sideways: in a window under 1600px wide
|
|
1350
|
+
(the workspace list beside it) Visit is its ↗; its name and title
|
|
1351
|
+
still say Visit. */
|
|
1352
|
+
@media (max-width: 1599px) {
|
|
1353
|
+
.tbl-visit .tbl-visit-w {
|
|
1354
|
+
display: none;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
table.svc a.repo-name {
|
|
1358
|
+
text-decoration: underline;
|
|
1359
|
+
text-decoration-color: var(--border-strong);
|
|
1360
|
+
text-underline-offset: 2px;
|
|
1361
|
+
}
|
|
1362
|
+
table.svc a.repo-name:hover {
|
|
1363
|
+
text-decoration-color: currentColor;
|
|
1364
|
+
}
|
|
1365
|
+
/* The favourite star (0.4.17): the shell's pins, as on Services. */
|
|
1366
|
+
.tbl-star {
|
|
1367
|
+
background: none;
|
|
1368
|
+
border: 1px solid transparent;
|
|
1369
|
+
border-radius: var(--radius-button);
|
|
1370
|
+
color: var(--faint);
|
|
1371
|
+
cursor: pointer;
|
|
1372
|
+
font-size: 13px;
|
|
1373
|
+
line-height: 1.2;
|
|
1374
|
+
padding: 0 2px;
|
|
1375
|
+
}
|
|
1376
|
+
.tbl-star:hover,
|
|
1377
|
+
.tbl-star[aria-pressed="true"] {
|
|
1378
|
+
color: var(--accent-text);
|
|
1379
|
+
background: var(--sunken);
|
|
1380
|
+
}
|
|
1333
1381
|
.tbl-btn:disabled {
|
|
1334
1382
|
opacity: 0.45;
|
|
1335
1383
|
cursor: not-allowed;
|
|
@@ -1769,7 +1817,11 @@
|
|
|
1769
1817
|
.querySelectorAll(".ws-menu-dropdown.open")
|
|
1770
1818
|
.forEach((d) => d.classList.remove("open"));
|
|
1771
1819
|
const menu = document.getElementById(menuBtn.dataset.menuId);
|
|
1772
|
-
menu?.classList.toggle("open")
|
|
1820
|
+
if (menu?.classList.toggle("open"))
|
|
1821
|
+
window.denShell?.topLayer?.show(menu, menuBtn, {
|
|
1822
|
+
align: "right",
|
|
1823
|
+
gap: 2,
|
|
1824
|
+
});
|
|
1773
1825
|
return;
|
|
1774
1826
|
}
|
|
1775
1827
|
// ↗ open in editor
|
|
@@ -1822,7 +1874,12 @@
|
|
|
1822
1874
|
if (updateUrl) {
|
|
1823
1875
|
const params = new URLSearchParams(location.search);
|
|
1824
1876
|
params.set("ws", name);
|
|
1825
|
-
|
|
1877
|
+
// The hash stays (#focus=<id>: Open in Graph's card).
|
|
1878
|
+
history.replaceState(
|
|
1879
|
+
null,
|
|
1880
|
+
"",
|
|
1881
|
+
"?" + params.toString() + location.hash,
|
|
1882
|
+
);
|
|
1826
1883
|
}
|
|
1827
1884
|
document
|
|
1828
1885
|
.querySelectorAll(".ws-item")
|
|
@@ -1858,8 +1915,16 @@
|
|
|
1858
1915
|
if (d.id !== btn.dataset.menuId) d.style.display = "none";
|
|
1859
1916
|
});
|
|
1860
1917
|
const menu = document.getElementById(btn.dataset.menuId);
|
|
1861
|
-
if (menu)
|
|
1862
|
-
|
|
1918
|
+
if (menu) {
|
|
1919
|
+
const show = menu.style.display === "none";
|
|
1920
|
+
menu.style.display = show ? "" : "none";
|
|
1921
|
+
// 0.4.17: in the top layer, so no row or card clips it.
|
|
1922
|
+
if (show)
|
|
1923
|
+
window.denShell?.topLayer?.show(menu, btn, {
|
|
1924
|
+
align: "right",
|
|
1925
|
+
gap: 3,
|
|
1926
|
+
});
|
|
1927
|
+
}
|
|
1863
1928
|
e.stopPropagation();
|
|
1864
1929
|
return;
|
|
1865
1930
|
}
|
|
@@ -2102,7 +2167,7 @@
|
|
|
2102
2167
|
<th scope="col">Health</th>
|
|
2103
2168
|
<th scope="col">Branch</th>
|
|
2104
2169
|
<th scope="col">Mode</th>
|
|
2105
|
-
<th scope="col">Last run</th>
|
|
2170
|
+
<th scope="col">Last test run</th>
|
|
2106
2171
|
<th scope="col"><span class="tbl-sr">Actions</span></th>
|
|
2107
2172
|
</tr></thead>
|
|
2108
2173
|
</table>
|
|
@@ -2272,22 +2337,60 @@
|
|
|
2272
2337
|
return [{ key: "", title: null, rows }];
|
|
2273
2338
|
}
|
|
2274
2339
|
|
|
2275
|
-
|
|
2340
|
+
// 0.4.17: each repo's last test run, from the server's run history
|
|
2341
|
+
// (every browser's runs, kept across restarts), and when its
|
|
2342
|
+
// service started (the daemons list), shown apart from it.
|
|
2343
|
+
let _lastRuns = {};
|
|
2344
|
+
let _upSince = new Map();
|
|
2345
|
+
async function loadLastRuns() {
|
|
2346
|
+
const [last, daemons] = await Promise.all([
|
|
2347
|
+
fetch("/api/runs/last")
|
|
2348
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
2349
|
+
.catch(() => null),
|
|
2350
|
+
fetch("/api/service/daemons")
|
|
2351
|
+
.then((r) => (r.ok ? r.json() : []))
|
|
2352
|
+
.catch(() => []),
|
|
2353
|
+
]);
|
|
2354
|
+
_lastRuns = last?.repos ?? {};
|
|
2355
|
+
_upSince = new Map();
|
|
2356
|
+
for (const d of Array.isArray(daemons) ? daemons : []) {
|
|
2357
|
+
const at = Date.parse(d.startedAt);
|
|
2358
|
+
if (!d.repoName || !Number.isFinite(at)) continue;
|
|
2359
|
+
const cur = _upSince.get(d.repoName);
|
|
2360
|
+
if (cur === undefined || at < cur) _upSince.set(d.repoName, at);
|
|
2361
|
+
}
|
|
2362
|
+
paintLastRuns();
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
/** A repo's last test run: "✓ 12:03", "✗ failed 12:03", "running…", "—". */
|
|
2276
2366
|
function lastRunText(repo) {
|
|
2277
|
-
const
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
);
|
|
2281
|
-
const r = mine.sort((a, b) => (b.ranAt ?? 0) - (a.ranAt ?? 0))[0];
|
|
2282
|
-
if (!r || (!r.ranAt && !r.running)) return { text: "—", cls: "" };
|
|
2283
|
-
if (r.running) return { text: "running…", cls: "" };
|
|
2367
|
+
const r = _lastRuns[repo];
|
|
2368
|
+
if (!r) return { text: "—", cls: "", title: "" };
|
|
2369
|
+
const title = [r.pillar, r.env].filter(Boolean).join(" · ");
|
|
2370
|
+
if (r.running) return { text: "running…", cls: "", title };
|
|
2284
2371
|
const t = new Intl.DateTimeFormat(undefined, {
|
|
2285
2372
|
hour: "2-digit",
|
|
2286
2373
|
minute: "2-digit",
|
|
2287
|
-
}).format(new Date(r.endedAt ?? r.
|
|
2288
|
-
return r.
|
|
2289
|
-
? { text:
|
|
2290
|
-
: { text:
|
|
2374
|
+
}).format(new Date(r.endedAt ?? r.startedAt));
|
|
2375
|
+
return r.exitCode === 0
|
|
2376
|
+
? { text: `✓ ${t}`, cls: "ok", title }
|
|
2377
|
+
: { text: `✗ failed ${t}`, cls: "bad", title };
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
/** How long a repo's service has run: "up 2h", or "" when it doesn't. */
|
|
2381
|
+
function upText(repo) {
|
|
2382
|
+
const at = _upSince.get(repo);
|
|
2383
|
+
if (at === undefined) return "";
|
|
2384
|
+
const s = Math.max(0, Math.round((Date.now() - at) / 1000));
|
|
2385
|
+
const d =
|
|
2386
|
+
s < 60
|
|
2387
|
+
? `${s}s`
|
|
2388
|
+
: s < 3600
|
|
2389
|
+
? `${Math.floor(s / 60)}m`
|
|
2390
|
+
: s < 86400
|
|
2391
|
+
? `${Math.floor(s / 3600)}h`
|
|
2392
|
+
: `${Math.floor(s / 86400)}d`;
|
|
2393
|
+
return `up ${d}`;
|
|
2291
2394
|
}
|
|
2292
2395
|
|
|
2293
2396
|
/** A health cell: the dot and the rollup, or the failing check. */
|
|
@@ -2335,6 +2438,47 @@
|
|
|
2335
2438
|
);
|
|
2336
2439
|
}
|
|
2337
2440
|
|
|
2441
|
+
/** A service's URL to visit (its edge URL if routed, else its local one), or null. */
|
|
2442
|
+
function visitUrl(svc) {
|
|
2443
|
+
const u = svc?.open;
|
|
2444
|
+
return typeof u === "string" && /^https?:\/\//.test(u) ? u : null;
|
|
2445
|
+
}
|
|
2446
|
+
/** The shell's words (its language), else English. */
|
|
2447
|
+
function shT(key, vars, en) {
|
|
2448
|
+
const v = window.denShell?.t?.(key, vars);
|
|
2449
|
+
return v && v !== key ? v : en;
|
|
2450
|
+
}
|
|
2451
|
+
/** Visit ↗ (0.4.17): opens the service in a new tab; nothing without a URL. */
|
|
2452
|
+
function visitBtn(svc, name) {
|
|
2453
|
+
const url = visitUrl(svc);
|
|
2454
|
+
if (!url) return "";
|
|
2455
|
+
const label = shT(
|
|
2456
|
+
"act.visitX",
|
|
2457
|
+
{ s: name },
|
|
2458
|
+
`Visit ${name} (opens a new tab)`,
|
|
2459
|
+
);
|
|
2460
|
+
return `<a class="den-btn sm tbl-btn tbl-visit" href="${escHtml(url)}" target="_blank" rel="noopener" aria-label="${escHtml(label)}" title="${escHtml(url)}" data-k="visit:${escHtml(name)}"><span class="tbl-visit-w">${escHtml(shT("act.visit", {}, "Visit"))} </span>↗</a>`;
|
|
2461
|
+
}
|
|
2462
|
+
/** A row's name: a link to visit when the service has a URL. */
|
|
2463
|
+
function nameLink(svc, text) {
|
|
2464
|
+
const url = visitUrl(svc);
|
|
2465
|
+
return url
|
|
2466
|
+
? `<a class="repo-name" href="${escHtml(url)}" target="_blank" rel="noopener" title="${escHtml(shT("act.visitX", { s: url }, `Visit ${url} (opens a new tab)`))}">${escHtml(text)}</a>`
|
|
2467
|
+
: `<span class="repo-name">${escHtml(text)}</span>`;
|
|
2468
|
+
}
|
|
2469
|
+
/** The favourite star (0.4.17): the shell's pins, the same store as Services. */
|
|
2470
|
+
function starBtn(id) {
|
|
2471
|
+
const sh = window.denShell;
|
|
2472
|
+
if (!sh?.isPinned) return "";
|
|
2473
|
+
const on = sh.isPinned(id);
|
|
2474
|
+
const label = shT(
|
|
2475
|
+
on ? "act.unpinX" : "act.pinX",
|
|
2476
|
+
{ s: id },
|
|
2477
|
+
`${on ? "Unpin" : "Pin"} ${id}`,
|
|
2478
|
+
);
|
|
2479
|
+
return `<button type="button" class="tbl-star" data-pin-toggle="${escHtml(id)}" aria-pressed="${on ? "true" : "false"}" aria-label="${escHtml(label)}" title="${escHtml(label)}" data-k="star:${escHtml(id)}">${on ? "★" : "☆"}</button>`;
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2338
2482
|
/** Action buttons; each one's name says what and on which repo or service. */
|
|
2339
2483
|
function actBtns(id, o) {
|
|
2340
2484
|
const b = (act, label, name, extra = "") =>
|
|
@@ -2407,12 +2551,12 @@
|
|
|
2407
2551
|
? `<span class="tbl-mode"><span class="badge ${escHtml(mode)}">${escHtml(mode)}</span><button type="button" class="tbl-switch" data-action="tbl-mode" data-repo="${escHtml(repo)}" data-mode="${switchTo}" aria-label="${escHtml(repo)}: use ${mode === "managed" ? "local" : "managed"}" data-k="mode:${escHtml(repo)}">use ${mode === "managed" ? "local" : "managed"}</button></span>`
|
|
2408
2552
|
: "";
|
|
2409
2553
|
const head = `<tr class="repo${r.problem ? " prob" : ""}" data-repo="${escHtml(repo)}">
|
|
2410
|
-
<th scope="row">${exp}
|
|
2411
|
-
<td>${repoHealth(r)}</td>
|
|
2554
|
+
<th scope="row">${exp}${nameLink(primary, repo)} ${starBtn(repo)}${badges}</th>
|
|
2555
|
+
<td>${repoHealth(r)}<span class="tbl-muted tbl-up" data-up="${escHtml(repo)}">${escHtml(upText(repo))}</span></td>
|
|
2412
2556
|
<td class="tbl-mono tbl-muted">${escHtml(primary?.branch ?? "")}</td>
|
|
2413
2557
|
<td>${modeCell}</td>
|
|
2414
|
-
<td class="tbl-muted${run.cls ? " " + run.cls : ""}" data-last="${escHtml(repo)}">${escHtml(run.text)}</td>
|
|
2415
|
-
<td><div class="tbl-acts">${actBtns(repo, { up: !!startOf(repo).length, restart: !!startOf(repo).length })}${repoMenu(r)}</div></td>
|
|
2558
|
+
<td class="tbl-muted${run.cls ? " " + run.cls : ""}" data-last="${escHtml(repo)}" title="${escHtml(run.title)}">${escHtml(run.text)}</td>
|
|
2559
|
+
<td><div class="tbl-acts">${visitBtn(primary, repo)}${actBtns(repo, { up: !!startOf(repo).length, restart: !!startOf(repo).length })}${repoMenu(r)}</div></td>
|
|
2416
2560
|
</tr>`;
|
|
2417
2561
|
const fixes = r.fixes
|
|
2418
2562
|
.map(
|
|
@@ -2434,10 +2578,10 @@
|
|
|
2434
2578
|
s,
|
|
2435
2579
|
i,
|
|
2436
2580
|
) => `<tr class="sub" id="${subIds[i]}" data-repo="${escHtml(repo)}" data-id="${escHtml(s.id)}"${shown(s) ? "" : " hidden"}>
|
|
2437
|
-
<th scope="row"
|
|
2581
|
+
<th scope="row">${nameLink(s, s.name)} ${starBtn(s.id)}${s.entry ? ' <span class="badge">entry</span>' : ""}</th>
|
|
2438
2582
|
<td>${!_lastHealthAt ? CHECKING() : healthCell(status(s), s.health.ok === false ? checkLineOf(s) : healthLine(s))}</td>
|
|
2439
2583
|
<td></td><td></td><td></td>
|
|
2440
|
-
<td><div class="tbl-acts">${actBtns(s.id, { up: s.commands.includes("up"), restart: s.commands.includes("up") || s.commands.includes("restart") })}<span class="tbl-more-sp" aria-hidden="true"></span></div></td>
|
|
2584
|
+
<td><div class="tbl-acts">${visitBtn(s, s.id)}${actBtns(s.id, { up: s.commands.includes("up"), restart: s.commands.includes("up") || s.commands.includes("restart") })}<span class="tbl-more-sp" aria-hidden="true"></span></div></td>
|
|
2441
2585
|
</tr>`,
|
|
2442
2586
|
)
|
|
2443
2587
|
.join("")
|
|
@@ -2516,13 +2660,17 @@
|
|
|
2516
2660
|
if (keep)
|
|
2517
2661
|
table.querySelector(`[data-k="${CSS.escape(keep)}"]`)?.focus();
|
|
2518
2662
|
}
|
|
2519
|
-
/** Only the Last run cells (the
|
|
2663
|
+
/** Only the Last test run and "up" cells (the history changed). */
|
|
2520
2664
|
function paintLastRuns() {
|
|
2521
2665
|
document.querySelectorAll("#repo-table [data-last]").forEach((td) => {
|
|
2522
2666
|
const run = lastRunText(td.dataset.last);
|
|
2523
2667
|
td.textContent = run.text;
|
|
2668
|
+
td.title = run.title;
|
|
2524
2669
|
td.className = "tbl-muted" + (run.cls ? " " + run.cls : "");
|
|
2525
2670
|
});
|
|
2671
|
+
document.querySelectorAll("#repo-table [data-up]").forEach((el) => {
|
|
2672
|
+
el.textContent = upText(el.dataset.up);
|
|
2673
|
+
});
|
|
2526
2674
|
}
|
|
2527
2675
|
|
|
2528
2676
|
/**
|
|
@@ -2581,6 +2729,14 @@
|
|
|
2581
2729
|
setTableGroup(g.dataset.group);
|
|
2582
2730
|
return;
|
|
2583
2731
|
}
|
|
2732
|
+
const star = e.target.closest("#repo-table [data-pin-toggle]");
|
|
2733
|
+
if (star) {
|
|
2734
|
+
const sh = window.denShell;
|
|
2735
|
+
const id = star.dataset.pinToggle;
|
|
2736
|
+
if (sh?.isPinned(id)) sh.unpin(id);
|
|
2737
|
+
else sh?.pin(id);
|
|
2738
|
+
return;
|
|
2739
|
+
}
|
|
2584
2740
|
const b = e.target.closest("[data-action^='tbl-']");
|
|
2585
2741
|
if (!b || b.disabled) return;
|
|
2586
2742
|
const a = b.dataset.action;
|
|
@@ -2641,8 +2797,20 @@
|
|
|
2641
2797
|
const v = localStorage.getItem(LS_GROUP);
|
|
2642
2798
|
if (["none", "app", "org"].includes(v)) _tblGroup = v;
|
|
2643
2799
|
} catch {}
|
|
2644
|
-
// The Last run column
|
|
2645
|
-
|
|
2800
|
+
// The Last test run column: the server's history, read now, when a
|
|
2801
|
+
// run here changes, and every 30 s for runs from elsewhere.
|
|
2802
|
+
void loadLastRuns();
|
|
2803
|
+
// The dock changes on every line of a run: read at most every 2 s.
|
|
2804
|
+
let _lastRunsSoon = null;
|
|
2805
|
+
window.denShell?.runs?.onChange?.(() => {
|
|
2806
|
+
_lastRunsSoon ??= setTimeout(() => {
|
|
2807
|
+
_lastRunsSoon = null;
|
|
2808
|
+
void loadLastRuns();
|
|
2809
|
+
}, 2_000);
|
|
2810
|
+
});
|
|
2811
|
+
setInterval(() => {
|
|
2812
|
+
if (!document.hidden) void loadLastRuns();
|
|
2813
|
+
}, 30_000);
|
|
2646
2814
|
|
|
2647
2815
|
// ── Tab management (one persistent tab per service address) ───────────────
|
|
2648
2816
|
//
|
|
@@ -4110,7 +4278,11 @@
|
|
|
4110
4278
|
const params = new URLSearchParams(location.search);
|
|
4111
4279
|
params.delete("ws");
|
|
4112
4280
|
const qs = params.toString();
|
|
4113
|
-
history.replaceState(
|
|
4281
|
+
history.replaceState(
|
|
4282
|
+
null,
|
|
4283
|
+
"",
|
|
4284
|
+
(qs ? "?" + qs : location.pathname) + location.hash,
|
|
4285
|
+
);
|
|
4114
4286
|
}
|
|
4115
4287
|
await loadWorkspaces();
|
|
4116
4288
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a service's live log comes from (0.4.18): one rule for the Logs
|
|
3
|
+
* page and the service panel's Logs, so they never disagree.
|
|
4
|
+
*
|
|
5
|
+
* - Den's own server log (the "__DEN_CLI__/server" pseudo-service):
|
|
6
|
+
* /api/server-log, the server's log ring;
|
|
7
|
+
* - a local service: /api/service/taillog, the workspace page's source
|
|
8
|
+
* chain (the manifest's command, the conventional container, the
|
|
9
|
+
* daemon's startup log). /api/service/logs answers 404 for a service
|
|
10
|
+
* with no `logs:` block;
|
|
11
|
+
* - any other env: /api/service/logs, for cloud sources and the reauth
|
|
12
|
+
* flow.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** The server's own log, shown like a service's. */
|
|
16
|
+
export function isServerLog(repo, service) {
|
|
17
|
+
return repo === "__DEN_CLI__" && service === "server";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The SSE URL of `repo/service`'s live log in `env`. */
|
|
21
|
+
export function logStreamUrl(repo, service, env) {
|
|
22
|
+
if (isServerLog(repo, service)) return "/api/server-log";
|
|
23
|
+
if (env === "local")
|
|
24
|
+
return `/api/service/taillog?${new URLSearchParams({ repo, service })}`;
|
|
25
|
+
return `/api/service/logs?${new URLSearchParams({ repo, service, env })}`;
|
|
26
|
+
}
|
|
@@ -476,6 +476,7 @@
|
|
|
476
476
|
envReady,
|
|
477
477
|
initWorkspacePill,
|
|
478
478
|
} from "/header.js";
|
|
479
|
+
import { isServerLog, logStreamUrl } from "/log-source.js";
|
|
479
480
|
|
|
480
481
|
// ── Safe highlight rendering ──────────────────────────────────────────────
|
|
481
482
|
// Log lines are UNTRUSTED (any service can log attacker-influenced text,
|
|
@@ -550,11 +551,9 @@
|
|
|
550
551
|
const logStreams = new Map();
|
|
551
552
|
|
|
552
553
|
// "__DEN_CLI__/server" is a pseudo-service that taps the server's own log ring
|
|
553
|
-
// via /api/server-log instead of /api/service/logs
|
|
554
|
-
// page treats it like a normal
|
|
555
|
-
|
|
556
|
-
return repo === "__DEN_CLI__" && service === "server";
|
|
557
|
-
}
|
|
554
|
+
// via /api/server-log instead of /api/service/logs (isServerLog,
|
|
555
|
+
// log-source.js). Everything else in the page treats it like a normal
|
|
556
|
+
// log stream.
|
|
558
557
|
function formatServerLogEntry(entry) {
|
|
559
558
|
// "14:07:41 [lifecycle] ▶ run synthetic/local · modules=all [abcxyz]"
|
|
560
559
|
// An agent's action reads "[agent]" and ends "· by agent <client> …".
|
|
@@ -1075,21 +1074,9 @@
|
|
|
1075
1074
|
// ─────────────────────────────────────────────────────────────────────
|
|
1076
1075
|
|
|
1077
1076
|
const serverLog = isServerLog(repo, service);
|
|
1078
|
-
//
|
|
1079
|
-
//
|
|
1080
|
-
|
|
1081
|
-
// /api/service/logs 404s for services with no logs block (shop-web,
|
|
1082
|
-
// control-plane). Non-local envs keep /api/service/logs for
|
|
1083
|
-
// cloudwatch sources + the AWS reauth flow.
|
|
1084
|
-
const es = serverLog
|
|
1085
|
-
? new EventSource("/api/server-log")
|
|
1086
|
-
: env === "local"
|
|
1087
|
-
? new EventSource(
|
|
1088
|
-
`/api/service/taillog?${new URLSearchParams({ repo, service })}`,
|
|
1089
|
-
)
|
|
1090
|
-
: new EventSource(
|
|
1091
|
-
`/api/service/logs?${new URLSearchParams({ repo, service, env })}`,
|
|
1092
|
-
);
|
|
1077
|
+
// Where the stream comes from (taillog for local, logs elsewhere):
|
|
1078
|
+
// log-source.js, which the service panel's Logs uses too.
|
|
1079
|
+
const es = new EventSource(logStreamUrl(repo, service, env));
|
|
1093
1080
|
|
|
1094
1081
|
es.onmessage = (e) => {
|
|
1095
1082
|
const msg = JSON.parse(e.data);
|
|
@@ -134,6 +134,10 @@
|
|
|
134
134
|
font-size: 11px;
|
|
135
135
|
color: var(--muted);
|
|
136
136
|
}
|
|
137
|
+
.view-note {
|
|
138
|
+
color: var(--muted);
|
|
139
|
+
font-size: 12px;
|
|
140
|
+
}
|
|
137
141
|
#status-text {
|
|
138
142
|
font-size: 11px;
|
|
139
143
|
color: var(--muted);
|
|
@@ -678,6 +682,7 @@
|
|
|
678
682
|
⟳ Refresh
|
|
679
683
|
</button>
|
|
680
684
|
<span id="status-text">idle</span>
|
|
685
|
+
<span id="view-note"></span>
|
|
681
686
|
<div class="h-spacer" style="flex: 1"></div>
|
|
682
687
|
<button class="btn" id="waves-btn" title="Edit wave assignment">
|
|
683
688
|
Waves…
|
|
@@ -735,6 +740,7 @@
|
|
|
735
740
|
refreshHealth,
|
|
736
741
|
initWorkspacePill,
|
|
737
742
|
} from "/header.js";
|
|
743
|
+
import { viewNoteHtml, viewRepos, viewScope } from "/view-scope.js";
|
|
738
744
|
|
|
739
745
|
// The header's dots report LOCAL service health here: #den-shell pins
|
|
740
746
|
// data-health-env="local" (shell.js). The env picker is the shell's
|
|
@@ -819,6 +825,15 @@
|
|
|
819
825
|
.replace(/>/g, ">")
|
|
820
826
|
.replace(/"/g, """);
|
|
821
827
|
|
|
828
|
+
// 0.4.17: the repos follow the top bar's View (Org and Workspace),
|
|
829
|
+
// as on Environments: no picker of this page's own (view-scope.js).
|
|
830
|
+
let viewIn = null;
|
|
831
|
+
const viewReady = viewScope().then(async (v) => {
|
|
832
|
+
const note = document.getElementById("view-note");
|
|
833
|
+
if (note) note.innerHTML = viewNoteHtml(v);
|
|
834
|
+
viewIn = await viewRepos(v);
|
|
835
|
+
});
|
|
836
|
+
|
|
822
837
|
// ── Init ──────────────────────────────────────────────────────────────────
|
|
823
838
|
async function init() {
|
|
824
839
|
try {
|
|
@@ -909,7 +924,10 @@
|
|
|
909
924
|
const data = await fetch(
|
|
910
925
|
`/api/release/status?env=${encodeURIComponent(state.env)}&refresh=${refresh ? 1 : 0}`,
|
|
911
926
|
).then((r) => r.json());
|
|
912
|
-
|
|
927
|
+
await viewReady;
|
|
928
|
+
state.rows = (data.rows || []).filter(
|
|
929
|
+
(r) => !viewIn || viewIn.has(r.repo),
|
|
930
|
+
);
|
|
913
931
|
setStatus(
|
|
914
932
|
data.error
|
|
915
933
|
? `error: ${data.error}`
|
|
@@ -211,6 +211,20 @@
|
|
|
211
211
|
font-weight: 600;
|
|
212
212
|
color: var(--text);
|
|
213
213
|
}
|
|
214
|
+
/* 0.4.18: the name opens the service panel (svc-panel.js). */
|
|
215
|
+
.svc-name-btn {
|
|
216
|
+
background: none;
|
|
217
|
+
border: 0;
|
|
218
|
+
padding: 0;
|
|
219
|
+
font: inherit;
|
|
220
|
+
color: inherit;
|
|
221
|
+
cursor: pointer;
|
|
222
|
+
text-align: left;
|
|
223
|
+
}
|
|
224
|
+
.svc-name-btn:hover,
|
|
225
|
+
.svc-name-btn[aria-expanded="true"] {
|
|
226
|
+
text-decoration: underline;
|
|
227
|
+
}
|
|
214
228
|
.service-repo .owners {
|
|
215
229
|
color: var(--accent-text);
|
|
216
230
|
}
|
|
@@ -335,6 +349,7 @@
|
|
|
335
349
|
envReady,
|
|
336
350
|
sudoRun,
|
|
337
351
|
} from "/header.js";
|
|
352
|
+
import { logStreamUrl } from "/log-source.js";
|
|
338
353
|
import { ownersLabel } from "/workspace-picker.js";
|
|
339
354
|
|
|
340
355
|
let es = null;
|
|
@@ -656,7 +671,7 @@
|
|
|
656
671
|
<div class="service-card-header">
|
|
657
672
|
<span class="dot" id="dot-${s.repo}-${s.service}"></span>
|
|
658
673
|
<div class="service-card-title">
|
|
659
|
-
<div class="service-name">${addr}</div>
|
|
674
|
+
<div class="service-name"><button type="button" class="svc-name-btn" data-svpanel-open="${esc(isSub ? addr : s.repo)}" aria-haspopup="dialog" aria-expanded="false" title="${esc(window.denShell?.t?.("svp.more", { s: isSub ? addr : s.repo }) ?? addr)}">${esc(addr)}</button></div>
|
|
660
675
|
<div class="service-repo">${s.repo}${
|
|
661
676
|
ownersLabel(s.inWorkspaces)
|
|
662
677
|
? `<span class="owners" title="Shared: listed once for every workspace in view that defines it">${esc(ownersLabel(s.inWorkspaces))}</span>`
|
|
@@ -821,8 +836,8 @@
|
|
|
821
836
|
);
|
|
822
837
|
appendLine(`\n▶ ${repo}/${service} logs · ${env}\n`, "cmd");
|
|
823
838
|
|
|
824
|
-
|
|
825
|
-
es = new EventSource(
|
|
839
|
+
// The Logs page's source (log-source.js): taillog for local.
|
|
840
|
+
es = new EventSource(logStreamUrl(repo, service, env));
|
|
826
841
|
handleSSE(es, repo, service, { stream: true });
|
|
827
842
|
}
|
|
828
843
|
|