@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
|
@@ -145,6 +145,21 @@
|
|
|
145
145
|
.dot.slow {
|
|
146
146
|
background: var(--warn);
|
|
147
147
|
}
|
|
148
|
+
/* 0.4.17: a check in flight; its row is there before it lands. */
|
|
149
|
+
.dot.checking {
|
|
150
|
+
animation: env-pulse 1.2s ease-in-out infinite;
|
|
151
|
+
}
|
|
152
|
+
@keyframes env-pulse {
|
|
153
|
+
50% {
|
|
154
|
+
opacity: 0.25;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
@media (prefers-reduced-motion: reduce) {
|
|
158
|
+
.dot.checking {
|
|
159
|
+
animation: none;
|
|
160
|
+
opacity: 0.5;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
148
163
|
.empty {
|
|
149
164
|
color: var(--muted);
|
|
150
165
|
padding: 24px 0;
|
|
@@ -212,13 +227,8 @@
|
|
|
212
227
|
<main id="den-content">
|
|
213
228
|
<div class="controls">
|
|
214
229
|
<button id="refresh">Re-check</button>
|
|
215
|
-
<
|
|
216
|
-
|
|
217
|
-
<select id="org-filter">
|
|
218
|
-
<option value="">all</option>
|
|
219
|
-
</select></label
|
|
220
|
-
>
|
|
221
|
-
<span class="note" id="status">checking…</span>
|
|
230
|
+
<span id="view-note"></span>
|
|
231
|
+
<span class="note" id="status" aria-live="polite">checking…</span>
|
|
222
232
|
<span class="note"
|
|
223
233
|
>Each repo's <code>__DEN_MANIFEST_YML__</code> health checks, run
|
|
224
234
|
against every environment. ↗ opens the service in a new tab. beta and
|
|
@@ -247,6 +257,7 @@
|
|
|
247
257
|
initLogBadge,
|
|
248
258
|
initWorkspacePill,
|
|
249
259
|
} from "/header.js";
|
|
260
|
+
import { viewNoteHtml, viewQuery, viewScope } from "/view-scope.js";
|
|
250
261
|
void Promise.all([
|
|
251
262
|
initHealth(),
|
|
252
263
|
initWorkspacePill(),
|
|
@@ -306,12 +317,13 @@
|
|
|
306
317
|
: r.ok === false
|
|
307
318
|
? "err"
|
|
308
319
|
: "";
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
?
|
|
313
|
-
: r.error
|
|
314
|
-
const
|
|
320
|
+
// A check that ran out of time says how long it waited (0.4.17).
|
|
321
|
+
const why =
|
|
322
|
+
r.error === "timeout" && r.ms
|
|
323
|
+
? `timed out after ${Math.max(1, Math.round(r.ms / 1000))} s`
|
|
324
|
+
: r.error;
|
|
325
|
+
const what = r.status ? String(r.status) : r.ok ? "up" : why || "down";
|
|
326
|
+
const title = [r.label, checked, why].filter(Boolean).join(" — ");
|
|
315
327
|
const link =
|
|
316
328
|
r.url && /^https?:/.test(r.url)
|
|
317
329
|
? `<a href="${esc(r.url)}" target="_blank" rel="noopener" title="${esc(title)}">${esc(what)}</a>`
|
|
@@ -320,157 +332,228 @@
|
|
|
320
332
|
return `<span class="cell"><span class="dot ${cls}"></span>${link}${ms}${openLink(r.openUrl, `${r.repo}/${r.service}`)}</span>`;
|
|
321
333
|
}
|
|
322
334
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
+
// 0.4.17: every row at once, each cell "checking" until its result
|
|
336
|
+
// streams in (/api/health/stream, one per environment, in parallel);
|
|
337
|
+
// the page's controls never wait. The rows are the View's (the top
|
|
338
|
+
// bar's Org and Workspace): this page doesn't filter by org itself.
|
|
339
|
+
const checking = () =>
|
|
340
|
+
'<span class="cell" data-checking><span class="dot checking"></span><span class="none">checking…</span></span>';
|
|
341
|
+
const st = {
|
|
342
|
+
gen: 0,
|
|
343
|
+
envs: [],
|
|
344
|
+
rows: new Map(),
|
|
345
|
+
orgInfo: {},
|
|
346
|
+
release: {},
|
|
347
|
+
streams: [],
|
|
348
|
+
pending: new Set(),
|
|
349
|
+
view: { scope: "all", inView: [] },
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
function stopStreams() {
|
|
353
|
+
for (const es of st.streams) es.close();
|
|
354
|
+
st.streams = [];
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function sortedRows() {
|
|
358
|
+
const orgRank = (o) => {
|
|
359
|
+
const i = Object.keys(st.orgInfo).indexOf(o);
|
|
360
|
+
return i < 0 ? 1e6 : i;
|
|
361
|
+
};
|
|
362
|
+
return [...st.rows.values()].sort(
|
|
363
|
+
(a, b) =>
|
|
364
|
+
orgRank(a.org) - orgRank(b.org) ||
|
|
365
|
+
a.idx - b.idx ||
|
|
366
|
+
a.service.localeCompare(b.service),
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function cellHtml(row, env) {
|
|
371
|
+
const r = row.byEnv[env];
|
|
372
|
+
if (r) return cell(r);
|
|
373
|
+
return st.pending.has(env) ? checking() : '<span class="none">—</span>';
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function render() {
|
|
377
|
+
const envs = st.envs;
|
|
378
|
+
document.getElementById("head").innerHTML =
|
|
379
|
+
`<tr><th>Service</th>${envs.map((e) => `<th>${esc(e)}</th>`).join("")}</tr>`;
|
|
380
|
+
const body = document.getElementById("body");
|
|
381
|
+
const sorted = sortedRows();
|
|
382
|
+
if (!sorted.length) {
|
|
383
|
+
body.innerHTML = st.pending.size
|
|
384
|
+
? `<tr><td class="empty" colspan="${envs.length + 1}">Loading…</td></tr>`
|
|
385
|
+
: `<tr><td class="empty" colspan="${envs.length + 1}">No services in view have a __DEN_MANIFEST_YML__ yet.</td></tr>`;
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
let html = "",
|
|
389
|
+
lastRepo = null,
|
|
390
|
+
lastOrg = null;
|
|
391
|
+
for (const row of sorted) {
|
|
392
|
+
if (row.org !== lastOrg) {
|
|
393
|
+
const o = st.orgInfo[row.org];
|
|
394
|
+
html += `<tr class="org" style="${o?.color ? `--org:${esc(o.color)}` : ""}"><td colspan="${envs.length + 1}"><span class="swatch"></span>${esc(o?.label || row.org || "no org")}</td></tr>`;
|
|
395
|
+
lastOrg = row.org;
|
|
396
|
+
}
|
|
397
|
+
if (row.repo !== lastRepo) {
|
|
398
|
+
const entry =
|
|
399
|
+
sorted.find((r) => r.repo === row.repo && r.isEntrypoint) || row;
|
|
400
|
+
html += `<tr class="repo" data-repo="${esc(row.repo)}"><td>${esc(row.repo)}</td>${envs
|
|
401
|
+
.map(
|
|
402
|
+
(e) =>
|
|
403
|
+
`<td class="rel">${relCell(st.release[row.repo]?.[e])}${openLink(entry.byEnv[e]?.openUrl, `${row.repo} on ${e}`)}</td>`,
|
|
335
404
|
)
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
),
|
|
405
|
+
.join("")}</tr>`;
|
|
406
|
+
lastRepo = row.repo;
|
|
407
|
+
}
|
|
408
|
+
html += `<tr data-svc="${esc(row.repo)}/${esc(row.service)}"><td class="svc">${esc(row.service)}</td>${envs.map((e) => `<td data-env="${esc(e)}">${cellHtml(row, e)}</td>`).join("")}</tr>`;
|
|
409
|
+
}
|
|
410
|
+
body.innerHTML = html;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Results land many per second: paint once per frame.
|
|
414
|
+
let painting = false;
|
|
415
|
+
function paintSoon() {
|
|
416
|
+
if (painting) return;
|
|
417
|
+
painting = true;
|
|
418
|
+
requestAnimationFrame(() => {
|
|
419
|
+
painting = false;
|
|
420
|
+
render();
|
|
421
|
+
paintStatus();
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function paintStatus() {
|
|
426
|
+
const status = document.getElementById("status");
|
|
427
|
+
const all = [...st.rows.values()].flatMap((r) => Object.values(r.byEnv));
|
|
428
|
+
const down = all.filter((r) => r.ok === false).length;
|
|
429
|
+
status.textContent = st.pending.size
|
|
430
|
+
? `checking ${st.pending.size} environment${st.pending.size === 1 ? "" : "s"}…${down ? ` · ${down} failing so far` : ""}`
|
|
431
|
+
: `checked ${new Date().toLocaleTimeString()} · ${down ? down + " failing" : "nothing failing"}`;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function upsert(r, env, orgOfRepo) {
|
|
435
|
+
const key = `${r.repo}/${r.service}`;
|
|
436
|
+
let row = st.rows.get(key);
|
|
437
|
+
if (!row) {
|
|
438
|
+
row = {
|
|
439
|
+
repo: r.repo,
|
|
440
|
+
service: r.service,
|
|
441
|
+
label: r.label,
|
|
442
|
+
org: orgOfRepo[r.repo] || "",
|
|
443
|
+
isEntrypoint: !!r.isEntrypoint,
|
|
444
|
+
idx: r.idx ?? 1e9,
|
|
445
|
+
seeded: false,
|
|
446
|
+
byEnv: {},
|
|
447
|
+
};
|
|
448
|
+
st.rows.set(key, row);
|
|
449
|
+
}
|
|
450
|
+
if (!row.seeded && r.idx != null) row.idx = Math.min(row.idx, r.idx);
|
|
451
|
+
if (r.isEntrypoint) row.isEntrypoint = true;
|
|
452
|
+
if (env) row.byEnv[env] = r;
|
|
453
|
+
return row;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async function load(refresh = false) {
|
|
457
|
+
const gen = ++st.gen;
|
|
458
|
+
stopStreams();
|
|
459
|
+
st.view = await viewScope();
|
|
460
|
+
document.getElementById("view-note").innerHTML = viewNoteHtml(st.view);
|
|
461
|
+
const q = viewQuery(st.view);
|
|
462
|
+
const [envs, orgs, relCfg, seed] = await Promise.all([
|
|
463
|
+
fetch("/api/environments")
|
|
464
|
+
.then((r) => r.json())
|
|
465
|
+
.then((v) => (Array.isArray(v) && v.length ? v : ["local"]))
|
|
466
|
+
.catch(() => ["local"]),
|
|
340
467
|
// The org of every repo comes from the catalog (/api/orgs?repos=1):
|
|
341
|
-
// /api/health rows never carry one
|
|
342
|
-
// service under "no org" (0.3.0-dev.14).
|
|
468
|
+
// /api/health rows never carry one (0.3.0-dev.14).
|
|
343
469
|
fetch("/api/orgs?repos=1")
|
|
344
470
|
.then((r) => r.json())
|
|
345
471
|
.catch(() => ({ orgs: [], repos: {} })),
|
|
346
472
|
fetch("/api/release/config")
|
|
347
473
|
.then((r) => r.json())
|
|
348
474
|
.catch(() => ({ environments: [] })),
|
|
475
|
+
// The rows, before any check: the View's services.
|
|
476
|
+
fetch(`/api/services?env=local${q}`)
|
|
477
|
+
.then((r) => (r.ok ? r.json() : []))
|
|
478
|
+
.catch(() => []),
|
|
349
479
|
]);
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
);
|
|
354
|
-
const release = {};
|
|
355
|
-
await Promise.all(
|
|
356
|
-
relEnvs.map(async (e) => {
|
|
357
|
-
const r = await fetch(
|
|
358
|
-
`/api/release/status?env=${encodeURIComponent(e)}${refresh ? "&refresh=1" : ""}`,
|
|
359
|
-
)
|
|
360
|
-
.then((x) => (x.ok ? x.json() : null))
|
|
361
|
-
.catch(() => null);
|
|
362
|
-
for (const row of r?.rows || [])
|
|
363
|
-
(release[row.repo] ??= {})[e] = row;
|
|
364
|
-
}),
|
|
365
|
-
);
|
|
366
|
-
const orgInfo = Object.fromEntries(
|
|
480
|
+
if (gen !== st.gen) return;
|
|
481
|
+
st.envs = envs;
|
|
482
|
+
st.orgInfo = Object.fromEntries(
|
|
367
483
|
(Array.isArray(orgs?.orgs) ? orgs.orgs : []).map((o) => [o.id, o]),
|
|
368
484
|
);
|
|
369
485
|
const orgOfRepo = orgs?.repos ?? {};
|
|
370
|
-
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
.
|
|
382
|
-
.join("");
|
|
383
|
-
filterSel.value = orgInfo[filterSel.dataset.value ?? saved]
|
|
384
|
-
? (filterSel.dataset.value ?? saved)
|
|
385
|
-
: "";
|
|
386
|
-
const orgFilter = filterSel.value;
|
|
387
|
-
|
|
388
|
-
// One row per service, in the dependency-graph order the server ranks
|
|
389
|
-
// (idx); grouped under its repo.
|
|
390
|
-
const rows = new Map();
|
|
391
|
-
results.forEach((list, i) => {
|
|
392
|
-
for (const r of Array.isArray(list) ? list : []) {
|
|
393
|
-
const key = `${r.repo}/${r.service}`;
|
|
394
|
-
const row = rows.get(key) ?? {
|
|
395
|
-
repo: r.repo,
|
|
396
|
-
service: r.service,
|
|
397
|
-
label: r.label,
|
|
398
|
-
org: orgOfRepo[r.repo] || "",
|
|
399
|
-
isEntrypoint: r.isEntrypoint,
|
|
400
|
-
idx: r.idx ?? 1e9,
|
|
401
|
-
byEnv: {},
|
|
402
|
-
};
|
|
403
|
-
row.idx = Math.min(row.idx, r.idx ?? 1e9);
|
|
404
|
-
row.byEnv[envs[i]] = r;
|
|
405
|
-
rows.set(key, row);
|
|
486
|
+
// Every cell checks again; the rows stay.
|
|
487
|
+
for (const row of st.rows.values()) row.byEnv = {};
|
|
488
|
+
(Array.isArray(seed) ? seed : []).forEach((s, i) => {
|
|
489
|
+
if (!s?.repo || !s?.service) return;
|
|
490
|
+
const row = upsert(
|
|
491
|
+
{ repo: s.repo, service: s.service, isEntrypoint: s.isEntrypoint },
|
|
492
|
+
null,
|
|
493
|
+
orgOfRepo,
|
|
494
|
+
);
|
|
495
|
+
if (!row.seeded) {
|
|
496
|
+
row.seeded = true;
|
|
497
|
+
row.idx = i;
|
|
406
498
|
}
|
|
407
499
|
});
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
.filter((r) => !orgFilter || r.org === orgFilter)
|
|
415
|
-
.sort(
|
|
416
|
-
(a, b) =>
|
|
417
|
-
orgRank(a.org) - orgRank(b.org) ||
|
|
418
|
-
a.idx - b.idx ||
|
|
419
|
-
a.service.localeCompare(b.service),
|
|
500
|
+
st.pending = new Set(envs);
|
|
501
|
+
render();
|
|
502
|
+
paintStatus();
|
|
503
|
+
for (const env of envs) {
|
|
504
|
+
const es = new EventSource(
|
|
505
|
+
`/api/health/stream?env=${encodeURIComponent(env)}${q}${refresh ? "&refresh=1" : ""}`,
|
|
420
506
|
);
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
sorted.find((r) => r.repo === row.repo && r.isEntrypoint) ||
|
|
440
|
-
row;
|
|
441
|
-
html += `<tr class="repo"><td>${esc(row.repo)}</td>${envs
|
|
442
|
-
.map(
|
|
443
|
-
(e) =>
|
|
444
|
-
`<td class="rel">${relCell(release[row.repo]?.[e])}${openLink(entry.byEnv[e]?.openUrl, `${row.repo} on ${e}`)}</td>`,
|
|
445
|
-
)
|
|
446
|
-
.join("")}</tr>`;
|
|
447
|
-
lastRepo = row.repo;
|
|
507
|
+
st.streams.push(es);
|
|
508
|
+
const finish = () => {
|
|
509
|
+
es.close();
|
|
510
|
+
if (gen !== st.gen) return;
|
|
511
|
+
st.pending.delete(env);
|
|
512
|
+
if (!st.pending.size)
|
|
513
|
+
// Seeded rows no check reached (a module, a library): gone.
|
|
514
|
+
for (const [k, row] of st.rows)
|
|
515
|
+
if (!Object.keys(row.byEnv).length) st.rows.delete(k);
|
|
516
|
+
paintSoon();
|
|
517
|
+
};
|
|
518
|
+
es.onmessage = (e) => {
|
|
519
|
+
if (gen !== st.gen) return es.close();
|
|
520
|
+
let m = null;
|
|
521
|
+
try {
|
|
522
|
+
m = JSON.parse(e.data);
|
|
523
|
+
} catch {
|
|
524
|
+
return;
|
|
448
525
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
526
|
+
if (m?.type === "result" && m.result) {
|
|
527
|
+
upsert(m.result, env, orgOfRepo);
|
|
528
|
+
paintSoon();
|
|
529
|
+
} else if (m?.type === "done" || m?.type === "error") finish();
|
|
530
|
+
};
|
|
531
|
+
es.onerror = finish;
|
|
452
532
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
533
|
+
// Deployed commit per repo for the release environments, as each
|
|
534
|
+
// lands; nothing waits on it.
|
|
535
|
+
for (const e of envs.filter((x) =>
|
|
536
|
+
(relCfg.environments || []).includes(x),
|
|
537
|
+
))
|
|
538
|
+
fetch(
|
|
539
|
+
`/api/release/status?env=${encodeURIComponent(e)}${refresh ? "&refresh=1" : ""}`,
|
|
540
|
+
)
|
|
541
|
+
.then((x) => (x.ok ? x.json() : null))
|
|
542
|
+
.catch(() => null)
|
|
543
|
+
.then((r) => {
|
|
544
|
+
if (gen !== st.gen) return;
|
|
545
|
+
for (const row of r?.rows || [])
|
|
546
|
+
(st.release[row.repo] ??= {})[e] = row;
|
|
547
|
+
paintSoon();
|
|
548
|
+
});
|
|
457
549
|
}
|
|
458
550
|
|
|
459
551
|
document
|
|
460
552
|
.getElementById("refresh")
|
|
461
553
|
.addEventListener("click", () => load(true));
|
|
462
|
-
document.getElementById("org-filter").addEventListener("change", (e) => {
|
|
463
|
-
e.target.dataset.value = e.target.value;
|
|
464
|
-
try {
|
|
465
|
-
localStorage.setItem("__DEN_CLI__-env-org", e.target.value);
|
|
466
|
-
} catch {
|
|
467
|
-
/* private window */
|
|
468
|
-
}
|
|
469
|
-
load();
|
|
470
|
-
});
|
|
471
554
|
load();
|
|
472
555
|
setInterval(() => {
|
|
473
|
-
if (!document.hidden) load();
|
|
556
|
+
if (!document.hidden && !st.pending.size) load();
|
|
474
557
|
}, POLL_MS);
|
|
475
558
|
</script>
|
|
476
559
|
</main>
|
|
@@ -331,6 +331,46 @@
|
|
|
331
331
|
.dg-card .r1 .dg-dot {
|
|
332
332
|
margin-top: 5px;
|
|
333
333
|
}
|
|
334
|
+
/* The favourite star (0.4.17): top-right, the shell's pins. */
|
|
335
|
+
.dg-star {
|
|
336
|
+
margin-left: auto;
|
|
337
|
+
flex: none;
|
|
338
|
+
background: none;
|
|
339
|
+
border: 1px solid transparent;
|
|
340
|
+
border-radius: 6px;
|
|
341
|
+
color: var(--faint);
|
|
342
|
+
cursor: pointer;
|
|
343
|
+
font-size: 13px;
|
|
344
|
+
line-height: 1.2;
|
|
345
|
+
padding: 0 3px;
|
|
346
|
+
}
|
|
347
|
+
.dg-star:hover,
|
|
348
|
+
.dg-star[aria-pressed="true"] {
|
|
349
|
+
color: var(--accent-text);
|
|
350
|
+
background: var(--sunken);
|
|
351
|
+
}
|
|
352
|
+
/* 0.4.18: ⋯ opens the service panel (svc-panel.js), as a pinned row does.
|
|
353
|
+
It sits before the star, which stays top-right. */
|
|
354
|
+
.dg-svp {
|
|
355
|
+
margin-left: auto;
|
|
356
|
+
flex: none;
|
|
357
|
+
background: none;
|
|
358
|
+
border: 1px solid transparent;
|
|
359
|
+
border-radius: 6px;
|
|
360
|
+
color: var(--faint);
|
|
361
|
+
cursor: pointer;
|
|
362
|
+
font-size: 13px;
|
|
363
|
+
line-height: 1;
|
|
364
|
+
padding: 1px 4px;
|
|
365
|
+
}
|
|
366
|
+
.dg-svp + .dg-star {
|
|
367
|
+
margin-left: 0;
|
|
368
|
+
}
|
|
369
|
+
.dg-svp:hover,
|
|
370
|
+
.dg-svp[aria-expanded="true"] {
|
|
371
|
+
color: var(--accent-text);
|
|
372
|
+
background: var(--sunken);
|
|
373
|
+
}
|
|
334
374
|
.dg-dot {
|
|
335
375
|
display: inline-block;
|
|
336
376
|
width: 9px;
|
|
@@ -256,9 +256,26 @@ export function mountGraph(el, data, pageHooks) {
|
|
|
256
256
|
st.fitted = true;
|
|
257
257
|
} else applyView();
|
|
258
258
|
if (st.sel) openDetail(st.sel, true);
|
|
259
|
+
focusFromHash();
|
|
259
260
|
});
|
|
260
261
|
}
|
|
261
262
|
|
|
263
|
+
/** 0.4.18: the panel's Open in Graph (#focus=<repo> or <repo>/<service>), once. */
|
|
264
|
+
let hashFocused = false;
|
|
265
|
+
function focusFromHash() {
|
|
266
|
+
if (hashFocused || !location.hash.startsWith("#focus=")) return;
|
|
267
|
+
let id = "";
|
|
268
|
+
try {
|
|
269
|
+
id = decodeURIComponent(location.hash.slice(7));
|
|
270
|
+
} catch {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (M.byId.has(id)) focusOn({ id });
|
|
274
|
+
else if (M.byRepo.has(id)) focusOn({ repo: id, viaRepo: true });
|
|
275
|
+
else return;
|
|
276
|
+
hashFocused = true;
|
|
277
|
+
}
|
|
278
|
+
|
|
262
279
|
/** A /api/health update: refresh dots, health lines and the open panel in place. */
|
|
263
280
|
export function graphHealth(results) {
|
|
264
281
|
if (!M || !host?.isConnected) return;
|
|
@@ -501,15 +518,38 @@ function repoGroupEl(r, x, y, w, h) {
|
|
|
501
518
|
el.innerHTML = `<div class="dg-ghead"><button class="dg-gname" data-goto-repo-panel="${esc(r)}" title="Repo details">${esc(r)}</button><span class="meta">${esc(branch)}${svcs.length} services</span><button class="dg-lnk" data-collapse="${esc(r)}">Collapse</button></div>`;
|
|
502
519
|
return place(el, x, y, w, h);
|
|
503
520
|
}
|
|
521
|
+
/** The favourite star (0.4.17): the shell's pins, the same store as Services. */
|
|
522
|
+
function starBtn(id) {
|
|
523
|
+
const sh = window.denShell;
|
|
524
|
+
if (!sh?.isPinned) return "";
|
|
525
|
+
const on = sh.isPinned(id);
|
|
526
|
+
const label = sh.t
|
|
527
|
+
? sh.t(on ? "act.unpinX" : "act.pinX", { s: id })
|
|
528
|
+
: `${on ? "Unpin" : "Pin"} ${id}`;
|
|
529
|
+
return `<button type="button" class="dg-star" data-pin-toggle="${esc(id)}" aria-pressed="${on}" aria-label="${esc(label)}" title="${esc(label)}">${on ? "★" : "☆"}</button>`;
|
|
530
|
+
}
|
|
531
|
+
/** 0.4.18: ⋯ opens the service's quick panel (svc-panel.js), the rail's. */
|
|
532
|
+
function svpBtn(id) {
|
|
533
|
+
const sh = window.denShell;
|
|
534
|
+
if (!sh?.openServicePanel) return "";
|
|
535
|
+
const label = sh.t ? sh.t("svp.more", { s: id }) : id;
|
|
536
|
+
return `<button type="button" class="dg-svp" data-svpanel-open="${esc(id)}" aria-haspopup="dialog" aria-expanded="false" aria-label="${esc(label)}" title="${esc(label)}">⋯</button>`;
|
|
537
|
+
}
|
|
538
|
+
/** A card's actions: the first three, and Visit ↗ always when it has a URL. */
|
|
539
|
+
function cardActs(s) {
|
|
540
|
+
const acts = actionsFor(s);
|
|
541
|
+
const list = [acts.primary, ...acts.common].filter(Boolean);
|
|
542
|
+
const top = list.slice(0, 3);
|
|
543
|
+
const visit = list.find((a) => a.id === "open");
|
|
544
|
+
if (visit && !top.includes(visit)) top[top.length - 1] = visit;
|
|
545
|
+
return top;
|
|
546
|
+
}
|
|
504
547
|
function cardInner(s) {
|
|
505
548
|
const showPlace = s.runs === "remote" || M.mixed;
|
|
506
|
-
|
|
507
|
-
return `<div class="r1"><span class="dg-dot ${status(s)}" title="${statusWord(s)}"></span><span class="nm" title="${esc(s.name)}">${esc(s.name)}</span>${showPlace ? `<span class="dg-badge ${s.runs}">${esc(placementText(s))}</span>` : ""}</div>
|
|
549
|
+
return `<div class="r1"><span class="dg-dot ${status(s)}" title="${statusWord(s)}"></span><span class="nm" title="${esc(s.name)}">${esc(s.name)}</span>${showPlace ? `<span class="dg-badge ${s.runs}">${esc(placementText(s))}</span>` : ""}${svpBtn(s.id)}${starBtn(s.id)}</div>
|
|
508
550
|
<div class="r2" title="${esc(s.id)}${s.inWorkspaces.length > 1 ? `: in ${esc(s.inWorkspaces.join(", "))}` : ""}">${esc(s.repo + ownersLabel(s.inWorkspaces))}</div>
|
|
509
551
|
<div class="r3">${s.entry ? '<span class="dg-tag entry">entry</span>' : ""}${s.infra ? '<span class="dg-tag">infra</span>' : ""}${s.missing.length ? `<span class="dg-tag warn" title="Missing: ${esc(s.missing.join(", "))}">⚠ missing</span>` : ""}${!s.cloned ? '<span class="dg-tag warn">not cloned</span>' : ""}<span class="h ${status(s)}">${esc(healthLine(s))}</span></div>
|
|
510
|
-
<div class="dg-acts">${
|
|
511
|
-
.filter(Boolean)
|
|
512
|
-
.slice(0, 3)
|
|
552
|
+
<div class="dg-acts">${cardActs(s)
|
|
513
553
|
.map((a) => actBtn(a, s, "sm"))
|
|
514
554
|
.join("")}</div>`;
|
|
515
555
|
}
|
|
@@ -915,7 +955,8 @@ function onClick(e) {
|
|
|
915
955
|
const id = pinT.dataset.pinToggle;
|
|
916
956
|
if (window.denShell.isPinned(id)) window.denShell.unpin(id);
|
|
917
957
|
else window.denShell.pin(id);
|
|
918
|
-
|
|
958
|
+
// A card's star (0.4.17) only pins: it doesn't open the card.
|
|
959
|
+
if (!pinT.closest(".dg-card")) openDetail(st.sel, true);
|
|
919
960
|
return;
|
|
920
961
|
}
|
|
921
962
|
if (t.closest("[data-popout]")) {
|
|
@@ -17,6 +17,7 @@ import { changelogView } from "./changelog-view.js";
|
|
|
17
17
|
import { buildRow, healthCycle, paintGate, shellHealth } from "./health-fit.js";
|
|
18
18
|
import { initTopbar, paintChecking, paintStatus } from "./topbar.js";
|
|
19
19
|
import { registerDenSettings } from "./settings-registry.js";
|
|
20
|
+
import { initServicePanel } from "./svc-panel.js";
|
|
20
21
|
|
|
21
22
|
/** The shell's message lookup (R19; shell.js MESSAGES). */
|
|
22
23
|
const t = (key, vars) =>
|
|
@@ -54,6 +55,12 @@ if (typeof document !== "undefined")
|
|
|
54
55
|
healthEnv: () => healthEnv(),
|
|
55
56
|
storeOpenLog: (repo, service, env) => storeOpenLog(repo, service, env),
|
|
56
57
|
});
|
|
58
|
+
// 0.4.18: the service panel (rail pins, Graph ⋯, Services names).
|
|
59
|
+
if (typeof document !== "undefined")
|
|
60
|
+
initServicePanel({
|
|
61
|
+
currentEnv: () => currentEnv(),
|
|
62
|
+
refreshHealth: (env, opts) => refreshHealth(env, opts),
|
|
63
|
+
});
|
|
57
64
|
|
|
58
65
|
// 5s between polls — measured from each poll's COMPLETION (schedulePoll
|
|
59
66
|
// re-arms after renderHealth resolves), so slow probes can't overlap.
|
|
@@ -53,6 +53,43 @@
|
|
|
53
53
|
"b.down.one": "{n} caído",
|
|
54
54
|
"act.pin": "Fijar",
|
|
55
55
|
"act.unpin": "Quitar",
|
|
56
|
+
"act.visit": "Visitar",
|
|
57
|
+
"act.visitX": "Visitar {s} (se abre en una pestaña nueva)",
|
|
58
|
+
// 0.4.18: the service panel (svc-panel.js) and Start's Folder.
|
|
59
|
+
"svp.label": "{s}: panel rápido",
|
|
60
|
+
"svp.more": "Panel rápido de {s}",
|
|
61
|
+
"svp.notRunning": "No está en marcha",
|
|
62
|
+
"svp.up": "activo {d}",
|
|
63
|
+
"svp.upNow": "activo",
|
|
64
|
+
"svp.down": "caído: {c}",
|
|
65
|
+
"svp.downNone": "caído",
|
|
66
|
+
"svp.starting": "iniciando…",
|
|
67
|
+
"svp.stopped": "detenido",
|
|
68
|
+
"svp.noUrl": "Sin URL",
|
|
69
|
+
"svp.start": "Iniciar",
|
|
70
|
+
"svp.stop": "Detener",
|
|
71
|
+
"svp.restart": "Reiniciar",
|
|
72
|
+
"svp.logs": "Registros",
|
|
73
|
+
"svp.stopping": "Deteniendo {s}",
|
|
74
|
+
"svp.tests": "Última ejecución de pruebas",
|
|
75
|
+
"svp.testsOk": "✓ {t}",
|
|
76
|
+
"svp.testsBad": "✗ falló {t}",
|
|
77
|
+
"svp.testsRun": "en curso…",
|
|
78
|
+
"svp.testsNone": "—",
|
|
79
|
+
"svp.runTests": "Ejecutar pruebas",
|
|
80
|
+
"svp.testsDone": "Pruebas superadas: {s}",
|
|
81
|
+
"svp.testsFailed": "Pruebas fallidas: {s}",
|
|
82
|
+
"svp.agents": "Agentes",
|
|
83
|
+
"svp.noAgents": "No hay agentes en este repo.",
|
|
84
|
+
"svp.open": "Abrir",
|
|
85
|
+
"svp.openAg": "Abrir {s}",
|
|
86
|
+
"svp.startHere": "Iniciar agente aquí",
|
|
87
|
+
"svp.openSvc": "Abrir en Servicios",
|
|
88
|
+
"svp.openGraph": "Abrir en Grafo",
|
|
89
|
+
"svp.close": "Cerrar",
|
|
90
|
+
"ag.new.folder": "Carpeta (opcional)",
|
|
91
|
+
"ag.new.folderHelp":
|
|
92
|
+
"Dentro de la carpeta del espacio de trabajo. Vacío: la suya.",
|
|
56
93
|
"act.pinX": "Fijar {s}",
|
|
57
94
|
"act.unpinX": "Quitar {s}",
|
|
58
95
|
"toast.pinned": "{s} fijado",
|
|
@@ -109,6 +146,8 @@
|
|
|
109
146
|
"kind.app": "App",
|
|
110
147
|
"kind.action": "Acción",
|
|
111
148
|
"kind.setting": "Ajuste",
|
|
149
|
+
"kind.workspace": "Espacio de trabajo",
|
|
150
|
+
"pal.goWs": "Ir al espacio de trabajo {ws}",
|
|
112
151
|
"pal.repos": "{n} repositorios",
|
|
113
152
|
"pal.repos.one": "{n} repositorio",
|
|
114
153
|
"pal.restart": "Reiniciar {s}",
|
|
@@ -516,6 +555,10 @@
|
|
|
516
555
|
"Está trabajando. ¿Cambiar después de este turno o detenerlo ahora?",
|
|
517
556
|
"ag.pc.after": "Después de este turno",
|
|
518
557
|
"ag.pc.now": "Detenerlo ahora",
|
|
558
|
+
"ag.pc.fresh":
|
|
559
|
+
"{id} ahora es {p}: aún no hay nada que reanudar; se inició de nuevo (mismo id de conversación, carpeta y alcance)",
|
|
560
|
+
"ag.freshLine":
|
|
561
|
+
"Aún no hay nada que reanudar; se inició de nuevo (mismo id de conversación, carpeta y alcance)",
|
|
519
562
|
"ag.pc.done": "{id} ahora es {p}",
|
|
520
563
|
"ag.pc.pending": "{id} pasará a ser {p} cuando termine este turno",
|
|
521
564
|
"ag.more": "Más para {id}",
|
|
@@ -669,6 +712,9 @@
|
|
|
669
712
|
"Esta sesión ha terminado. Su registro se queda hasta que se reanude o se elimine.",
|
|
670
713
|
"ag.switched": "Cambiado a {p}",
|
|
671
714
|
"ag.exitedLine": "[la sesión terminó{code}]",
|
|
715
|
+
"ag.refusedUnknown": "el servidor rechazó la terminal",
|
|
716
|
+
"ag.refusedStatus": "el servidor rechazó la terminal ({status})",
|
|
717
|
+
"ag.goWs": "Abrir {ws} en Espacios de trabajo",
|
|
672
718
|
"ag.lost": "[desconectado: {why}]",
|
|
673
719
|
"ag.reconnect": "Reconectar",
|
|
674
720
|
"ag.noBackend":
|