@unbrained/pm-web 2026.8.10 → 2026.8.14

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.
Files changed (173) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/app.d.ts +16 -0
  3. package/dist/app.js +16 -0
  4. package/dist/app.js.map +1 -1
  5. package/dist/auth.d.ts +5 -0
  6. package/dist/auth.js.map +1 -1
  7. package/dist/board.d.ts +53 -0
  8. package/dist/board.js +48 -0
  9. package/dist/board.js.map +1 -1
  10. package/dist/crypto.d.ts +23 -0
  11. package/dist/crypto.js +32 -0
  12. package/dist/crypto.js.map +1 -1
  13. package/dist/db.d.ts +19 -0
  14. package/dist/db.js +19 -0
  15. package/dist/db.js.map +1 -1
  16. package/dist/ical.d.ts +72 -0
  17. package/dist/ical.js +73 -0
  18. package/dist/ical.js.map +1 -1
  19. package/dist/index.d.ts +11 -0
  20. package/dist/index.js +52 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/middleware/auth.d.ts +8 -0
  23. package/dist/middleware/auth.js.map +1 -1
  24. package/dist/oidc.d.ts +87 -0
  25. package/dist/oidc.js +113 -0
  26. package/dist/oidc.js.map +1 -1
  27. package/dist/routes/admin.js +15 -0
  28. package/dist/routes/admin.js.map +1 -1
  29. package/dist/routes/github.js +13 -0
  30. package/dist/routes/github.js.map +1 -1
  31. package/dist/routes/oidc.d.ts +10 -0
  32. package/dist/routes/oidc.js +51 -0
  33. package/dist/routes/oidc.js.map +1 -1
  34. package/dist/routes/pm.js +132 -1
  35. package/dist/routes/pm.js.map +1 -1
  36. package/dist/routes/sharing.js +8 -0
  37. package/dist/routes/sharing.js.map +1 -1
  38. package/dist/services/mutation-event-watcher.js +13 -0
  39. package/dist/services/mutation-event-watcher.js.map +1 -1
  40. package/dist/services/pm-runner.d.ts +82 -0
  41. package/dist/services/pm-runner.js +121 -0
  42. package/dist/services/pm-runner.js.map +1 -1
  43. package/dist/services/project-watcher.d.ts +22 -0
  44. package/dist/services/project-watcher.js +51 -0
  45. package/dist/services/project-watcher.js.map +1 -1
  46. package/dist/services/realtime-bus.d.ts +46 -0
  47. package/dist/services/realtime-bus.js +59 -0
  48. package/dist/services/realtime-bus.js.map +1 -1
  49. package/dist/services/sse.d.ts +134 -0
  50. package/dist/services/sse.js +145 -0
  51. package/dist/services/sse.js.map +1 -1
  52. package/manifest.json +1 -1
  53. package/package.json +5 -3
  54. package/public/src/app.js +44 -1
  55. package/public/src/app.js.map +1 -1
  56. package/public/src/app.ts +44 -1
  57. package/public/src/components/modals.js +11 -0
  58. package/public/src/components/modals.js.map +1 -1
  59. package/public/src/components/modals.ts +11 -0
  60. package/public/src/components/toast.js +2 -0
  61. package/public/src/components/toast.js.map +1 -1
  62. package/public/src/components/toast.ts +2 -0
  63. package/public/src/constants.js +21 -4
  64. package/public/src/constants.js.map +1 -1
  65. package/public/src/constants.ts +21 -4
  66. package/public/src/filters.js +10 -7
  67. package/public/src/filters.js.map +1 -1
  68. package/public/src/filters.ts +10 -7
  69. package/public/src/i18n.js +11 -1
  70. package/public/src/i18n.js.map +1 -1
  71. package/public/src/i18n.ts +13 -1
  72. package/public/src/state.js +3 -0
  73. package/public/src/state.js.map +1 -1
  74. package/public/src/state.ts +3 -0
  75. package/public/src/sw.ts +71 -7
  76. package/public/src/theme.js +13 -1
  77. package/public/src/theme.js.map +1 -1
  78. package/public/src/theme.ts +15 -1
  79. package/public/src/types.ts +2 -0
  80. package/public/src/utils.js +26 -1
  81. package/public/src/utils.js.map +1 -1
  82. package/public/src/utils.ts +26 -1
  83. package/public/src/views/activity.js +1 -0
  84. package/public/src/views/activity.js.map +1 -1
  85. package/public/src/views/activity.ts +1 -0
  86. package/public/src/views/admin.js +134 -0
  87. package/public/src/views/admin.js.map +1 -1
  88. package/public/src/views/admin.ts +134 -0
  89. package/public/src/views/auth.js +31 -0
  90. package/public/src/views/auth.js.map +1 -1
  91. package/public/src/views/auth.ts +31 -0
  92. package/public/src/views/calendar.js +3 -0
  93. package/public/src/views/calendar.js.map +1 -1
  94. package/public/src/views/calendar.ts +3 -0
  95. package/public/src/views/comments-audit.js +1 -0
  96. package/public/src/views/comments-audit.js.map +1 -1
  97. package/public/src/views/comments-audit.ts +1 -0
  98. package/public/src/views/config.js +67 -0
  99. package/public/src/views/config.js.map +1 -1
  100. package/public/src/views/config.ts +67 -0
  101. package/public/src/views/context.js +2 -0
  102. package/public/src/views/context.js.map +1 -1
  103. package/public/src/views/context.ts +2 -0
  104. package/public/src/views/create.js +3 -0
  105. package/public/src/views/create.js.map +1 -1
  106. package/public/src/views/create.ts +3 -0
  107. package/public/src/views/dedupe.js +1 -0
  108. package/public/src/views/dedupe.js.map +1 -1
  109. package/public/src/views/dedupe.ts +1 -0
  110. package/public/src/views/export.js +6 -0
  111. package/public/src/views/export.js.map +1 -1
  112. package/public/src/views/export.ts +6 -0
  113. package/public/src/views/github.js +58 -0
  114. package/public/src/views/github.js.map +1 -1
  115. package/public/src/views/github.ts +58 -0
  116. package/public/src/views/graph-canvas.js +18 -0
  117. package/public/src/views/graph-canvas.js.map +1 -1
  118. package/public/src/views/graph-canvas.ts +20 -0
  119. package/public/src/views/graph.js +41 -0
  120. package/public/src/views/graph.js.map +1 -1
  121. package/public/src/views/graph.ts +42 -1
  122. package/public/src/views/groups.js +45 -0
  123. package/public/src/views/groups.js.map +1 -1
  124. package/public/src/views/groups.ts +45 -0
  125. package/public/src/views/guide.js +3 -0
  126. package/public/src/views/guide.js.map +1 -1
  127. package/public/src/views/guide.ts +3 -0
  128. package/public/src/views/health.js +3 -0
  129. package/public/src/views/health.js.map +1 -1
  130. package/public/src/views/health.ts +3 -0
  131. package/public/src/views/items.js +196 -6
  132. package/public/src/views/items.js.map +1 -1
  133. package/public/src/views/items.ts +196 -6
  134. package/public/src/views/normalize.js +2 -0
  135. package/public/src/views/normalize.js.map +1 -1
  136. package/public/src/views/normalize.ts +2 -0
  137. package/public/src/views/packages.js +41 -0
  138. package/public/src/views/packages.js.map +1 -1
  139. package/public/src/views/packages.ts +41 -0
  140. package/public/src/views/plan-execution.js +6 -0
  141. package/public/src/views/plan-execution.js.map +1 -1
  142. package/public/src/views/plan-execution.ts +8 -0
  143. package/public/src/views/plan.js +145 -0
  144. package/public/src/views/plan.js.map +1 -1
  145. package/public/src/views/plan.ts +146 -1
  146. package/public/src/views/projects.js +60 -0
  147. package/public/src/views/projects.js.map +1 -1
  148. package/public/src/views/projects.ts +60 -0
  149. package/public/src/views/router.js +43 -0
  150. package/public/src/views/router.js.map +1 -1
  151. package/public/src/views/router.ts +43 -0
  152. package/public/src/views/search.js +5 -0
  153. package/public/src/views/search.js.map +1 -1
  154. package/public/src/views/search.ts +5 -0
  155. package/public/src/views/settings.js +36 -0
  156. package/public/src/views/settings.js.map +1 -1
  157. package/public/src/views/settings.ts +36 -0
  158. package/public/src/views/shared.js +1 -0
  159. package/public/src/views/shared.js.map +1 -1
  160. package/public/src/views/shared.ts +1 -0
  161. package/public/src/views/sharing.js +32 -0
  162. package/public/src/views/sharing.js.map +1 -1
  163. package/public/src/views/sharing.ts +32 -0
  164. package/public/src/views/stats.js +1 -0
  165. package/public/src/views/stats.js.map +1 -1
  166. package/public/src/views/stats.ts +1 -0
  167. package/public/src/views/templates.js +4 -0
  168. package/public/src/views/templates.js.map +1 -1
  169. package/public/src/views/templates.ts +4 -0
  170. package/public/src/views/validate.js +1 -0
  171. package/public/src/views/validate.js.map +1 -1
  172. package/public/src/views/validate.ts +1 -0
  173. package/public/sw.js +60 -5
@@ -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)