@unbrained/pm-web 2026.8.10 → 2026.8.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 +22 -0
- package/dist/app.d.ts +16 -0
- package/dist/app.js +16 -0
- package/dist/app.js.map +1 -1
- package/dist/auth.d.ts +5 -0
- package/dist/auth.js.map +1 -1
- package/dist/board.d.ts +53 -0
- package/dist/board.js +48 -0
- package/dist/board.js.map +1 -1
- package/dist/crypto.d.ts +23 -0
- package/dist/crypto.js +32 -0
- package/dist/crypto.js.map +1 -1
- package/dist/db.d.ts +19 -0
- package/dist/db.js +19 -0
- package/dist/db.js.map +1 -1
- package/dist/ical.d.ts +72 -0
- package/dist/ical.js +73 -0
- package/dist/ical.js.map +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.js +52 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/auth.d.ts +8 -0
- package/dist/middleware/auth.js.map +1 -1
- package/dist/oidc.d.ts +87 -0
- package/dist/oidc.js +113 -0
- package/dist/oidc.js.map +1 -1
- package/dist/routes/admin.js +15 -0
- package/dist/routes/admin.js.map +1 -1
- package/dist/routes/extensions.js +10 -0
- package/dist/routes/extensions.js.map +1 -1
- package/dist/routes/github.js +13 -0
- package/dist/routes/github.js.map +1 -1
- package/dist/routes/oidc.d.ts +10 -0
- package/dist/routes/oidc.js +51 -0
- package/dist/routes/oidc.js.map +1 -1
- package/dist/routes/pm.js +132 -1
- package/dist/routes/pm.js.map +1 -1
- package/dist/routes/sharing.js +8 -0
- package/dist/routes/sharing.js.map +1 -1
- package/dist/services/mutation-event-watcher.js +13 -0
- package/dist/services/mutation-event-watcher.js.map +1 -1
- package/dist/services/package-catalog.d.ts +24 -0
- package/dist/services/package-catalog.js +22 -2
- package/dist/services/package-catalog.js.map +1 -1
- package/dist/services/pm-runner.d.ts +82 -0
- package/dist/services/pm-runner.js +121 -0
- package/dist/services/pm-runner.js.map +1 -1
- package/dist/services/project-watcher.d.ts +22 -0
- package/dist/services/project-watcher.js +51 -0
- package/dist/services/project-watcher.js.map +1 -1
- package/dist/services/realtime-bus.d.ts +46 -0
- package/dist/services/realtime-bus.js +59 -0
- package/dist/services/realtime-bus.js.map +1 -1
- package/dist/services/sse.d.ts +134 -0
- package/dist/services/sse.js +145 -0
- package/dist/services/sse.js.map +1 -1
- package/manifest.json +1 -1
- package/package.json +5 -3
- package/public/src/app.js +44 -1
- package/public/src/app.js.map +1 -1
- package/public/src/app.ts +44 -1
- package/public/src/components/modals.js +11 -0
- package/public/src/components/modals.js.map +1 -1
- package/public/src/components/modals.ts +11 -0
- package/public/src/components/toast.js +2 -0
- package/public/src/components/toast.js.map +1 -1
- package/public/src/components/toast.ts +2 -0
- package/public/src/constants.js +21 -4
- package/public/src/constants.js.map +1 -1
- package/public/src/constants.ts +21 -4
- package/public/src/filters.js +10 -7
- package/public/src/filters.js.map +1 -1
- package/public/src/filters.ts +10 -7
- package/public/src/i18n/de.json +3 -1
- package/public/src/i18n/en.json +3 -1
- package/public/src/i18n/es.json +3 -1
- package/public/src/i18n/zh.json +3 -1
- package/public/src/i18n.js +11 -1
- package/public/src/i18n.js.map +1 -1
- package/public/src/i18n.ts +13 -1
- package/public/src/state.js +3 -0
- package/public/src/state.js.map +1 -1
- package/public/src/state.ts +3 -0
- package/public/src/sw.ts +71 -7
- package/public/src/theme.js +13 -1
- package/public/src/theme.js.map +1 -1
- package/public/src/theme.ts +15 -1
- package/public/src/types.ts +2 -0
- package/public/src/utils.js +26 -1
- package/public/src/utils.js.map +1 -1
- package/public/src/utils.ts +26 -1
- package/public/src/views/activity.js +1 -0
- package/public/src/views/activity.js.map +1 -1
- package/public/src/views/activity.ts +1 -0
- package/public/src/views/admin.js +134 -0
- package/public/src/views/admin.js.map +1 -1
- package/public/src/views/admin.ts +134 -0
- package/public/src/views/auth.js +31 -0
- package/public/src/views/auth.js.map +1 -1
- package/public/src/views/auth.ts +31 -0
- package/public/src/views/calendar.js +3 -0
- package/public/src/views/calendar.js.map +1 -1
- package/public/src/views/calendar.ts +3 -0
- package/public/src/views/comments-audit.js +1 -0
- package/public/src/views/comments-audit.js.map +1 -1
- package/public/src/views/comments-audit.ts +1 -0
- package/public/src/views/config.js +67 -0
- package/public/src/views/config.js.map +1 -1
- package/public/src/views/config.ts +67 -0
- package/public/src/views/context.js +2 -0
- package/public/src/views/context.js.map +1 -1
- package/public/src/views/context.ts +2 -0
- package/public/src/views/create.js +3 -0
- package/public/src/views/create.js.map +1 -1
- package/public/src/views/create.ts +3 -0
- package/public/src/views/dedupe.js +1 -0
- package/public/src/views/dedupe.js.map +1 -1
- package/public/src/views/dedupe.ts +1 -0
- package/public/src/views/export.js +6 -0
- package/public/src/views/export.js.map +1 -1
- package/public/src/views/export.ts +6 -0
- package/public/src/views/github.js +58 -0
- package/public/src/views/github.js.map +1 -1
- package/public/src/views/github.ts +58 -0
- package/public/src/views/graph-canvas.js +18 -0
- package/public/src/views/graph-canvas.js.map +1 -1
- package/public/src/views/graph-canvas.ts +20 -0
- package/public/src/views/graph.js +41 -0
- package/public/src/views/graph.js.map +1 -1
- package/public/src/views/graph.ts +42 -1
- package/public/src/views/groups.js +45 -0
- package/public/src/views/groups.js.map +1 -1
- package/public/src/views/groups.ts +45 -0
- package/public/src/views/guide.js +3 -0
- package/public/src/views/guide.js.map +1 -1
- package/public/src/views/guide.ts +3 -0
- package/public/src/views/health.js +3 -0
- package/public/src/views/health.js.map +1 -1
- package/public/src/views/health.ts +3 -0
- package/public/src/views/items.js +196 -6
- package/public/src/views/items.js.map +1 -1
- package/public/src/views/items.ts +196 -6
- package/public/src/views/normalize.js +2 -0
- package/public/src/views/normalize.js.map +1 -1
- package/public/src/views/normalize.ts +2 -0
- package/public/src/views/packages.js +67 -5
- package/public/src/views/packages.js.map +1 -1
- package/public/src/views/packages.ts +74 -5
- package/public/src/views/plan-execution.js +6 -0
- package/public/src/views/plan-execution.js.map +1 -1
- package/public/src/views/plan-execution.ts +8 -0
- package/public/src/views/plan.js +145 -0
- package/public/src/views/plan.js.map +1 -1
- package/public/src/views/plan.ts +146 -1
- package/public/src/views/projects.js +60 -0
- package/public/src/views/projects.js.map +1 -1
- package/public/src/views/projects.ts +60 -0
- package/public/src/views/router.js +43 -0
- package/public/src/views/router.js.map +1 -1
- package/public/src/views/router.ts +43 -0
- package/public/src/views/search.js +5 -0
- package/public/src/views/search.js.map +1 -1
- package/public/src/views/search.ts +5 -0
- package/public/src/views/settings.js +36 -0
- package/public/src/views/settings.js.map +1 -1
- package/public/src/views/settings.ts +36 -0
- package/public/src/views/shared.js +1 -0
- package/public/src/views/shared.js.map +1 -1
- package/public/src/views/shared.ts +1 -0
- package/public/src/views/sharing.js +32 -0
- package/public/src/views/sharing.js.map +1 -1
- package/public/src/views/sharing.ts +32 -0
- package/public/src/views/stats.js +1 -0
- package/public/src/views/stats.js.map +1 -1
- package/public/src/views/stats.ts +1 -0
- package/public/src/views/templates.js +4 -0
- package/public/src/views/templates.js.map +1 -1
- package/public/src/views/templates.ts +4 -0
- package/public/src/views/validate.js +1 -0
- package/public/src/views/validate.js.map +1 -1
- package/public/src/views/validate.ts +1 -0
- package/public/sw.js +60 -5
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// spatial partitioning, edge bundling, hierarchical layout
|
|
7
7
|
// ═══════════════════════════════════════════════════════════════
|
|
8
8
|
|
|
9
|
+
/** Display model for a single node rendered on the graph canvas: the id and label plus type, status, lane, degree, and optional tags, priority, and assignee used for sizing and coloring. */
|
|
9
10
|
export interface CanvasNode {
|
|
10
11
|
id: string;
|
|
11
12
|
label: string;
|
|
@@ -26,6 +27,7 @@ export interface CanvasEdge {
|
|
|
26
27
|
|
|
27
28
|
export type LayoutMode = 'force' | 'hierarchical';
|
|
28
29
|
|
|
30
|
+
/** Configuration passed to the GraphCanvas constructor: selection, open, and context-menu callbacks plus optional layout, edge-bundling, and PNG export hooks. */
|
|
29
31
|
export interface GraphCanvasOptions {
|
|
30
32
|
onSelectNode(id: string | null): void;
|
|
31
33
|
onOpenNode(id: string): void;
|
|
@@ -161,6 +163,7 @@ function truncate(text: string, maxLen: number): string {
|
|
|
161
163
|
return text.length > maxLen ? text.slice(0, maxLen - 1) + '…' : text;
|
|
162
164
|
}
|
|
163
165
|
|
|
166
|
+
/** Parses a six-digit `#rrggbb` hex color string into red, green, and blue byte values, returning null when the input does not match that format. */
|
|
164
167
|
function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
|
|
165
168
|
const m = /^#([0-9a-f]{6})$/i.exec(hex);
|
|
166
169
|
if (!m) return null;
|
|
@@ -174,6 +177,7 @@ function hexAlpha(hex: string, a: number): string {
|
|
|
174
177
|
return `rgba(${c.r},${c.g},${c.b},${a})`;
|
|
175
178
|
}
|
|
176
179
|
|
|
180
|
+
/** Computes starting positions for the given nodes arranged along a golden-angle (sunflower) spiral so the force simulation begins evenly spread. */
|
|
177
181
|
function initialPositions(nodes: CanvasNode[]): Array<{ x: number; y: number }> {
|
|
178
182
|
const golden = 2.399963;
|
|
179
183
|
return nodes.map((_, i) => {
|
|
@@ -217,6 +221,7 @@ function convexHull(pts: Array<{ x: number; y: number }>): Array<{ x: number; y:
|
|
|
217
221
|
}
|
|
218
222
|
|
|
219
223
|
// escHtml is imported from utils.ts in graph.ts — canvas uses its own for DOM tooltip only
|
|
224
|
+
/** Escapes the four HTML-significant characters (ampersand, angle brackets, and double quote) so a string can be safely interpolated into DOM markup such as the canvas tooltip. */
|
|
220
225
|
function escHtml(s: string): string {
|
|
221
226
|
return s
|
|
222
227
|
.replace(/&/g, '&')
|
|
@@ -228,6 +233,7 @@ function escHtml(s: string): string {
|
|
|
228
233
|
// ═══════════════════════════════════════════════════════════════
|
|
229
234
|
// GraphCanvas class
|
|
230
235
|
// ═══════════════════════════════════════════════════════════════
|
|
236
|
+
/** Canvas 2D renderer and force-directed physics simulation for the knowledge graph: draws nodes, edges, particles, minimap, and HUD, and handles pan, zoom, drag, select, and keyboard interaction. */
|
|
231
237
|
export class GraphCanvas {
|
|
232
238
|
private canvas: HTMLCanvasElement;
|
|
233
239
|
private ctx: CanvasRenderingContext2D;
|
|
@@ -365,6 +371,7 @@ export class GraphCanvas {
|
|
|
365
371
|
|
|
366
372
|
// ── Public API ─────────────────────────────────────────────
|
|
367
373
|
|
|
374
|
+
/** Loads the given nodes and edges into the simulation, preserving prior node positions where possible, precomputing bidirectional edge pairs, recoloring nodes, scheduling the initial zoom-to-fit, and applying the hierarchical layout when selected. */
|
|
368
375
|
setData(nodes: CanvasNode[], edges: CanvasEdge[]): void {
|
|
369
376
|
const prevPos = new Map(this.nodes.map((n) => [n.id, { x: n.x, y: n.y }]));
|
|
370
377
|
const initPos = initialPositions(nodes);
|
|
@@ -425,6 +432,7 @@ export class GraphCanvas {
|
|
|
425
432
|
this.hideTooltip();
|
|
426
433
|
}
|
|
427
434
|
|
|
435
|
+
/** Merges a partial filter into the current filter, rebuilding keyboard-navigation order, resetting flow particles when the selection changes, and recoloring nodes when the color mode or tag changes. */
|
|
428
436
|
setFilter(filter: Partial<CanvasFilter>): void {
|
|
429
437
|
const prevMode = this.filter.colorMode;
|
|
430
438
|
const prevTag = this.filter.colorTag;
|
|
@@ -440,9 +448,11 @@ export class GraphCanvas {
|
|
|
440
448
|
else if (filter.colorTag !== undefined && filter.colorTag !== prevTag) this.recolorNodes();
|
|
441
449
|
}
|
|
442
450
|
|
|
451
|
+
/** Returns the current tag-to-color map used when coloring nodes by tag. */
|
|
443
452
|
getTagColorMap(): Map<string, string> { return this.tagColorMap; }
|
|
444
453
|
|
|
445
454
|
// Live physics control — used by the physics sliders panel
|
|
455
|
+
/** Updates one or more live physics constants (repulsion, link distance, center gravity, link strength) and reheats the simulation so the new values take effect. */
|
|
446
456
|
setPhysicsParams(params: {
|
|
447
457
|
repulsion?: number;
|
|
448
458
|
linkDistance?: number;
|
|
@@ -456,10 +466,12 @@ export class GraphCanvas {
|
|
|
456
466
|
this.reheat();
|
|
457
467
|
}
|
|
458
468
|
|
|
469
|
+
/** Returns the current physics constants (repulsion, link distance, center force, and link strength) so external sliders can reflect them. */
|
|
459
470
|
getPhysicsParams(): { repulsion: number; linkDistance: number; centerForce: number; linkStrength: number } {
|
|
460
471
|
return { repulsion: this.REPULSE, linkDistance: this.REST_LEN, centerForce: this.CENTER, linkStrength: this.SPRING };
|
|
461
472
|
}
|
|
462
473
|
|
|
474
|
+
/** Sets the selected node id, resets flow particles, and flies the camera to the node when an id is given. */
|
|
463
475
|
setSelected(id: string | null): void {
|
|
464
476
|
this.filter = { ...this.filter, selectedId: id };
|
|
465
477
|
this.particles = [];
|
|
@@ -470,11 +482,13 @@ export class GraphCanvas {
|
|
|
470
482
|
}
|
|
471
483
|
}
|
|
472
484
|
|
|
485
|
+
/** Flies the camera to center on the node with the given id, if it exists. */
|
|
473
486
|
jumpToNode(id: string): void {
|
|
474
487
|
const node = this.nodeMap.get(id);
|
|
475
488
|
if (node) this.flyTo(node);
|
|
476
489
|
}
|
|
477
490
|
|
|
491
|
+
/** Animates the camera to frame all visible nodes with padding by setting a fly-to target computed from their bounding box. */
|
|
478
492
|
fitView(): void {
|
|
479
493
|
if (!this.nodes.length) return;
|
|
480
494
|
const vis = this.filter.visibleNodeIds;
|
|
@@ -499,17 +513,20 @@ export class GraphCanvas {
|
|
|
499
513
|
};
|
|
500
514
|
}
|
|
501
515
|
|
|
516
|
+
/** Pauses or resumes the physics simulation and returns the new paused state; resuming also bumps the simulation alpha. */
|
|
502
517
|
togglePhysics(): boolean {
|
|
503
518
|
this.paused = !this.paused;
|
|
504
519
|
if (!this.paused) this.alpha = Math.max(this.alpha, 0.05);
|
|
505
520
|
return this.paused;
|
|
506
521
|
}
|
|
507
522
|
|
|
523
|
+
/** Resumes the simulation and sets the alpha to 0.3 so nodes re-settle into a new layout. */
|
|
508
524
|
reheat(): void {
|
|
509
525
|
this.alpha = 0.3;
|
|
510
526
|
this.paused = false;
|
|
511
527
|
}
|
|
512
528
|
|
|
529
|
+
/** Switches to the given layout, applying the topological hierarchical layout or, for force mode, jittering node positions and reheating the simulation. */
|
|
513
530
|
setLayout(layout: LayoutMode): void {
|
|
514
531
|
if (this.layout === layout) return;
|
|
515
532
|
this.layout = layout;
|
|
@@ -525,10 +542,12 @@ export class GraphCanvas {
|
|
|
525
542
|
}
|
|
526
543
|
}
|
|
527
544
|
|
|
545
|
+
/** Enables or disables edge bundling, which routes many same-type edges with control points biased toward a shared centroid. */
|
|
528
546
|
setEdgeBundling(enabled: boolean): void {
|
|
529
547
|
this.edgeBundling = enabled;
|
|
530
548
|
}
|
|
531
549
|
|
|
550
|
+
/** Exports the canvas as a PNG, delegating to the configured callback if provided, otherwise triggering a browser download of the canvas image data. */
|
|
532
551
|
exportPng(): void {
|
|
533
552
|
if (this.onExportPng) {
|
|
534
553
|
this.onExportPng(this.canvas);
|
|
@@ -783,6 +802,7 @@ export class GraphCanvas {
|
|
|
783
802
|
return best;
|
|
784
803
|
}
|
|
785
804
|
|
|
805
|
+
/** Tears down the canvas: stops the animation loop and initial-fit timer, aborts bound event listeners, disconnects the ResizeObserver, and removes the canvas and its tooltip from the DOM. */
|
|
786
806
|
destroy(): void {
|
|
787
807
|
this.destroyed = true;
|
|
788
808
|
if (this.rafId !== null) cancelAnimationFrame(this.rafId);
|
|
@@ -50,6 +50,7 @@ function removeCtxMenu() {
|
|
|
50
50
|
ctxMenuEl = null;
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
+
/** Builds and shows the right-click context menu for a graph node, with actions to open the item, select and focus, show its neighborhood, expand two hops, and copy its id. */
|
|
53
54
|
function showCtxMenu(nodeId, x, y) {
|
|
54
55
|
removeCtxMenu();
|
|
55
56
|
const graph = currentGraph?.graph || {};
|
|
@@ -121,6 +122,7 @@ function showCtxMenu(nodeId, x, y) {
|
|
|
121
122
|
function nodeTitle(node) {
|
|
122
123
|
return String(node.properties?.title || node.id);
|
|
123
124
|
}
|
|
125
|
+
/** Returns a display type for a node, preferring the `kind` property, then `type`, then a non-core label, defaulting to 'Item'. */
|
|
124
126
|
function nodeType(node) {
|
|
125
127
|
return String(node.properties?.kind
|
|
126
128
|
|| node.properties?.type
|
|
@@ -143,6 +145,7 @@ function nodeLane(node) {
|
|
|
143
145
|
return 'external';
|
|
144
146
|
return 'item';
|
|
145
147
|
}
|
|
148
|
+
/** Extracts a short user-facing error message from a raw error string (JSON detail, title, or code, otherwise the trimmed raw text), returning an empty string when the error is just the missing pm-graph extension. */
|
|
146
149
|
function compactError(raw) {
|
|
147
150
|
if (!raw)
|
|
148
151
|
return '';
|
|
@@ -157,6 +160,7 @@ function compactError(raw) {
|
|
|
157
160
|
}
|
|
158
161
|
}
|
|
159
162
|
// ── Graph data processing ─────────────────────────────────────
|
|
163
|
+
/** Returns the set of ids directly connected to the given node (plus the node itself) across the provided relationships. */
|
|
160
164
|
function directNeighborIds(nodeId, rels) {
|
|
161
165
|
const ids = new Set([nodeId]);
|
|
162
166
|
for (const r of rels) {
|
|
@@ -167,6 +171,7 @@ function directNeighborIds(nodeId, rels) {
|
|
|
167
171
|
}
|
|
168
172
|
return ids;
|
|
169
173
|
}
|
|
174
|
+
/** Returns the neighbor id(s) on the other end of a relationship relative to a node, filtered by traversal direction (incoming, outgoing, or connected). */
|
|
170
175
|
function walkDirectionMatch(rel, nodeId, dir) {
|
|
171
176
|
if (dir === 'incoming') {
|
|
172
177
|
return rel.to === nodeId ? [rel.from] : [];
|
|
@@ -176,6 +181,7 @@ function walkDirectionMatch(rel, nodeId, dir) {
|
|
|
176
181
|
}
|
|
177
182
|
return rel.to === nodeId ? [rel.from] : rel.from === nodeId ? [rel.to] : [];
|
|
178
183
|
}
|
|
184
|
+
/** Performs a bounded breadth-first traversal out to `depth` hops from a node, returning all reachable ids (including the node itself) while respecting the traversal direction. */
|
|
179
185
|
function expandedNeighborIds(nodeId, rels, depth, direction) {
|
|
180
186
|
const ids = new Set([nodeId]);
|
|
181
187
|
let frontier = new Set([nodeId]);
|
|
@@ -198,6 +204,7 @@ function expandedNeighborIds(nodeId, rels, depth, direction) {
|
|
|
198
204
|
}
|
|
199
205
|
return ids;
|
|
200
206
|
}
|
|
207
|
+
/** Counts both endpoints of every relationship, returning a map of node id to its total edge degree (incrementing the count for both `from` and `to`). */
|
|
201
208
|
function degreeMap(rels) {
|
|
202
209
|
const m = new Map();
|
|
203
210
|
for (const r of rels) {
|
|
@@ -216,6 +223,7 @@ function blockingPair(rel) {
|
|
|
216
223
|
return { blocked: rel.from, blocker: rel.to };
|
|
217
224
|
return null;
|
|
218
225
|
}
|
|
226
|
+
/** Maps a relationship type to a human-readable dependency label (for example 'Depends on'), falling back to the lowercased type with underscores replaced by spaces. */
|
|
219
227
|
function dependencyLabel(rel) {
|
|
220
228
|
const labels = {
|
|
221
229
|
DEPENDS_ON: 'Depends on',
|
|
@@ -230,6 +238,7 @@ function dependencyLabel(rel) {
|
|
|
230
238
|
};
|
|
231
239
|
return labels[rel.type] ?? rel.type.replace(/_/g, ' ').toLowerCase();
|
|
232
240
|
}
|
|
241
|
+
/** Computes the critical (longest) dependency path through the given relationships and returns its node ids as a set, or an empty set when there is no multi-step chain. */
|
|
233
242
|
function computeCriticalPath(rels) {
|
|
234
243
|
const depRels = rels.filter(isDependencyRel);
|
|
235
244
|
if (!depRels.length)
|
|
@@ -247,6 +256,7 @@ function computeCriticalPath(rels) {
|
|
|
247
256
|
allIds.add(pair.blocker);
|
|
248
257
|
}
|
|
249
258
|
const memo = new Map();
|
|
259
|
+
/** Memoized depth-first search returning the longest chain of blockers starting from the given id, guarding against cycles with a visited set. */
|
|
250
260
|
function longestPathFrom(id, seen = new Set()) {
|
|
251
261
|
if (memo.has(id))
|
|
252
262
|
return memo.get(id);
|
|
@@ -271,6 +281,7 @@ function computeCriticalPath(rels) {
|
|
|
271
281
|
.sort((a, b) => b.length - a.length)[0] ?? [];
|
|
272
282
|
return longest.length < 2 ? new Set() : new Set(longest);
|
|
273
283
|
}
|
|
284
|
+
/** Builds per-node dependency stats from blocking relationships, mapping each node id to the sets of items that block it and that it blocks. */
|
|
274
285
|
function blockerStats(rels) {
|
|
275
286
|
const stats = new Map();
|
|
276
287
|
const entry = (id) => {
|
|
@@ -287,6 +298,7 @@ function blockerStats(rels) {
|
|
|
287
298
|
}
|
|
288
299
|
return stats;
|
|
289
300
|
}
|
|
301
|
+
/** Applies the current filters (dep mode, focus neighborhood, node kind, status, text query, relationship type, and direction) to a graph, returning the surviving nodes, relationships, and the set of all connected node ids. */
|
|
290
302
|
function visibleGraph(graph) {
|
|
291
303
|
const nodes = graph.nodes || [];
|
|
292
304
|
let rels = graph.relationships || [];
|
|
@@ -341,6 +353,7 @@ function visibleGraph(graph) {
|
|
|
341
353
|
return { nodes: visNodes, rels: visRels, connected };
|
|
342
354
|
}
|
|
343
355
|
// ── Canvas data conversion ────────────────────────────────────
|
|
356
|
+
/** Converts graph nodes into canvas display nodes, attaching each node's edge degree (computed from the relationships) so the renderer can size them. */
|
|
344
357
|
function toCanvasNodes(nodes, rels) {
|
|
345
358
|
const deg = degreeMap(rels);
|
|
346
359
|
return nodes.map((n) => ({
|
|
@@ -361,6 +374,7 @@ function toCanvasEdges(rels) {
|
|
|
361
374
|
return rels.map((r) => ({ from: r.from, to: r.to, type: r.type }));
|
|
362
375
|
}
|
|
363
376
|
// ── Info panel rendering ──────────────────────────────────────
|
|
377
|
+
/** Renders the graph-coverage HTML: a linked-item percentage bar, the external cross-project reference count, and the top relationship type with its edge count. */
|
|
364
378
|
function renderCoverage(itemNodes, rels, connected, relCounts) {
|
|
365
379
|
const linked = itemNodes.filter((n) => connected.has(n.id)).length;
|
|
366
380
|
const linkedPct = itemNodes.length > 0 ? Math.round((linked / itemNodes.length) * 100) : 0;
|
|
@@ -390,6 +404,7 @@ function renderCoverage(itemNodes, rels, connected, relCounts) {
|
|
|
390
404
|
</div>
|
|
391
405
|
</div>`;
|
|
392
406
|
}
|
|
407
|
+
/** Renders the selected-node HTML panel, including metadata, action buttons, blocker counts, the property grid, tags, an optional description, and its direct relationships. */
|
|
393
408
|
function renderSelectedNode(node, rels, byId, fullItem) {
|
|
394
409
|
if (!node)
|
|
395
410
|
return '<div class="graph-node-empty">Click any node in the graph to inspect it.</div>';
|
|
@@ -454,6 +469,7 @@ function renderSelectedNode(node, rels, byId, fullItem) {
|
|
|
454
469
|
}).join('') + (direct.length > 16 ? `<div class="graph-limit-note">+${direct.length - 16} more — use Focus scope to narrow</div>` : '')}
|
|
455
470
|
</div>`;
|
|
456
471
|
}
|
|
472
|
+
/** Renders the neighborhood HTML for the selected node, showing clickable one-hop and two-hop neighbor chips. */
|
|
457
473
|
function renderPaths(node, rels, byId) {
|
|
458
474
|
if (!node)
|
|
459
475
|
return '<div class="graph-node-empty">Select a node to explore paths.</div>';
|
|
@@ -471,6 +487,7 @@ function renderPaths(node, rels, byId) {
|
|
|
471
487
|
<div class="graph-path-section"><div class="graph-path-label">One hop (${oneHop.size})</div><div class="graph-path-chips">${chips(oneHop) || '<span>None.</span>'}</div></div>
|
|
472
488
|
<div class="graph-path-section"><div class="graph-path-label">Two hops (${twoHop.size})</div><div class="graph-path-chips">${chips(twoHop) || '<span>None.</span>'}</div></div>`;
|
|
473
489
|
}
|
|
490
|
+
/** Renders the item-hubs HTML, listing the top eight most-connected items by degree with their outgoing and incoming edge counts. */
|
|
474
491
|
function renderHubs(nodes, rels) {
|
|
475
492
|
const deg = degreeMap(rels);
|
|
476
493
|
const hubs = nodes.filter(isItemNode)
|
|
@@ -487,6 +504,7 @@ function renderHubs(nodes, rels) {
|
|
|
487
504
|
<small>${e.out}↑ ${e.inn}↓</small>
|
|
488
505
|
</button>`).join('');
|
|
489
506
|
}
|
|
507
|
+
/** Renders the dependency-blocker HTML, listing the top eight items that block or are blocked by the most other items and flagging those on the critical path. */
|
|
490
508
|
function renderBlockingInsights(nodes, rels) {
|
|
491
509
|
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
492
510
|
const stats = blockerStats(rels);
|
|
@@ -504,6 +522,7 @@ function renderBlockingInsights(nodes, rels) {
|
|
|
504
522
|
<small>${row.blockers.size} blockers · ${row.blocked.size} items blocked${criticalPath.has(row.id) ? ' · critical path' : ''}</small>
|
|
505
523
|
</button>`).join('');
|
|
506
524
|
}
|
|
525
|
+
/** Renders the dependency-chains HTML: the longest dependency chains, potential cycle warnings, and root blockers, with critical-path highlighting. */
|
|
507
526
|
function renderDependencyChains(nodes, rels) {
|
|
508
527
|
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
509
528
|
const depRels = rels.filter(isDependencyRel);
|
|
@@ -525,6 +544,7 @@ function renderDependencyChains(nodes, rels) {
|
|
|
525
544
|
.map(([id, blocked]) => ({ id, blocked: blocked.length }))
|
|
526
545
|
.sort((a, b) => b.blocked - a.blocked || nodeTitle(byId.get(a.id) || { id: a.id }).localeCompare(nodeTitle(byId.get(b.id) || { id: b.id })))
|
|
527
546
|
.slice(0, 4);
|
|
547
|
+
/** Depth-first trace of a dependency chain from the given id, recording detected cycle starts and returning the longest chain of blockers found. */
|
|
528
548
|
function traceFrom(id, seen = new Set()) {
|
|
529
549
|
if (seen.has(id)) {
|
|
530
550
|
cycleStarts.add(id);
|
|
@@ -570,6 +590,7 @@ function renderDependencyChains(nodes, rels) {
|
|
|
570
590
|
${leafRows}
|
|
571
591
|
</div>`;
|
|
572
592
|
}
|
|
593
|
+
/** Renders the full graph-analysis info panel HTML by composing the coverage, selected node, neighborhood, hubs, blockers, dependency-chain, and node/relationship count sections. */
|
|
573
594
|
function renderInfoPanel(data, fullItem) {
|
|
574
595
|
const graph = data.graph || {};
|
|
575
596
|
const nodes = graph.nodes || [];
|
|
@@ -607,6 +628,7 @@ function renderInfoPanel(data, fullItem) {
|
|
|
607
628
|
</div>`;
|
|
608
629
|
}
|
|
609
630
|
// ── Rel list (inside bottom drawer) ──────────────────────────
|
|
631
|
+
/** Renders the relationship list shown in the bottom drawer: add/remove dependency buttons and up to 100 filtered relationship rows, each linking its two endpoints. */
|
|
610
632
|
function renderRelList(data) {
|
|
611
633
|
const graph = data.graph || {};
|
|
612
634
|
const nodes = graph.nodes || [];
|
|
@@ -640,6 +662,7 @@ function renderRelList(data) {
|
|
|
640
662
|
return editBtns + rows + limitNote;
|
|
641
663
|
}
|
|
642
664
|
// ── Immersive shell ───────────────────────────────────────────
|
|
665
|
+
/** Returns whether the given preset id (knowledge, dependency, unlinked, metadata, critical, or tags) matches the current filter configuration. */
|
|
643
666
|
function graphPresetActive(id) {
|
|
644
667
|
if (id === 'knowledge') {
|
|
645
668
|
return !filter.depMode && (filter.kind === 'items' || filter.kind === 'all') && filter.rel === 'all' && filter.scope === 'all' && filter.colorMode === 'status';
|
|
@@ -656,6 +679,7 @@ function graphPresetActive(id) {
|
|
|
656
679
|
return !filter.depMode && filter.colorMode === 'tag';
|
|
657
680
|
return false;
|
|
658
681
|
}
|
|
682
|
+
/** Renders the preset rail toolbar HTML, with one button per view preset (Knowledge, Dependencies, Unlinked, Tags, Metadata, Critical) showing live counts and active state. */
|
|
659
683
|
function renderGraphPresets(depRels, isolatedCount) {
|
|
660
684
|
const allNodes = currentGraph?.graph?.nodes ?? [];
|
|
661
685
|
const tagSet = new Set();
|
|
@@ -681,6 +705,7 @@ function renderGraphPresets(depRels, isolatedCount) {
|
|
|
681
705
|
</button>`).join('')}
|
|
682
706
|
</div>`;
|
|
683
707
|
}
|
|
708
|
+
/** Renders the full immersive graph shell HTML: the canvas host, the top HUD bar with search and presets, physics controls, filter overlay, legend, info drawer, and relationship drawer. */
|
|
684
709
|
function renderGraphShell(data) {
|
|
685
710
|
const graph = data.graph || {};
|
|
686
711
|
const nodes = graph.nodes || [];
|
|
@@ -878,6 +903,7 @@ function renderGraphShell(data) {
|
|
|
878
903
|
</div>`;
|
|
879
904
|
}
|
|
880
905
|
// ── Canvas init / update ──────────────────────────────────────
|
|
906
|
+
/** Fetches the full item record for the selected item-lane node and re-renders the info panel with its description; clears the cached item for non-item nodes or on failure. */
|
|
881
907
|
async function fetchAndUpdateSelectedItem(nodeId) {
|
|
882
908
|
if (!state.currentProject || !nodeId)
|
|
883
909
|
return;
|
|
@@ -903,6 +929,7 @@ async function fetchAndUpdateSelectedItem(nodeId) {
|
|
|
903
929
|
selectedItemCache = null;
|
|
904
930
|
}
|
|
905
931
|
}
|
|
932
|
+
/** Pushes the current filters and selection onto the canvas and, when searching with no node selected, jumps the camera to the first matching node. */
|
|
906
933
|
function syncCanvas() {
|
|
907
934
|
if (!canvasRef.current || !currentGraph)
|
|
908
935
|
return;
|
|
@@ -926,6 +953,7 @@ function syncCanvas() {
|
|
|
926
953
|
canvasRef.current.jumpToNode(match.id);
|
|
927
954
|
}
|
|
928
955
|
}
|
|
956
|
+
/** Creates the GraphCanvas instance for the current graph, wires its selection, open, and context-menu callbacks, computes the critical path, loads the data, and applies the initial filter. */
|
|
929
957
|
function initCanvas() {
|
|
930
958
|
const host = document.getElementById('graph-canvas-host');
|
|
931
959
|
if (!host || !currentGraph)
|
|
@@ -965,6 +993,7 @@ function initCanvas() {
|
|
|
965
993
|
syncCanvas();
|
|
966
994
|
}
|
|
967
995
|
// ── Panel / drawer updates ────────────────────────────────────
|
|
996
|
+
/** Re-renders the info panel and relationship list into the DOM from the current graph (and cached selected item) and re-binds their event handlers. */
|
|
968
997
|
function updateInfoPanel() {
|
|
969
998
|
if (!currentGraph)
|
|
970
999
|
return;
|
|
@@ -976,6 +1005,7 @@ function updateInfoPanel() {
|
|
|
976
1005
|
relList.innerHTML = renderRelList(currentGraph);
|
|
977
1006
|
bindInfoPanelEvents();
|
|
978
1007
|
}
|
|
1008
|
+
/** Synchronizes the filter toolbar DOM (direction select enabled state, depth slider and label, scope button text, and preset active states) with the current selection and filter. */
|
|
979
1009
|
function updateFilterToolbarState() {
|
|
980
1010
|
const dirSel = document.getElementById('graph-filter-direction');
|
|
981
1011
|
const depthSldr = document.getElementById('graph-filter-depth');
|
|
@@ -1001,6 +1031,7 @@ const TYPE_COLORS_MAP = {
|
|
|
1001
1031
|
milestone: '#fbbf24', story: '#34d399', chore: '#94a3b8', release: '#38bdf8',
|
|
1002
1032
|
};
|
|
1003
1033
|
const TAG_PALETTE_JS = ['#2dd4bf', '#60a5fa', '#a78bfa', '#f87171', '#fbbf24', '#34d399', '#fb923c', '#e879f9'];
|
|
1034
|
+
/** Computes a tag-to-color map by ranking tags by how often they appear across the nodes and assigning each of the most frequent ones a palette color. */
|
|
1004
1035
|
function computeTagColorMap(nodes) {
|
|
1005
1036
|
const freq = new Map();
|
|
1006
1037
|
for (const n of nodes) {
|
|
@@ -1011,6 +1042,7 @@ function computeTagColorMap(nodes) {
|
|
|
1011
1042
|
const top = [...freq.entries()].sort((a, b) => b[1] - a[1]).slice(0, TAG_PALETTE_JS.length).map(([t]) => t);
|
|
1012
1043
|
return new Map(top.map((t, i) => [t, TAG_PALETTE_JS[i]]));
|
|
1013
1044
|
}
|
|
1045
|
+
/** Rebuilds the legend HUD HTML to match the current color mode (status, type, or tag) or dependency mode, including clickable tag chips when coloring by tag. */
|
|
1014
1046
|
function updateLegend() {
|
|
1015
1047
|
const legend = document.getElementById('graph-legend-hud');
|
|
1016
1048
|
if (!legend)
|
|
@@ -1086,6 +1118,7 @@ function updateLegend() {
|
|
|
1086
1118
|
}
|
|
1087
1119
|
}
|
|
1088
1120
|
// ── Event bindings ────────────────────────────────────────────
|
|
1121
|
+
/** Binds click handlers for the info-panel controls (open and clear the selected item, add and remove dependency) and for neighbor, tag, and relationship rows so they update the selection and canvas. */
|
|
1089
1122
|
function bindInfoPanelEvents() {
|
|
1090
1123
|
document.getElementById('graph-open-selected')?.addEventListener('click', () => {
|
|
1091
1124
|
if (selectedNodeId)
|
|
@@ -1138,6 +1171,7 @@ function bindInfoPanelEvents() {
|
|
|
1138
1171
|
});
|
|
1139
1172
|
});
|
|
1140
1173
|
}
|
|
1174
|
+
/** Binds all HUD control handlers for the graph view: back/refresh/sync/fit buttons, presets, physics toggles and sliders, filter selects, search input, drawer toggles, color mode, and global keyboard shortcuts. */
|
|
1141
1175
|
function bindHudEvents() {
|
|
1142
1176
|
// Back button
|
|
1143
1177
|
document.getElementById('graph-back-btn')?.addEventListener('click', () => {
|
|
@@ -1482,6 +1516,7 @@ function bindHudEvents() {
|
|
|
1482
1516
|
window.__graphKeyHandler = graphKeyHandler;
|
|
1483
1517
|
}
|
|
1484
1518
|
// ── URL routing (pushState) ─────────────────────────────────
|
|
1519
|
+
/** Serializes the current selection and filter into URL query parameters and replaces the browser history entry so the view can be restored or shared. */
|
|
1485
1520
|
function pushGraphState() {
|
|
1486
1521
|
if (!state.currentProject)
|
|
1487
1522
|
return;
|
|
@@ -1505,6 +1540,7 @@ function pushGraphState() {
|
|
|
1505
1540
|
history.replaceState(null, '', url);
|
|
1506
1541
|
}
|
|
1507
1542
|
let urlStateRestored = false;
|
|
1543
|
+
/** Reads graph view parameters from the current URL query string and applies them to the selection and filter, marking state as restored. */
|
|
1508
1544
|
function restoreGraphState() {
|
|
1509
1545
|
const params = new URLSearchParams(window.location.search);
|
|
1510
1546
|
if (!params.has('graph'))
|
|
@@ -1524,6 +1560,7 @@ function restoreGraphState() {
|
|
|
1524
1560
|
filter = { ...filter, layout: (params.get('layout') || 'force') };
|
|
1525
1561
|
}
|
|
1526
1562
|
// ── Dependency editing modals ────────────────────────────────
|
|
1563
|
+
/** Shows the add-dependency modal listing item nodes as source and target selects plus a relationship type, and POSTs the new dependency then refreshes the graph on submit. */
|
|
1527
1564
|
function showAddDependencyModal() {
|
|
1528
1565
|
if (!state.currentProject || !currentGraph)
|
|
1529
1566
|
return;
|
|
@@ -1598,6 +1635,7 @@ function showAddDependencyModal() {
|
|
|
1598
1635
|
}
|
|
1599
1636
|
});
|
|
1600
1637
|
}
|
|
1638
|
+
/** Shows the remove-dependency modal listing existing dependency relationships and DELETEs the chosen one on submit, then refreshes the graph. */
|
|
1601
1639
|
function showRemoveDependencyModal() {
|
|
1602
1640
|
if (!state.currentProject || !currentGraph)
|
|
1603
1641
|
return;
|
|
@@ -1663,6 +1701,7 @@ function showRemoveDependencyModal() {
|
|
|
1663
1701
|
});
|
|
1664
1702
|
}
|
|
1665
1703
|
// ── Main entry point ──────────────────────────────────────────
|
|
1704
|
+
/** Renders the full graph view into the `content-graph` element: clears any prior canvas, restores URL state, fetches the project graph, computes the critical path, renders the shell, binds events, and initializes the canvas. */
|
|
1666
1705
|
export async function renderGraphView() {
|
|
1667
1706
|
const el = document.getElementById('content-graph');
|
|
1668
1707
|
if (!el)
|
|
@@ -1735,6 +1774,7 @@ export async function refreshGraphData() {
|
|
|
1735
1774
|
// ── Local graph (embedded mini-graph for item detail) ─────────
|
|
1736
1775
|
// Registry of active local graph canvases so they can be cleaned up
|
|
1737
1776
|
const localGraphRegistry = new Map();
|
|
1777
|
+
/** Destroys and unregisters the local mini-graph canvas previously created for the given container id, if any. */
|
|
1738
1778
|
export function destroyLocalGraph(containerId) {
|
|
1739
1779
|
const existing = localGraphRegistry.get(containerId);
|
|
1740
1780
|
if (existing) {
|
|
@@ -1742,6 +1782,7 @@ export function destroyLocalGraph(containerId) {
|
|
|
1742
1782
|
localGraphRegistry.delete(containerId);
|
|
1743
1783
|
}
|
|
1744
1784
|
}
|
|
1785
|
+
/** Renders an embedded mini-graph for a single item showing its neighborhood out to the given depth, with neighbor-node clicks navigating to that item's detail view. */
|
|
1745
1786
|
export async function renderLocalGraph(containerId, nodeId, depth = 2) {
|
|
1746
1787
|
const container = document.getElementById(containerId);
|
|
1747
1788
|
if (!container || !state.currentProject)
|