@monster-orange/den 0.2.8 → 0.4.0
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 +1731 -0
- package/README.md +4 -2
- package/bin/den +2 -1
- package/docs/distributions.md +730 -6
- package/make/den.mk +19 -4
- package/make/journeys.mk +2 -1
- package/make/local_dev.mk +6 -13
- package/make/secrets-1password.mk +18 -0
- package/make/test.mk +1 -1
- package/package.json +12 -2
- package/schemas/den.plugin.v1.json +156 -0
- package/schemas/den.theme.v1.json +269 -0
- package/scripts/auth-configure.ts +16 -8
- package/scripts/heal-config.ts +0 -5
- package/scripts/setup-1password.ts +41 -19
- package/scripts/setup-secrets.ts +67 -101
- package/src/pillars/api/global-setup.ts +1 -1
- package/src/pillars/journeys/sandbox.ts +23 -18
- package/src/pillars/journeys/tools/browser-session.ts +2 -2
- package/src/scripts/setup-default-workspace.ts +2 -2
- package/src/shared/agents/default-pack/be-dev.md +20 -0
- package/src/shared/agents/default-pack/devops.md +14 -0
- package/src/shared/agents/default-pack/fe-dev.md +20 -0
- package/src/shared/agents/default-pack/orchestrator.md +16 -0
- package/src/shared/agents/default-pack/planner.md +17 -0
- package/src/shared/agents/default-pack/reviewer.md +14 -0
- package/src/shared/agents/default-pack/ux-designer.md +15 -0
- package/src/shared/agents/den-claude.ts +173 -0
- package/src/shared/agents/generic.ts +52 -0
- package/src/shared/agents/personas.ts +218 -0
- package/src/shared/agents/records.ts +118 -0
- package/src/shared/agents/runtime.ts +192 -0
- package/src/shared/agents/runtimes.ts +47 -0
- package/src/shared/auth/headless-login.ts +1 -1
- package/src/shared/auth/sudo-vault.ts +93 -31
- package/src/shared/auth/token-providers/index.ts +7 -3
- package/src/shared/brand.ts +7 -0
- package/src/shared/config-error.ts +33 -3
- package/src/shared/constants.ts +3 -24
- package/src/shared/den-config.ts +257 -5
- package/src/shared/edge/config.ts +147 -21
- package/src/shared/edge/exec.ts +31 -0
- package/src/shared/edge/included.ts +169 -0
- package/src/shared/edge/render.ts +69 -14
- package/src/shared/edge/runtime.ts +128 -121
- package/src/shared/edge/status.ts +68 -8
- package/src/shared/edge/tls.ts +328 -0
- package/src/shared/edge/types.ts +25 -9
- package/src/shared/engine.ts +23 -0
- package/src/shared/environment-manager/default-workspace.ts +16 -1
- package/src/shared/environment-manager/health.ts +73 -38
- package/src/shared/environment-manager/scope.ts +168 -0
- package/src/shared/environment-manager/service-runner.ts +789 -43
- package/src/shared/environment-manager/up-targets.ts +132 -0
- package/src/shared/environment-manager/workspaces.ts +19 -0
- package/src/shared/health/rollup.ts +48 -0
- package/src/shared/open-url.ts +21 -2
- package/src/shared/org-sources.ts +78 -59
- package/src/shared/org.ts +53 -5
- package/src/shared/plugins/manifest.ts +406 -0
- package/src/shared/plugins/store.ts +358 -0
- package/src/shared/process/legacy-adopt.ts +285 -0
- package/src/shared/process/process-group.ts +202 -0
- package/src/shared/process/spawn-streaming.ts +7 -0
- package/src/shared/secret-env.ts +70 -0
- package/src/shared/secrets/doppler.ts +1 -1
- package/src/shared/secrets/index.ts +279 -50
- package/src/shared/secrets/none.ts +56 -0
- package/src/shared/secrets/strategies.ts +202 -0
- package/src/shared/secrets/types.ts +1 -1
- package/src/shared/sessions/client.ts +277 -0
- package/src/shared/sessions/den-pty.ts +97 -0
- package/src/shared/sessions/env.ts +79 -0
- package/src/shared/sessions/host.ts +667 -0
- package/src/shared/sessions/names.ts +85 -0
- package/src/shared/sessions/paths.ts +27 -0
- package/src/shared/sessions/protocol.ts +72 -0
- package/src/shared/sessions/pty.ts +141 -0
- package/src/shared/sessions/ring-buffer.ts +160 -0
- package/src/shared/sessions/types.ts +81 -0
- package/src/shared/smoke/git-remote.ts +40 -0
- package/src/shared/stacks/manifest-commands.ts +1 -1
- package/src/shared/stacks/namespace.ts +178 -0
- package/src/shared/stacks/owners.ts +303 -0
- package/src/shared/stacks/render.ts +8 -3
- package/src/shared/stacks/runtime.ts +100 -16
- package/src/shared/stacks/status.ts +63 -12
- package/src/shared/strategies/registry.ts +89 -0
- package/src/shared/theme/contract.ts +538 -0
- package/src/shared/types/index.ts +12 -3
- package/src/ui/cli/agents-command.ts +812 -0
- package/src/ui/cli/create.ts +24 -7
- package/src/ui/cli/doctor-command.ts +260 -0
- package/src/ui/cli/edge-command.ts +56 -10
- package/src/ui/cli/env-probe.ts +266 -0
- package/src/ui/cli/flag-parser.ts +18 -5
- package/src/ui/cli/health-display.ts +25 -0
- package/src/ui/cli/index.ts +580 -60
- package/src/ui/cli/notice.ts +20 -0
- package/src/ui/cli/plugin-command.ts +849 -0
- package/src/ui/cli/sessions-command.ts +718 -0
- package/src/ui/cli/stack-command.ts +63 -2
- package/src/ui/cli/test-runner.ts +11 -9
- package/src/ui/cli/theme-command.ts +136 -0
- package/src/ui/cli/web-command.ts +47 -23
- package/src/ui/mcp/api.ts +28 -2
- package/src/ui/mcp/scope.ts +145 -20
- package/src/ui/mcp/server.ts +27 -2
- package/src/ui/mcp/tools.ts +94 -12
- package/src/ui/server/appearance-prefs.ts +95 -0
- package/src/ui/server/audit.ts +35 -0
- package/src/ui/server/daemon-reaper.ts +29 -1
- package/src/ui/server/engine-stamp.ts +83 -0
- package/src/ui/server/github-service.ts +23 -44
- package/src/ui/server/graph-page.ts +20 -0
- package/src/ui/server/graph-prefs.ts +44 -1
- package/src/ui/server/http-errors.ts +293 -0
- package/src/ui/server/http-helpers.ts +22 -3
- package/src/ui/server/index.ts +539 -51
- package/src/ui/server/public/appearance.js +531 -0
- package/src/ui/server/public/edge-page.js +248 -0
- package/src/ui/server/public/edge.html +251 -0
- package/src/ui/server/public/environments.html +65 -83
- package/src/ui/server/public/error-page.js +247 -0
- package/src/ui/server/public/error.html +260 -0
- package/src/ui/server/public/graph-model.js +332 -14
- package/src/ui/server/public/graph.css +141 -47
- package/src/ui/server/public/graph.js +223 -44
- package/src/ui/server/public/header.js +396 -600
- package/src/ui/server/public/health-fit.js +248 -0
- package/src/ui/server/public/health-rollup.js +128 -0
- package/src/ui/server/public/i18n/es.js +287 -0
- package/src/ui/server/public/i18n/ja.js +276 -0
- package/src/ui/server/public/index.html +139 -438
- package/src/ui/server/public/local-dev.html +3468 -4044
- package/src/ui/server/public/logs.html +71 -208
- package/src/ui/server/public/panel.js +14 -2
- package/src/ui/server/public/release.html +175 -389
- package/src/ui/server/public/services.html +596 -770
- package/src/ui/server/public/settings-page.js +759 -0
- package/src/ui/server/public/settings-registry.js +521 -0
- package/src/ui/server/public/settings.html +590 -0
- package/src/ui/server/public/shell.js +3869 -0
- package/src/ui/server/public/topbar.js +1413 -0
- package/src/ui/server/public/version-label.js +12 -0
- package/src/ui/server/public/workspace-picker.js +74 -34
- package/src/ui/server/readonly-routes.ts +37 -3
- package/src/ui/server/services-list.ts +94 -17
- package/src/ui/server/settings-distribution.ts +74 -0
- package/src/ui/server/shell-prefs.ts +192 -0
- package/src/ui/server/theme-routes.ts +292 -0
- package/src/ui/server/theme-runtime.ts +407 -0
- package/src/ui/server/ui-assets.ts +16 -1
- package/src/ui/server/ui-language.ts +50 -0
- package/src/ui/server/workspace-routes.ts +48 -18
- package/themes/den/den-theme.json +72 -0
- package/themes/den/favicon.ico +0 -0
- package/themes/den/mark.svg +6 -0
|
@@ -21,13 +21,20 @@ import {
|
|
|
21
21
|
buildModel,
|
|
22
22
|
edgeModel,
|
|
23
23
|
entryOf,
|
|
24
|
+
findHit,
|
|
24
25
|
healthLine,
|
|
25
26
|
isCollapsed,
|
|
26
27
|
neighbours,
|
|
27
28
|
nodeFor,
|
|
28
29
|
orgOf,
|
|
30
|
+
packGroups,
|
|
29
31
|
placementText,
|
|
30
32
|
repoPlacement,
|
|
33
|
+
repoRollup,
|
|
34
|
+
repoStatus,
|
|
35
|
+
repoStatusWord,
|
|
36
|
+
routeCross,
|
|
37
|
+
splitTopLevel,
|
|
31
38
|
status,
|
|
32
39
|
statusWord,
|
|
33
40
|
} from "/graph-model.js";
|
|
@@ -40,6 +47,7 @@ import {
|
|
|
40
47
|
renderPanel,
|
|
41
48
|
runAction,
|
|
42
49
|
} from "/panel.js";
|
|
50
|
+
import { ownersLabel } from "/workspace-picker.js";
|
|
43
51
|
|
|
44
52
|
const LS = "__DEN_CLI__-graph:";
|
|
45
53
|
const store = {
|
|
@@ -90,9 +98,26 @@ let edges = [];
|
|
|
90
98
|
let elk = null;
|
|
91
99
|
let renderSeq = 0;
|
|
92
100
|
let prefsLoaded = false;
|
|
101
|
+
// The floor of the last fit while the view is still the automatic one; null
|
|
102
|
+
// once the user pans or zooms. A viewport that changes size (Local Dev's
|
|
103
|
+
// output panel opening below it) re-fits an automatic view, so cards never
|
|
104
|
+
// sit under the panel; a view the user moved is left where they put it.
|
|
105
|
+
let autoFit = null;
|
|
106
|
+
let vpObserver = null;
|
|
93
107
|
|
|
94
108
|
const $ = (s, el = host) => el?.querySelector(s);
|
|
95
109
|
const zoomFloor = 0.35;
|
|
110
|
+
/**
|
|
111
|
+
* The automatic fit (first draw, a new set of cards, a layout change, the
|
|
112
|
+
* viewport resizing) shows the whole graph, as Fit does (0.3.0-dev.34).
|
|
113
|
+
* Until dev.33 a graph that needed less than 25% opened on the top of it
|
|
114
|
+
* at 60%, and a 31-card graph across 10 org groups opened with 17 of 31
|
|
115
|
+
* cards outside the stage (a consumer's finding); the groups now wrap to
|
|
116
|
+
* the stage (packGroups), so a graph that big fits at a readable zoom.
|
|
117
|
+
*/
|
|
118
|
+
const AUTO_FIT = "auto";
|
|
119
|
+
/** How far Fit (and zooming out to it) goes for a very big graph. */
|
|
120
|
+
const FIT_MIN = 0.1;
|
|
96
121
|
|
|
97
122
|
bindPanel({
|
|
98
123
|
model: () => M,
|
|
@@ -142,7 +167,7 @@ function loadServerPrefs() {
|
|
|
142
167
|
store.set("prefs", prefs());
|
|
143
168
|
if (JSON.stringify(prefs()) !== before && host?.isConnected) {
|
|
144
169
|
syncControls();
|
|
145
|
-
render().then(() => fit(
|
|
170
|
+
render().then((d) => d && fit(AUTO_FIT));
|
|
146
171
|
}
|
|
147
172
|
})
|
|
148
173
|
.catch(() => {});
|
|
@@ -165,6 +190,25 @@ function loadElk() {
|
|
|
165
190
|
return elkLoading;
|
|
166
191
|
}
|
|
167
192
|
|
|
193
|
+
// ── maximize (R23, Ward QA 2026-11-05) ────────────────────────────────────
|
|
194
|
+
// The page owns the layout: hooks.maximize = { isOn(), toggle() } gives the
|
|
195
|
+
// zoom group a Maximize button (F on the page; Esc restores). Without it,
|
|
196
|
+
// no button.
|
|
197
|
+
function maxButton() {
|
|
198
|
+
const m = hooks.maximize;
|
|
199
|
+
if (!m) return "";
|
|
200
|
+
const on = !!m.isOn?.();
|
|
201
|
+
return `<button data-z="max" aria-pressed="${on}" aria-keyshortcuts="F" aria-label="Maximize graph" title="${on ? "Restore (Esc)" : "Maximize graph (F)"}">${on ? "⤡" : "⤢"}</button>`;
|
|
202
|
+
}
|
|
203
|
+
/** Repaint the Maximize button after the page changed the layout. */
|
|
204
|
+
export function graphMaximized(on) {
|
|
205
|
+
const b = $('[data-z="max"]');
|
|
206
|
+
if (!b) return;
|
|
207
|
+
b.setAttribute("aria-pressed", String(!!on));
|
|
208
|
+
b.title = on ? "Restore (Esc)" : "Maximize graph (F)";
|
|
209
|
+
b.textContent = on ? "⤡" : "⤢";
|
|
210
|
+
}
|
|
211
|
+
|
|
168
212
|
// ── mount ────────────────────────────────────────────────────────────────
|
|
169
213
|
export function mountGraph(el, data, pageHooks) {
|
|
170
214
|
hooks = pageHooks || {};
|
|
@@ -191,7 +235,7 @@ export function mountGraph(el, data, pageHooks) {
|
|
|
191
235
|
<div class="dg-vp" tabindex="0" aria-label="Service graph. Drag to pan, scroll to move, Ctrl or Cmd + scroll to zoom.">
|
|
192
236
|
<div class="dg-world"><svg aria-hidden="true"></svg></div>
|
|
193
237
|
<div class="dg-legend"></div>
|
|
194
|
-
<div class="dg-zoom" role="group" aria-label="Zoom"><button data-z="out" aria-label="Zoom out">−</button><button data-z="100" aria-label="Reset zoom to 100%">100%</button><button data-z="in" aria-label="Zoom in">+</button><button data-z="fit">Fit</button
|
|
238
|
+
<div class="dg-zoom" role="group" aria-label="Zoom"><button data-z="out" aria-label="Zoom out">−</button><button data-z="100" aria-label="Reset zoom to 100%">100%</button><button data-z="in" aria-label="Zoom in">+</button><button data-z="fit">Fit</button>${maxButton()}</div>
|
|
195
239
|
</div>
|
|
196
240
|
<aside class="dg-panel" hidden aria-label="Details"></aside>
|
|
197
241
|
</div>
|
|
@@ -199,9 +243,16 @@ export function mountGraph(el, data, pageHooks) {
|
|
|
199
243
|
wire();
|
|
200
244
|
syncControls();
|
|
201
245
|
loadServerPrefs();
|
|
202
|
-
render
|
|
203
|
-
|
|
204
|
-
|
|
246
|
+
// Fit once the layout that is drawn is known: a render a newer one
|
|
247
|
+
// overtook draws nothing and fits nothing (All active, and the org
|
|
248
|
+
// frames landing after the first draw, mount twice in quick succession).
|
|
249
|
+
// A new set of cards (the orgs landing, the picker, a workspace
|
|
250
|
+
// activated) re-fits while the view is the automatic one; a view the
|
|
251
|
+
// user zoomed or panned stays where they put it.
|
|
252
|
+
render().then((drawn) => {
|
|
253
|
+
if (!drawn) return;
|
|
254
|
+
if (!st.fitted || autoFit != null) {
|
|
255
|
+
fit(AUTO_FIT);
|
|
205
256
|
st.fitted = true;
|
|
206
257
|
} else applyView();
|
|
207
258
|
if (st.sel) openDetail(st.sel, true);
|
|
@@ -213,10 +264,18 @@ export function graphHealth(results) {
|
|
|
213
264
|
if (!M || !host?.isConnected) return;
|
|
214
265
|
applyHealth(M, results);
|
|
215
266
|
for (const c of host.querySelectorAll(".dg-card")) {
|
|
216
|
-
if (c.dataset.id && M.byId.has(c.dataset.id))
|
|
217
|
-
|
|
218
|
-
|
|
267
|
+
if (c.dataset.id && M.byId.has(c.dataset.id)) {
|
|
268
|
+
const s = M.byId.get(c.dataset.id);
|
|
269
|
+
c.innerHTML = cardInner(s);
|
|
270
|
+
// A service card's border follows its health too (0.3.0-dev.40,
|
|
271
|
+
// DOC-1: it stayed red after a recovery; only repo cards toggled).
|
|
272
|
+
c.classList.toggle("down", s.health.ok === false);
|
|
273
|
+
} else if (c.dataset.repo && M.byRepo.has(c.dataset.repo)) {
|
|
219
274
|
c.innerHTML = repoCardInner(c.dataset.repo);
|
|
275
|
+
const s = repoRollup(M, c.dataset.repo).state;
|
|
276
|
+
c.classList.toggle("down", s === "down");
|
|
277
|
+
c.classList.toggle("degraded", s === "degraded");
|
|
278
|
+
}
|
|
220
279
|
}
|
|
221
280
|
for (const g of host.querySelectorAll(".dg-grp.dg-org"))
|
|
222
281
|
g.querySelector(".roll").textContent = rollup(groupServices(g.dataset.key));
|
|
@@ -255,8 +314,21 @@ async function render() {
|
|
|
255
314
|
for (const e of n.edges || []) meta.set(e.id, e._m);
|
|
256
315
|
(n.children || []).forEach(walk);
|
|
257
316
|
})(root);
|
|
258
|
-
|
|
259
|
-
|
|
317
|
+
// Each top-level group on its own, then wrapped to the stage
|
|
318
|
+
// (graph-model.js splitTopLevel / packGroups, 0.3.0-dev.34).
|
|
319
|
+
const { parts, cross, groupOf } = splitTopLevel(root);
|
|
320
|
+
const results = await Promise.all(parts.map((p) => elk.layout(p)));
|
|
321
|
+
if (seq !== renderSeq || !world.isConnected) return false; // a newer render won
|
|
322
|
+
const area = fitArea();
|
|
323
|
+
const tops = results.map((r) => r.children[0]);
|
|
324
|
+
const pack = packGroups(
|
|
325
|
+
tops.map((g) => ({ w: g.width, h: g.height })),
|
|
326
|
+
{
|
|
327
|
+
direction: st.dir,
|
|
328
|
+
// The world adds 30px to the groups' extent (padding and margin).
|
|
329
|
+
...(area ? { width: area.w - 30, height: area.h - 30 } : {}),
|
|
330
|
+
},
|
|
331
|
+
);
|
|
260
332
|
world.querySelectorAll(".dg-grp,.dg-card,.dg-msg").forEach((n) => n.remove());
|
|
261
333
|
edges = edgeModel(M, st.collapsed);
|
|
262
334
|
layoutAbs = {};
|
|
@@ -264,7 +336,7 @@ async function render() {
|
|
|
264
336
|
const paths = [];
|
|
265
337
|
let maxX = 0,
|
|
266
338
|
maxY = 0;
|
|
267
|
-
|
|
339
|
+
function walk(n, ox, oy) {
|
|
268
340
|
const x = ox + (n.x || 0),
|
|
269
341
|
y = oy + (n.y || 0);
|
|
270
342
|
if (n.id !== "root") {
|
|
@@ -297,35 +369,69 @@ async function render() {
|
|
|
297
369
|
}
|
|
298
370
|
}
|
|
299
371
|
for (const c of n.children || []) walk(c, x, y);
|
|
300
|
-
}
|
|
372
|
+
}
|
|
373
|
+
// The root's padding (10) around the wrapped groups.
|
|
374
|
+
const PAD = 10;
|
|
375
|
+
const bands = new Map();
|
|
376
|
+
results.forEach((r, i) => {
|
|
377
|
+
const g = tops[i],
|
|
378
|
+
at = pack.pos[i];
|
|
379
|
+
walk(r, PAD + at.x - g.x, PAD + at.y - g.y);
|
|
380
|
+
const down = st.dir !== "right";
|
|
381
|
+
const from = PAD + (down ? at.y : at.x);
|
|
382
|
+
const b = bands.get(at.band) || { band: at.band, from, to: from };
|
|
383
|
+
b.from = Math.min(b.from, from);
|
|
384
|
+
b.to = Math.max(b.to, from + (down ? g.height : g.width));
|
|
385
|
+
bands.set(at.band, b);
|
|
386
|
+
});
|
|
387
|
+
const bandOf = new Map(
|
|
388
|
+
tops.map((g, i) => [g.id, bands.get(pack.pos[i].band)]),
|
|
389
|
+
);
|
|
390
|
+
// Edges between groups: routed through the gaps between the wrap's rows.
|
|
391
|
+
for (const e of cross) {
|
|
392
|
+
const a = layoutAbs[e.sources[0]],
|
|
393
|
+
b = layoutAbs[e.targets[0]];
|
|
394
|
+
const ba = bandOf.get(groupOf.get(e.sources[0])),
|
|
395
|
+
bb = bandOf.get(groupOf.get(e.targets[0]));
|
|
396
|
+
if (!a || !b || !ba || !bb) continue;
|
|
397
|
+
paths.push({ d: roundPath(routeCross(a, b, ba, bb, st.dir)), m: e._m });
|
|
398
|
+
}
|
|
301
399
|
world.appendChild(frag);
|
|
302
400
|
const svg = $(".dg-world > svg");
|
|
303
401
|
svg.setAttribute("width", maxX + 20);
|
|
304
402
|
svg.setAttribute("height", maxY + 20);
|
|
305
403
|
const MK = {
|
|
306
|
-
intra: "
|
|
307
|
-
hard: "
|
|
308
|
-
soft: "
|
|
309
|
-
event: "
|
|
310
|
-
cross: "
|
|
404
|
+
intra: "var(--muted)",
|
|
405
|
+
hard: "var(--accent)",
|
|
406
|
+
soft: "var(--muted)",
|
|
407
|
+
event: "var(--dg-event)",
|
|
408
|
+
cross: "var(--remote)",
|
|
311
409
|
};
|
|
312
410
|
svg.innerHTML =
|
|
313
411
|
`<defs>${Object.entries(MK)
|
|
314
412
|
.map(
|
|
315
413
|
([k, c]) =>
|
|
316
|
-
`<marker id="dg-ah-${k}" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z"
|
|
414
|
+
`<marker id="dg-ah-${k}" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" style="fill:${c}"/></marker>`,
|
|
317
415
|
)
|
|
318
416
|
.join("")}</defs>` +
|
|
319
417
|
paths
|
|
320
418
|
.map(({ d, m }) => {
|
|
321
419
|
const k = m.cross ? "cross" : m.kind;
|
|
322
|
-
|
|
420
|
+
// R22b-2: each edge's flow overlay, drawn on its base path (same
|
|
421
|
+
// d, same kind, so the same colour); it shows and marches from
|
|
422
|
+
// data-f to data-t only while the edge is highlighted.
|
|
423
|
+
const ends = `data-f="${esc(m.from)}" data-t="${esc(m.to)}" d="${d}"`;
|
|
424
|
+
return (
|
|
425
|
+
`<path class="dg-e ${k}" ${ends} marker-end="url(#dg-ah-${k})"/>` +
|
|
426
|
+
`<path class="dg-flow ${k}" ${ends}/>`
|
|
427
|
+
);
|
|
323
428
|
})
|
|
324
429
|
.join("");
|
|
325
430
|
world.style.width = maxX + 20 + "px";
|
|
326
431
|
world.style.height = maxY + 20 + "px";
|
|
327
432
|
legend();
|
|
328
433
|
applyFocus();
|
|
434
|
+
return true;
|
|
329
435
|
}
|
|
330
436
|
|
|
331
437
|
function roundPath(pts) {
|
|
@@ -399,7 +505,7 @@ function cardInner(s) {
|
|
|
399
505
|
const showPlace = s.runs === "remote" || M.mixed;
|
|
400
506
|
const acts = actionsFor(s);
|
|
401
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>
|
|
402
|
-
<div class="r2" title="${esc(s.id)}">${esc(s.repo)}</div>
|
|
508
|
+
<div class="r2" title="${esc(s.id)}${s.inWorkspaces.length > 1 ? `: in ${esc(s.inWorkspaces.join(", "))}` : ""}">${esc(s.repo + ownersLabel(s.inWorkspaces))}</div>
|
|
403
509
|
<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>
|
|
404
510
|
<div class="dg-acts">${[acts.primary, ...acts.common]
|
|
405
511
|
.filter(Boolean)
|
|
@@ -423,8 +529,9 @@ function card(s, x, y, w, h) {
|
|
|
423
529
|
function repoCardInner(r) {
|
|
424
530
|
const svcs = M.byRepo.get(r),
|
|
425
531
|
ok = svcs.filter((s) => s.health.ok).length,
|
|
426
|
-
pl = repoPlacement(M, r)
|
|
427
|
-
|
|
532
|
+
pl = repoPlacement(M, r),
|
|
533
|
+
roll = repoRollup(M, r);
|
|
534
|
+
return `<div class="r1"><span class="dg-dot ${repoStatus(roll)}" title="${esc(repoStatusWord(roll))}"></span><span class="nm">${esc(r)}</span>${pl !== "local" || M.mixed ? `<span class="dg-badge ${pl}">${pl === "hybrid" ? "⇄ hybrid" : pl === "remote" ? "☁ remote" : "⌂ local"}</span>` : ""}</div>
|
|
428
535
|
<div class="r2">${svcs.length} services · ${ok}/${svcs.length} healthy</div>
|
|
429
536
|
<div class="r3 sum">${svcs.map((s) => `<span class="dg-dot ${status(s)}" title="${esc(s.name)}: ${statusWord(s)}"></span>`).join("")}</div>
|
|
430
537
|
<div class="dg-acts"><button class="dg-btn sm" data-expand="${esc(r)}">Expand</button></div>`;
|
|
@@ -432,13 +539,14 @@ function repoCardInner(r) {
|
|
|
432
539
|
function repoCard(r, x, y, w, h) {
|
|
433
540
|
const svcs = M.byRepo.get(r);
|
|
434
541
|
const el = document.createElement("div");
|
|
435
|
-
|
|
542
|
+
const roll = repoRollup(M, r);
|
|
543
|
+
el.className = `dg-card ${st.density === "compact" ? "compact" : ""} ${repoPlacement(M, r) === "remote" ? "remote" : ""} ${roll.state === "down" ? "down" : roll.state === "degraded" ? "degraded" : ""}`;
|
|
436
544
|
el.dataset.repo = r;
|
|
437
545
|
el.tabIndex = 0;
|
|
438
546
|
el.setAttribute("role", "button");
|
|
439
547
|
el.setAttribute(
|
|
440
548
|
"aria-label",
|
|
441
|
-
`${r}, ${svcs.length} services, ${svcs.filter((s) => s.health.ok).length} healthy. Open details.`,
|
|
549
|
+
`${r}, ${repoStatusWord(roll)}, ${svcs.length} services, ${svcs.filter((s) => s.health.ok).length} healthy. Open details.`,
|
|
442
550
|
);
|
|
443
551
|
el.innerHTML = repoCardInner(r);
|
|
444
552
|
return place(el, x, y, w, h);
|
|
@@ -448,9 +556,9 @@ function legend() {
|
|
|
448
556
|
const kinds = new Set(edges.map((e) => (e.cross ? "cross" : e.kind)));
|
|
449
557
|
const items = [
|
|
450
558
|
["hard", "var(--accent)", "solid", "hard dependency"],
|
|
451
|
-
["soft", "
|
|
452
|
-
["event", "
|
|
453
|
-
["intra", "
|
|
559
|
+
["soft", "var(--muted)", "dashed", "soft"],
|
|
560
|
+
["event", "var(--dg-event)", "dotted", "event"],
|
|
561
|
+
["intra", "var(--muted)", "solid", "within a repo"],
|
|
454
562
|
["cross", "var(--dg-remote)", "dashed", "local → remote"],
|
|
455
563
|
].filter(([k]) => kinds.has(k));
|
|
456
564
|
$(".dg-legend").innerHTML =
|
|
@@ -488,7 +596,7 @@ function applyFocus() {
|
|
|
488
596
|
const q = st.q.trim().toLowerCase();
|
|
489
597
|
host.querySelectorAll(".dg-card").forEach((c) => {
|
|
490
598
|
const k = c.dataset.id || "repo:" + c.dataset.repo;
|
|
491
|
-
const hit =
|
|
599
|
+
const hit = findHit(k, q);
|
|
492
600
|
c.classList.toggle("sel", key != null && k === key);
|
|
493
601
|
c.classList.toggle("hit", hit);
|
|
494
602
|
c.classList.toggle("dim", (focus && !focus.has(k)) || (!!q && !hit));
|
|
@@ -502,6 +610,9 @@ function applyFocus() {
|
|
|
502
610
|
: p.dataset.f === key || p.dataset.t === key);
|
|
503
611
|
p.classList.toggle("hl", on);
|
|
504
612
|
p.classList.toggle("dim", (!!focus && !on) || !!q);
|
|
613
|
+
// Its flow overlay (the next sibling, R22b-2) runs only while lit.
|
|
614
|
+
const flow = p.nextElementSibling;
|
|
615
|
+
if (flow?.classList.contains("dg-flow")) flow.classList.toggle("on", on);
|
|
505
616
|
});
|
|
506
617
|
}
|
|
507
618
|
function focusOn(sel) {
|
|
@@ -604,24 +715,64 @@ function zoomAt(f, cx, cy) {
|
|
|
604
715
|
const vp = $(".dg-vp").getBoundingClientRect();
|
|
605
716
|
cx ??= vp.width / 2;
|
|
606
717
|
cy ??= vp.height / 2;
|
|
607
|
-
|
|
718
|
+
autoFit = null;
|
|
719
|
+
// Out to 35%, or further when that is what the whole graph needs.
|
|
720
|
+
const lo = Math.min(zoomFloor, Math.max(FIT_MIN, fitNeed() ?? zoomFloor));
|
|
721
|
+
const nz = Math.min(2, Math.max(lo, st.z * f));
|
|
608
722
|
st.tx = cx - ((cx - st.tx) * nz) / st.z;
|
|
609
723
|
st.ty = cy - ((cy - st.ty) * nz) / st.z;
|
|
610
724
|
st.z = nz;
|
|
611
725
|
applyView();
|
|
612
726
|
}
|
|
613
|
-
/**
|
|
614
|
-
|
|
727
|
+
/** The fitted view's top inset (st.ty) and the gap kept above the overlays. */
|
|
728
|
+
const FIT_TOP = 16;
|
|
729
|
+
const FIT_GAP = 8;
|
|
730
|
+
/**
|
|
731
|
+
* The height the overlays at the viewport's foot take (the legend and the
|
|
732
|
+
* zoom group, from the bottom edge to the higher one's top, plus a gap):
|
|
733
|
+
* Fit keeps the graph above them (0.3.0-dev.33, a consumer's finding: a
|
|
734
|
+
* legend on two lines covered the last group's bottom 10px). Measured,
|
|
735
|
+
* so a wrapped legend reserves its real height.
|
|
736
|
+
*/
|
|
737
|
+
function bottomReserve(vp) {
|
|
738
|
+
let top = vp.bottom;
|
|
739
|
+
for (const el of [$(".dg-legend"), $(".dg-zoom")]) {
|
|
740
|
+
const r = el?.getBoundingClientRect();
|
|
741
|
+
if (r && r.height > 0) top = Math.min(top, r.top);
|
|
742
|
+
}
|
|
743
|
+
return vp.bottom - top + FIT_GAP;
|
|
744
|
+
}
|
|
745
|
+
/** The area a fitted graph may use in the viewport now, or null. */
|
|
746
|
+
function fitArea() {
|
|
747
|
+
const vp = $(".dg-vp")?.getBoundingClientRect();
|
|
748
|
+
if (!vp || vp.width < 1 || vp.height < 1) return null;
|
|
749
|
+
return { w: vp.width - 32, h: vp.height - FIT_TOP - bottomReserve(vp) };
|
|
750
|
+
}
|
|
751
|
+
/** The zoom that shows the whole graph in the viewport now (≤ 1), or null. */
|
|
752
|
+
function fitNeed() {
|
|
753
|
+
const area = fitArea();
|
|
754
|
+
const world = $(".dg-world");
|
|
755
|
+
if (!area || !world) return null;
|
|
756
|
+
const w = parseFloat(world.style.width) || 1,
|
|
757
|
+
h = parseFloat(world.style.height) || 1;
|
|
758
|
+
return Math.min(1, area.w / w, area.h / h);
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Fit the graph; automatic fits pass AUTO_FIT, the Fit button FIT_MIN.
|
|
762
|
+
* Fit fits: the whole graph, bound by the width and the height (R23), down
|
|
763
|
+
* to FIT_MIN. A 35% floor here made Fit read 35% for a big graph whether
|
|
764
|
+
* the graph was maximized or not (a consumer's dev.30 finding). The fitted
|
|
765
|
+
* view refits when the viewport changes size (maximize, restore, the dock).
|
|
766
|
+
*/
|
|
767
|
+
function fit(floor = FIT_MIN) {
|
|
615
768
|
const vp = $(".dg-vp")?.getBoundingClientRect();
|
|
616
769
|
if (!vp) return;
|
|
617
|
-
const w = parseFloat($(".dg-world").style.width) || 1
|
|
618
|
-
|
|
619
|
-
st.z = Math.max(
|
|
620
|
-
floor,
|
|
621
|
-
Math.min(1, (vp.width - 32) / w, (vp.height - 70) / h),
|
|
622
|
-
);
|
|
770
|
+
const w = parseFloat($(".dg-world").style.width) || 1;
|
|
771
|
+
const need = fitNeed();
|
|
772
|
+
st.z = Math.max(floor === AUTO_FIT ? FIT_MIN : floor, need);
|
|
623
773
|
st.tx = Math.max(16, (vp.width - w * st.z) / 2);
|
|
624
|
-
st.ty =
|
|
774
|
+
st.ty = FIT_TOP;
|
|
775
|
+
autoFit = floor;
|
|
625
776
|
applyView();
|
|
626
777
|
}
|
|
627
778
|
|
|
@@ -759,6 +910,14 @@ function onClick(e) {
|
|
|
759
910
|
openDetail(st.sel, true);
|
|
760
911
|
return;
|
|
761
912
|
}
|
|
913
|
+
const pinT = t.closest("[data-pin-toggle]");
|
|
914
|
+
if (pinT && window.denShell) {
|
|
915
|
+
const id = pinT.dataset.pinToggle;
|
|
916
|
+
if (window.denShell.isPinned(id)) window.denShell.unpin(id);
|
|
917
|
+
else window.denShell.pin(id);
|
|
918
|
+
openDetail(st.sel, true);
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
762
921
|
if (t.closest("[data-popout]")) {
|
|
763
922
|
st.popped = !st.popped;
|
|
764
923
|
openDetail(st.sel, true);
|
|
@@ -831,7 +990,7 @@ function wire() {
|
|
|
831
990
|
syncControls();
|
|
832
991
|
savePrefs();
|
|
833
992
|
render().then(() => {
|
|
834
|
-
if (el.dataset.seg !== "density") fit(
|
|
993
|
+
if (el.dataset.seg !== "density") fit(AUTO_FIT);
|
|
835
994
|
if (st.sel) keepVisible(st.sel);
|
|
836
995
|
});
|
|
837
996
|
}),
|
|
@@ -847,7 +1006,7 @@ function wire() {
|
|
|
847
1006
|
[...M.byRepo.keys()].filter((r) => M.byRepo.get(r).length > 1),
|
|
848
1007
|
);
|
|
849
1008
|
store.set("collapsed", [...st.collapsed]);
|
|
850
|
-
render().then(() => fit(
|
|
1009
|
+
render().then((d) => d && fit(AUTO_FIT));
|
|
851
1010
|
});
|
|
852
1011
|
const find = $("[data-find]");
|
|
853
1012
|
find.addEventListener("input", () => {
|
|
@@ -858,7 +1017,7 @@ function wire() {
|
|
|
858
1017
|
if (e.key !== "Enter") return;
|
|
859
1018
|
const q = st.q.trim().toLowerCase();
|
|
860
1019
|
if (!q) return;
|
|
861
|
-
const s = M.services.find((x) => x.id
|
|
1020
|
+
const s = M.services.find((x) => findHit(x.id, q));
|
|
862
1021
|
if (s)
|
|
863
1022
|
focusOn(
|
|
864
1023
|
isCollapsed(M, st.collapsed, s.repo) ? { repo: s.repo } : { id: s.id },
|
|
@@ -866,6 +1025,16 @@ function wire() {
|
|
|
866
1025
|
});
|
|
867
1026
|
// pan: drag the background; scroll moves; Ctrl/⌘ + scroll zooms
|
|
868
1027
|
const vp = $(".dg-vp");
|
|
1028
|
+
vpObserver?.disconnect();
|
|
1029
|
+
if (typeof ResizeObserver === "function") {
|
|
1030
|
+
let size = null;
|
|
1031
|
+
vpObserver = new ResizeObserver(([e]) => {
|
|
1032
|
+
const next = `${Math.round(e.contentRect.width)}x${Math.round(e.contentRect.height)}`;
|
|
1033
|
+
if (size && next !== size && autoFit != null) fit(autoFit);
|
|
1034
|
+
size = next;
|
|
1035
|
+
});
|
|
1036
|
+
vpObserver.observe(vp);
|
|
1037
|
+
}
|
|
869
1038
|
let drag = null;
|
|
870
1039
|
vp.addEventListener("pointerdown", (e) => {
|
|
871
1040
|
if (e.button !== 0 || e.target.closest(".dg-card,button,.dg-zoom")) return;
|
|
@@ -877,7 +1046,10 @@ function wire() {
|
|
|
877
1046
|
if (!drag) return;
|
|
878
1047
|
const dx = e.clientX - drag.x,
|
|
879
1048
|
dy = e.clientY - drag.y;
|
|
880
|
-
if (Math.abs(dx) + Math.abs(dy) > 3)
|
|
1049
|
+
if (Math.abs(dx) + Math.abs(dy) > 3) {
|
|
1050
|
+
drag.moved = true;
|
|
1051
|
+
autoFit = null;
|
|
1052
|
+
}
|
|
881
1053
|
st.tx = drag.tx + dx;
|
|
882
1054
|
st.ty = drag.ty + dy;
|
|
883
1055
|
applyView();
|
|
@@ -905,6 +1077,7 @@ function wire() {
|
|
|
905
1077
|
e.clientY - r.top,
|
|
906
1078
|
);
|
|
907
1079
|
else {
|
|
1080
|
+
autoFit = null;
|
|
908
1081
|
st.tx -= e.deltaX;
|
|
909
1082
|
st.ty -= e.deltaY;
|
|
910
1083
|
applyView();
|
|
@@ -917,12 +1090,14 @@ function wire() {
|
|
|
917
1090
|
if (z === "in") zoomAt(1.2);
|
|
918
1091
|
if (z === "out") zoomAt(1 / 1.2);
|
|
919
1092
|
if (z === "100") {
|
|
1093
|
+
autoFit = null;
|
|
920
1094
|
st.z = 1;
|
|
921
1095
|
st.tx = 24;
|
|
922
1096
|
st.ty = 24;
|
|
923
1097
|
applyView();
|
|
924
1098
|
}
|
|
925
|
-
if (z === "fit") fit(
|
|
1099
|
+
if (z === "fit") fit(FIT_MIN);
|
|
1100
|
+
if (z === "max") hooks.maximize?.toggle?.();
|
|
926
1101
|
});
|
|
927
1102
|
// panel resize (360–760px, remembered)
|
|
928
1103
|
let rs = null;
|
|
@@ -956,9 +1131,13 @@ document.addEventListener("keydown", (e) => {
|
|
|
956
1131
|
const more = menuOwner.get(m) ?? m.previousElementSibling;
|
|
957
1132
|
closeMenus();
|
|
958
1133
|
more?.focus();
|
|
1134
|
+
e.preventDefault(); // handled: the page's Esc (restore) waits
|
|
959
1135
|
return;
|
|
960
1136
|
}
|
|
961
|
-
if (st.sel)
|
|
1137
|
+
if (st.sel) {
|
|
1138
|
+
closeDetail();
|
|
1139
|
+
e.preventDefault();
|
|
1140
|
+
}
|
|
962
1141
|
return;
|
|
963
1142
|
}
|
|
964
1143
|
if (
|