@monster-orange/den 0.4.16 → 0.4.17
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 +58 -0
- package/README.md +7 -7
- package/docs/distributions.md +55 -0
- package/package.json +1 -1
- package/src/ui/cli/agent-ops.ts +7 -0
- package/src/ui/cli/agents-command.ts +5 -5
- package/src/ui/server/agent-routes.ts +4 -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 +75 -16
- package/src/ui/server/public/agents.html +9 -0
- package/src/ui/server/public/environments.html +223 -140
- package/src/ui/server/public/graph.css +18 -0
- package/src/ui/server/public/graph.js +23 -6
- package/src/ui/server/public/i18n/es.js +11 -0
- package/src/ui/server/public/i18n/ja.js +11 -0
- package/src/ui/server/public/index.html +6 -0
- package/src/ui/server/public/local-dev.html +188 -25
- package/src/ui/server/public/panel.js +1 -1
- package/src/ui/server/public/release.html +19 -1
- package/src/ui/server/public/shell.js +152 -7
- 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 +2 -0
|
@@ -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
|
|
@@ -1858,8 +1910,16 @@
|
|
|
1858
1910
|
if (d.id !== btn.dataset.menuId) d.style.display = "none";
|
|
1859
1911
|
});
|
|
1860
1912
|
const menu = document.getElementById(btn.dataset.menuId);
|
|
1861
|
-
if (menu)
|
|
1862
|
-
|
|
1913
|
+
if (menu) {
|
|
1914
|
+
const show = menu.style.display === "none";
|
|
1915
|
+
menu.style.display = show ? "" : "none";
|
|
1916
|
+
// 0.4.17: in the top layer, so no row or card clips it.
|
|
1917
|
+
if (show)
|
|
1918
|
+
window.denShell?.topLayer?.show(menu, btn, {
|
|
1919
|
+
align: "right",
|
|
1920
|
+
gap: 3,
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1863
1923
|
e.stopPropagation();
|
|
1864
1924
|
return;
|
|
1865
1925
|
}
|
|
@@ -2102,7 +2162,7 @@
|
|
|
2102
2162
|
<th scope="col">Health</th>
|
|
2103
2163
|
<th scope="col">Branch</th>
|
|
2104
2164
|
<th scope="col">Mode</th>
|
|
2105
|
-
<th scope="col">Last run</th>
|
|
2165
|
+
<th scope="col">Last test run</th>
|
|
2106
2166
|
<th scope="col"><span class="tbl-sr">Actions</span></th>
|
|
2107
2167
|
</tr></thead>
|
|
2108
2168
|
</table>
|
|
@@ -2272,22 +2332,60 @@
|
|
|
2272
2332
|
return [{ key: "", title: null, rows }];
|
|
2273
2333
|
}
|
|
2274
2334
|
|
|
2275
|
-
|
|
2335
|
+
// 0.4.17: each repo's last test run, from the server's run history
|
|
2336
|
+
// (every browser's runs, kept across restarts), and when its
|
|
2337
|
+
// service started (the daemons list), shown apart from it.
|
|
2338
|
+
let _lastRuns = {};
|
|
2339
|
+
let _upSince = new Map();
|
|
2340
|
+
async function loadLastRuns() {
|
|
2341
|
+
const [last, daemons] = await Promise.all([
|
|
2342
|
+
fetch("/api/runs/last")
|
|
2343
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
2344
|
+
.catch(() => null),
|
|
2345
|
+
fetch("/api/service/daemons")
|
|
2346
|
+
.then((r) => (r.ok ? r.json() : []))
|
|
2347
|
+
.catch(() => []),
|
|
2348
|
+
]);
|
|
2349
|
+
_lastRuns = last?.repos ?? {};
|
|
2350
|
+
_upSince = new Map();
|
|
2351
|
+
for (const d of Array.isArray(daemons) ? daemons : []) {
|
|
2352
|
+
const at = Date.parse(d.startedAt);
|
|
2353
|
+
if (!d.repoName || !Number.isFinite(at)) continue;
|
|
2354
|
+
const cur = _upSince.get(d.repoName);
|
|
2355
|
+
if (cur === undefined || at < cur) _upSince.set(d.repoName, at);
|
|
2356
|
+
}
|
|
2357
|
+
paintLastRuns();
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
/** A repo's last test run: "✓ 12:03", "✗ failed 12:03", "running…", "—". */
|
|
2276
2361
|
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: "" };
|
|
2362
|
+
const r = _lastRuns[repo];
|
|
2363
|
+
if (!r) return { text: "—", cls: "", title: "" };
|
|
2364
|
+
const title = [r.pillar, r.env].filter(Boolean).join(" · ");
|
|
2365
|
+
if (r.running) return { text: "running…", cls: "", title };
|
|
2284
2366
|
const t = new Intl.DateTimeFormat(undefined, {
|
|
2285
2367
|
hour: "2-digit",
|
|
2286
2368
|
minute: "2-digit",
|
|
2287
|
-
}).format(new Date(r.endedAt ?? r.
|
|
2288
|
-
return r.
|
|
2289
|
-
? { text:
|
|
2290
|
-
: { text:
|
|
2369
|
+
}).format(new Date(r.endedAt ?? r.startedAt));
|
|
2370
|
+
return r.exitCode === 0
|
|
2371
|
+
? { text: `✓ ${t}`, cls: "ok", title }
|
|
2372
|
+
: { text: `✗ failed ${t}`, cls: "bad", title };
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
/** How long a repo's service has run: "up 2h", or "" when it doesn't. */
|
|
2376
|
+
function upText(repo) {
|
|
2377
|
+
const at = _upSince.get(repo);
|
|
2378
|
+
if (at === undefined) return "";
|
|
2379
|
+
const s = Math.max(0, Math.round((Date.now() - at) / 1000));
|
|
2380
|
+
const d =
|
|
2381
|
+
s < 60
|
|
2382
|
+
? `${s}s`
|
|
2383
|
+
: s < 3600
|
|
2384
|
+
? `${Math.floor(s / 60)}m`
|
|
2385
|
+
: s < 86400
|
|
2386
|
+
? `${Math.floor(s / 3600)}h`
|
|
2387
|
+
: `${Math.floor(s / 86400)}d`;
|
|
2388
|
+
return `up ${d}`;
|
|
2291
2389
|
}
|
|
2292
2390
|
|
|
2293
2391
|
/** A health cell: the dot and the rollup, or the failing check. */
|
|
@@ -2335,6 +2433,47 @@
|
|
|
2335
2433
|
);
|
|
2336
2434
|
}
|
|
2337
2435
|
|
|
2436
|
+
/** A service's URL to visit (its edge URL if routed, else its local one), or null. */
|
|
2437
|
+
function visitUrl(svc) {
|
|
2438
|
+
const u = svc?.open;
|
|
2439
|
+
return typeof u === "string" && /^https?:\/\//.test(u) ? u : null;
|
|
2440
|
+
}
|
|
2441
|
+
/** The shell's words (its language), else English. */
|
|
2442
|
+
function shT(key, vars, en) {
|
|
2443
|
+
const v = window.denShell?.t?.(key, vars);
|
|
2444
|
+
return v && v !== key ? v : en;
|
|
2445
|
+
}
|
|
2446
|
+
/** Visit ↗ (0.4.17): opens the service in a new tab; nothing without a URL. */
|
|
2447
|
+
function visitBtn(svc, name) {
|
|
2448
|
+
const url = visitUrl(svc);
|
|
2449
|
+
if (!url) return "";
|
|
2450
|
+
const label = shT(
|
|
2451
|
+
"act.visitX",
|
|
2452
|
+
{ s: name },
|
|
2453
|
+
`Visit ${name} (opens a new tab)`,
|
|
2454
|
+
);
|
|
2455
|
+
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>`;
|
|
2456
|
+
}
|
|
2457
|
+
/** A row's name: a link to visit when the service has a URL. */
|
|
2458
|
+
function nameLink(svc, text) {
|
|
2459
|
+
const url = visitUrl(svc);
|
|
2460
|
+
return url
|
|
2461
|
+
? `<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>`
|
|
2462
|
+
: `<span class="repo-name">${escHtml(text)}</span>`;
|
|
2463
|
+
}
|
|
2464
|
+
/** The favourite star (0.4.17): the shell's pins, the same store as Services. */
|
|
2465
|
+
function starBtn(id) {
|
|
2466
|
+
const sh = window.denShell;
|
|
2467
|
+
if (!sh?.isPinned) return "";
|
|
2468
|
+
const on = sh.isPinned(id);
|
|
2469
|
+
const label = shT(
|
|
2470
|
+
on ? "act.unpinX" : "act.pinX",
|
|
2471
|
+
{ s: id },
|
|
2472
|
+
`${on ? "Unpin" : "Pin"} ${id}`,
|
|
2473
|
+
);
|
|
2474
|
+
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>`;
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2338
2477
|
/** Action buttons; each one's name says what and on which repo or service. */
|
|
2339
2478
|
function actBtns(id, o) {
|
|
2340
2479
|
const b = (act, label, name, extra = "") =>
|
|
@@ -2407,12 +2546,12 @@
|
|
|
2407
2546
|
? `<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
2547
|
: "";
|
|
2409
2548
|
const head = `<tr class="repo${r.problem ? " prob" : ""}" data-repo="${escHtml(repo)}">
|
|
2410
|
-
<th scope="row">${exp}
|
|
2411
|
-
<td>${repoHealth(r)}</td>
|
|
2549
|
+
<th scope="row">${exp}${nameLink(primary, repo)} ${starBtn(repo)}${badges}</th>
|
|
2550
|
+
<td>${repoHealth(r)}<span class="tbl-muted tbl-up" data-up="${escHtml(repo)}">${escHtml(upText(repo))}</span></td>
|
|
2412
2551
|
<td class="tbl-mono tbl-muted">${escHtml(primary?.branch ?? "")}</td>
|
|
2413
2552
|
<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>
|
|
2553
|
+
<td class="tbl-muted${run.cls ? " " + run.cls : ""}" data-last="${escHtml(repo)}" title="${escHtml(run.title)}">${escHtml(run.text)}</td>
|
|
2554
|
+
<td><div class="tbl-acts">${visitBtn(primary, repo)}${actBtns(repo, { up: !!startOf(repo).length, restart: !!startOf(repo).length })}${repoMenu(r)}</div></td>
|
|
2416
2555
|
</tr>`;
|
|
2417
2556
|
const fixes = r.fixes
|
|
2418
2557
|
.map(
|
|
@@ -2434,10 +2573,10 @@
|
|
|
2434
2573
|
s,
|
|
2435
2574
|
i,
|
|
2436
2575
|
) => `<tr class="sub" id="${subIds[i]}" data-repo="${escHtml(repo)}" data-id="${escHtml(s.id)}"${shown(s) ? "" : " hidden"}>
|
|
2437
|
-
<th scope="row"
|
|
2576
|
+
<th scope="row">${nameLink(s, s.name)} ${starBtn(s.id)}${s.entry ? ' <span class="badge">entry</span>' : ""}</th>
|
|
2438
2577
|
<td>${!_lastHealthAt ? CHECKING() : healthCell(status(s), s.health.ok === false ? checkLineOf(s) : healthLine(s))}</td>
|
|
2439
2578
|
<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>
|
|
2579
|
+
<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
2580
|
</tr>`,
|
|
2442
2581
|
)
|
|
2443
2582
|
.join("")
|
|
@@ -2516,13 +2655,17 @@
|
|
|
2516
2655
|
if (keep)
|
|
2517
2656
|
table.querySelector(`[data-k="${CSS.escape(keep)}"]`)?.focus();
|
|
2518
2657
|
}
|
|
2519
|
-
/** Only the Last run cells (the
|
|
2658
|
+
/** Only the Last test run and "up" cells (the history changed). */
|
|
2520
2659
|
function paintLastRuns() {
|
|
2521
2660
|
document.querySelectorAll("#repo-table [data-last]").forEach((td) => {
|
|
2522
2661
|
const run = lastRunText(td.dataset.last);
|
|
2523
2662
|
td.textContent = run.text;
|
|
2663
|
+
td.title = run.title;
|
|
2524
2664
|
td.className = "tbl-muted" + (run.cls ? " " + run.cls : "");
|
|
2525
2665
|
});
|
|
2666
|
+
document.querySelectorAll("#repo-table [data-up]").forEach((el) => {
|
|
2667
|
+
el.textContent = upText(el.dataset.up);
|
|
2668
|
+
});
|
|
2526
2669
|
}
|
|
2527
2670
|
|
|
2528
2671
|
/**
|
|
@@ -2581,6 +2724,14 @@
|
|
|
2581
2724
|
setTableGroup(g.dataset.group);
|
|
2582
2725
|
return;
|
|
2583
2726
|
}
|
|
2727
|
+
const star = e.target.closest("#repo-table [data-pin-toggle]");
|
|
2728
|
+
if (star) {
|
|
2729
|
+
const sh = window.denShell;
|
|
2730
|
+
const id = star.dataset.pinToggle;
|
|
2731
|
+
if (sh?.isPinned(id)) sh.unpin(id);
|
|
2732
|
+
else sh?.pin(id);
|
|
2733
|
+
return;
|
|
2734
|
+
}
|
|
2584
2735
|
const b = e.target.closest("[data-action^='tbl-']");
|
|
2585
2736
|
if (!b || b.disabled) return;
|
|
2586
2737
|
const a = b.dataset.action;
|
|
@@ -2641,8 +2792,20 @@
|
|
|
2641
2792
|
const v = localStorage.getItem(LS_GROUP);
|
|
2642
2793
|
if (["none", "app", "org"].includes(v)) _tblGroup = v;
|
|
2643
2794
|
} catch {}
|
|
2644
|
-
// The Last run column
|
|
2645
|
-
|
|
2795
|
+
// The Last test run column: the server's history, read now, when a
|
|
2796
|
+
// run here changes, and every 30 s for runs from elsewhere.
|
|
2797
|
+
void loadLastRuns();
|
|
2798
|
+
// The dock changes on every line of a run: read at most every 2 s.
|
|
2799
|
+
let _lastRunsSoon = null;
|
|
2800
|
+
window.denShell?.runs?.onChange?.(() => {
|
|
2801
|
+
_lastRunsSoon ??= setTimeout(() => {
|
|
2802
|
+
_lastRunsSoon = null;
|
|
2803
|
+
void loadLastRuns();
|
|
2804
|
+
}, 2_000);
|
|
2805
|
+
});
|
|
2806
|
+
setInterval(() => {
|
|
2807
|
+
if (!document.hidden) void loadLastRuns();
|
|
2808
|
+
}, 30_000);
|
|
2646
2809
|
|
|
2647
2810
|
// ── Tab management (one persistent tab per service address) ───────────────
|
|
2648
2811
|
//
|
|
@@ -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}`
|
|
@@ -62,8 +62,9 @@
|
|
|
62
62
|
* setOrgs([{ id, title, workspaces? }], currentId, onSelect?)
|
|
63
63
|
* two or more: the Org ▾ menu; a pick
|
|
64
64
|
* calls onSelect(id), fires `den:org`
|
|
65
|
-
* setWorkspaces({ label, title, items, focus, onSelect })
|
|
65
|
+
* setWorkspaces({ label, title, items, focus, names, onSelect })
|
|
66
66
|
* the Workspace ▾ menu (header.js)
|
|
67
|
+
* workspaceNames() every workspace's name, from it
|
|
67
68
|
* syncEnv() repaint Env ▾ from #env-sel
|
|
68
69
|
* registerCommand({ id, label, keywords?, kind?, run })
|
|
69
70
|
* a ⌘K palette row (R17, dev.20). label
|
|
@@ -77,6 +78,8 @@
|
|
|
77
78
|
* unregisterCommand(id), commands()
|
|
78
79
|
* openMenu(anchor, menu) a .pop menu below anchor (the scope
|
|
79
80
|
* parts' and the user menu's)
|
|
81
|
+
* topLayer.show(el, anchor, opts) a page's own menu in the top layer
|
|
82
|
+
* (0.4.17), placed by anchor; hide(el)
|
|
80
83
|
*
|
|
81
84
|
* Loading before shell.js: push functions onto window.denShellQueue (an
|
|
82
85
|
* array); each runs with denShell once it exists, and a later push runs at
|
|
@@ -160,6 +163,8 @@
|
|
|
160
163
|
"b.down.one": "{n} down",
|
|
161
164
|
"act.pin": "Pin",
|
|
162
165
|
"act.unpin": "Unpin",
|
|
166
|
+
"act.visit": "Visit",
|
|
167
|
+
"act.visitX": "Visit {s} (opens a new tab)",
|
|
163
168
|
"act.pinX": "Pin {s}",
|
|
164
169
|
"act.unpinX": "Unpin {s}",
|
|
165
170
|
"toast.pinned": "Pinned {s}",
|
|
@@ -220,6 +225,8 @@
|
|
|
220
225
|
"kind.app": "App",
|
|
221
226
|
"kind.action": "Action",
|
|
222
227
|
"kind.setting": "Setting",
|
|
228
|
+
"kind.workspace": "Workspace",
|
|
229
|
+
"pal.goWs": "Go to workspace {ws}",
|
|
223
230
|
"pal.repos": "{n} repos",
|
|
224
231
|
"pal.repos.one": "{n} repo",
|
|
225
232
|
"pal.restart": "Restart {s}",
|
|
@@ -625,6 +632,10 @@
|
|
|
625
632
|
"ag.pc.busy": "It's working. Change after this turn, or stop it now?",
|
|
626
633
|
"ag.pc.after": "After this turn",
|
|
627
634
|
"ag.pc.now": "Stop it now",
|
|
635
|
+
"ag.pc.fresh":
|
|
636
|
+
"{id} is now {p}: nothing to resume yet; started fresh (same conversation id, folder and scope)",
|
|
637
|
+
"ag.freshLine":
|
|
638
|
+
"Nothing to resume yet; started fresh (same conversation id, folder and scope)",
|
|
628
639
|
"ag.pc.done": "{id} is now {p}",
|
|
629
640
|
"ag.pc.pending": "{id} becomes {p} when this turn ends",
|
|
630
641
|
"ag.more": "More for {id}",
|
|
@@ -776,6 +787,9 @@
|
|
|
776
787
|
"This session has ended. Its record stays until it is resumed or removed.",
|
|
777
788
|
"ag.switched": "Switched to {p}",
|
|
778
789
|
"ag.exitedLine": "[the session exited{code}]",
|
|
790
|
+
"ag.refusedUnknown": "the server refused the terminal",
|
|
791
|
+
"ag.refusedStatus": "the server refused the terminal ({status})",
|
|
792
|
+
"ag.goWs": "Open {ws} on Workspaces",
|
|
779
793
|
"ag.lost": "[disconnected: {why}]",
|
|
780
794
|
"ag.reconnect": "Reconnect",
|
|
781
795
|
"ag.noBackend":
|
|
@@ -1411,6 +1425,116 @@
|
|
|
1411
1425
|
return el;
|
|
1412
1426
|
}
|
|
1413
1427
|
|
|
1428
|
+
// ── the top layer (0.4.17): one placement for every menu ─────────────
|
|
1429
|
+
// A menu shown with topShow is in the browser's top layer (a manual
|
|
1430
|
+
// popover), placed by its anchor: below it, above when there's more room
|
|
1431
|
+
// there, kept inside the window. No overflow or stacking ancestor can
|
|
1432
|
+
// clip or cover it, and it stays where it is in the page, so the page's
|
|
1433
|
+
// own click handlers still reach it. It closes when the page hides it
|
|
1434
|
+
// (display: none), its anchor goes, or topHide.
|
|
1435
|
+
var topOpen = [];
|
|
1436
|
+
var TOP_MARGIN = 8;
|
|
1437
|
+
function topSupported(el) {
|
|
1438
|
+
return typeof el.showPopover === "function";
|
|
1439
|
+
}
|
|
1440
|
+
function topPlace(el, anchor, opts) {
|
|
1441
|
+
var r = anchor.getBoundingClientRect();
|
|
1442
|
+
var gap = opts.gap != null ? opts.gap : 4;
|
|
1443
|
+
el.style.maxHeight = "";
|
|
1444
|
+
var w = el.offsetWidth;
|
|
1445
|
+
var h = el.offsetHeight;
|
|
1446
|
+
var vw = window.innerWidth;
|
|
1447
|
+
var vh = window.innerHeight;
|
|
1448
|
+
var below = vh - r.bottom - gap - TOP_MARGIN;
|
|
1449
|
+
var above = r.top - gap - TOP_MARGIN;
|
|
1450
|
+
var up = h > below && above > below;
|
|
1451
|
+
var room = Math.max(80, up ? above : below);
|
|
1452
|
+
if (h > room) {
|
|
1453
|
+
el.style.maxHeight = room + "px";
|
|
1454
|
+
h = room;
|
|
1455
|
+
}
|
|
1456
|
+
var top = up ? r.top - gap - h : r.bottom + gap;
|
|
1457
|
+
var left = opts.align === "right" ? r.right - w : r.left;
|
|
1458
|
+
left = Math.max(TOP_MARGIN, Math.min(left, vw - w - TOP_MARGIN));
|
|
1459
|
+
// Important, so no page or UA rule (a popover's inset) moves it.
|
|
1460
|
+
el.style.setProperty("top", Math.max(TOP_MARGIN, top) + "px", "important");
|
|
1461
|
+
el.style.setProperty("left", left + "px", "important");
|
|
1462
|
+
}
|
|
1463
|
+
function topIndex(el) {
|
|
1464
|
+
for (var i = 0; i < topOpen.length; i++) if (topOpen[i].el === el) return i;
|
|
1465
|
+
return -1;
|
|
1466
|
+
}
|
|
1467
|
+
function topShow(el, anchor, opts) {
|
|
1468
|
+
if (!el || !anchor) return;
|
|
1469
|
+
opts = opts || {};
|
|
1470
|
+
var i = topIndex(el);
|
|
1471
|
+
if (i >= 0) {
|
|
1472
|
+
topOpen[i].anchor = anchor;
|
|
1473
|
+
topOpen[i].opts = opts;
|
|
1474
|
+
} else {
|
|
1475
|
+
if (topSupported(el)) {
|
|
1476
|
+
if (!el.hasAttribute("popover")) {
|
|
1477
|
+
el.setAttribute("popover", "manual");
|
|
1478
|
+
el.setAttribute("data-den-top-added", "");
|
|
1479
|
+
}
|
|
1480
|
+
try {
|
|
1481
|
+
if (!el.matches(":popover-open")) el.showPopover();
|
|
1482
|
+
} catch (e) {
|
|
1483
|
+
/* not in the document yet: placed as fixed */
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
var o = { el: el, anchor: anchor, opts: opts, mo: null };
|
|
1487
|
+
if (typeof MutationObserver === "function") {
|
|
1488
|
+
o.mo = new MutationObserver(function () {
|
|
1489
|
+
topCheck(o);
|
|
1490
|
+
});
|
|
1491
|
+
o.mo.observe(el, {
|
|
1492
|
+
attributes: true,
|
|
1493
|
+
attributeFilter: ["style", "class", "hidden"],
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
topOpen.push(o);
|
|
1497
|
+
}
|
|
1498
|
+
el.classList.add("den-toplayer");
|
|
1499
|
+
topPlace(el, anchor, opts);
|
|
1500
|
+
}
|
|
1501
|
+
function topHide(el) {
|
|
1502
|
+
var i = topIndex(el);
|
|
1503
|
+
if (i < 0) return;
|
|
1504
|
+
var o = topOpen.splice(i, 1)[0];
|
|
1505
|
+
if (o.mo) o.mo.disconnect();
|
|
1506
|
+
try {
|
|
1507
|
+
if (el.matches && el.matches(":popover-open")) el.hidePopover();
|
|
1508
|
+
} catch (e) {
|
|
1509
|
+
/* already gone */
|
|
1510
|
+
}
|
|
1511
|
+
if (el.hasAttribute("data-den-top-added")) {
|
|
1512
|
+
el.removeAttribute("popover");
|
|
1513
|
+
el.removeAttribute("data-den-top-added");
|
|
1514
|
+
}
|
|
1515
|
+
el.classList.remove("den-toplayer");
|
|
1516
|
+
el.style.removeProperty("top");
|
|
1517
|
+
el.style.removeProperty("left");
|
|
1518
|
+
}
|
|
1519
|
+
/** The page hid it, or its anchor went: it's closed. */
|
|
1520
|
+
function topCheck(o) {
|
|
1521
|
+
if (
|
|
1522
|
+
!o.el.isConnected ||
|
|
1523
|
+
!o.anchor.isConnected ||
|
|
1524
|
+
o.el.hidden ||
|
|
1525
|
+
getComputedStyle(o.el).display === "none"
|
|
1526
|
+
)
|
|
1527
|
+
topHide(o.el);
|
|
1528
|
+
}
|
|
1529
|
+
function topReplace() {
|
|
1530
|
+
topOpen.slice().forEach(function (o) {
|
|
1531
|
+
topCheck(o);
|
|
1532
|
+
if (topIndex(o.el) >= 0) topPlace(o.el, o.anchor, o.opts);
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
window.addEventListener("resize", topReplace);
|
|
1536
|
+
document.addEventListener("scroll", topReplace, true);
|
|
1537
|
+
|
|
1414
1538
|
// ── menus (.pop, R18): the scope parts' popovers ─────────────────────
|
|
1415
1539
|
// A menu is { label, items: [{ heading } | { hr: true } | { label, tag?,
|
|
1416
1540
|
// checked?, href?, onSelect? }] }. Rows with `checked` are radios (✓ when
|
|
@@ -1422,6 +1546,7 @@
|
|
|
1422
1546
|
if (!openPop) return;
|
|
1423
1547
|
var o = openPop;
|
|
1424
1548
|
openPop = null;
|
|
1549
|
+
topHide(o.el);
|
|
1425
1550
|
if (o.el.remove) o.el.remove();
|
|
1426
1551
|
o.anchor.setAttribute("aria-expanded", "false");
|
|
1427
1552
|
// aria-controls names the menu only while it exists.
|
|
@@ -1497,15 +1622,14 @@
|
|
|
1497
1622
|
}
|
|
1498
1623
|
});
|
|
1499
1624
|
pop.innerHTML = html;
|
|
1500
|
-
|
|
1625
|
+
// Inside a modal, in it: anything outside a modal is inert.
|
|
1626
|
+
var host =
|
|
1627
|
+
(anchor.closest && anchor.closest("dialog[open]")) || document.body;
|
|
1628
|
+
host.appendChild(pop);
|
|
1501
1629
|
openPop = { el: pop, anchor: anchor };
|
|
1502
1630
|
anchor.setAttribute("aria-expanded", "true");
|
|
1503
1631
|
anchor.setAttribute("aria-controls", pop.id);
|
|
1504
|
-
|
|
1505
|
-
var w = pop.offsetWidth;
|
|
1506
|
-
pop.style.top = r.bottom + 6 + "px";
|
|
1507
|
-
pop.style.left =
|
|
1508
|
-
Math.max(8, Math.min(r.left, window.innerWidth - w - 8)) + "px";
|
|
1632
|
+
topShow(pop, anchor, { gap: 6 });
|
|
1509
1633
|
pop.addEventListener("click", function (e) {
|
|
1510
1634
|
var row = e.target.closest ? e.target.closest(".den-mi") : null;
|
|
1511
1635
|
if (!row) return;
|
|
@@ -1943,7 +2067,14 @@
|
|
|
1943
2067
|
* a link to manage them. onSelect(value, focus): choosing a workspace
|
|
1944
2068
|
* also makes it the CLI default (header.js).
|
|
1945
2069
|
*/
|
|
2070
|
+
// Every workspace's name, from the Workspace menu's model (header.js).
|
|
2071
|
+
var workspaceNameList = [];
|
|
2072
|
+
function workspaceNames() {
|
|
2073
|
+
return workspaceNameList.slice();
|
|
2074
|
+
}
|
|
1946
2075
|
function setWorkspaces(model) {
|
|
2076
|
+
workspaceNameList =
|
|
2077
|
+
model && Array.isArray(model.names) ? model.names.slice() : [];
|
|
1947
2078
|
var btn = document.getElementById("ws-pill");
|
|
1948
2079
|
if (!btn) return;
|
|
1949
2080
|
var sep = btn.nextElementSibling;
|
|
@@ -4377,6 +4508,7 @@
|
|
|
4377
4508
|
setOrg: setOrg,
|
|
4378
4509
|
setOrgs: setOrgs,
|
|
4379
4510
|
setWorkspaces: setWorkspaces,
|
|
4511
|
+
workspaceNames: workspaceNames,
|
|
4380
4512
|
/** Repaint the Env part from #env-sel (after setting its value). */
|
|
4381
4513
|
syncEnv: syncEnv,
|
|
4382
4514
|
syncPinToggles: syncPinToggles,
|
|
@@ -4385,6 +4517,12 @@
|
|
|
4385
4517
|
commands: commands,
|
|
4386
4518
|
/** A .pop menu below `anchor` (the model as the scope menus'). */
|
|
4387
4519
|
openMenu: openMenu,
|
|
4520
|
+
/**
|
|
4521
|
+
* The top layer (0.4.17): show(el, anchor, { align: "left"|"right",
|
|
4522
|
+
* gap }) puts a page's own menu above everything, placed by its
|
|
4523
|
+
* anchor; hide(el). The page hiding it (display: none) closes it too.
|
|
4524
|
+
*/
|
|
4525
|
+
topLayer: { show: topShow, hide: topHide },
|
|
4388
4526
|
closeMenu: function () {
|
|
4389
4527
|
closeMenu(false);
|
|
4390
4528
|
},
|
|
@@ -4544,6 +4682,13 @@
|
|
|
4544
4682
|
" max-height: calc(100vh - 72px); overflow-y: auto; box-sizing: border-box;",
|
|
4545
4683
|
" font: 14px/1.45 var(--sans); color: var(--text); }",
|
|
4546
4684
|
".den-pop:focus { outline: none; }",
|
|
4685
|
+
// The top layer (0.4.17): a page's menu placed by its anchor (topShow), not
|
|
4686
|
+
// where the UA puts a popover.
|
|
4687
|
+
// No inset here: top and left are topPlace's (an !important inset beat them,
|
|
4688
|
+
// and every menu sat at the window's corner, 0.4.17-dev.0).
|
|
4689
|
+
".den-toplayer { position: fixed !important; margin: 0 !important;",
|
|
4690
|
+
" right: auto !important; bottom: auto !important; z-index: 1000; overflow-y: auto;",
|
|
4691
|
+
" color: var(--text); }",
|
|
4547
4692
|
// LK-1: the admin-password prompt is a <dialog> in the top layer (a popover, or
|
|
4548
4693
|
// a modal over another modal): placed by its top/left, no UA box.
|
|
4549
4694
|
"dialog.den-pop { margin: 0; inset: auto; max-width: none; }",
|