bosun 0.40.2 → 0.40.3

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.
@@ -429,6 +429,7 @@ function defaultMeta() {
429
429
  version: 1,
430
430
  projectId: null,
431
431
  lastFullSync: null,
432
+ epicDependencies: {},
432
433
  sprintOrderMode: "parallel",
433
434
  taskCount: 0,
434
435
  stats: {
@@ -808,6 +809,25 @@ function ensureSprintsMap() {
808
809
  return _store.sprints;
809
810
  }
810
811
 
812
+ function ensureEpicDependenciesMap() {
813
+ if (!_store._meta || typeof _store._meta !== "object") {
814
+ _store._meta = defaultMeta();
815
+ }
816
+ if (!_store._meta.epicDependencies || typeof _store._meta.epicDependencies !== "object") {
817
+ _store._meta.epicDependencies = {};
818
+ }
819
+ const map = _store._meta.epicDependencies;
820
+ for (const [key, value] of Object.entries(map)) {
821
+ const epicId = String(key || "").trim();
822
+ if (!epicId) {
823
+ delete map[key];
824
+ continue;
825
+ }
826
+ map[epicId] = uniqueStringList(Array.isArray(value) ? value : []);
827
+ }
828
+ return map;
829
+ }
830
+
811
831
  export function listSprints() {
812
832
  ensureLoaded();
813
833
  const sprints = Object.values(ensureSprintsMap());
@@ -1693,12 +1713,18 @@ export function getGlobalDagOfDags() {
1693
1713
  return String(a.to).localeCompare(String(b.to));
1694
1714
  });
1695
1715
 
1716
+ const epicDependencies = getEpicDependencies();
1717
+
1696
1718
  return {
1697
1719
  sprintOrderMode: mode,
1698
1720
  sprintCount: nodes.length,
1699
1721
  edgeCount: edges.length,
1722
+ epicDependencies,
1700
1723
  nodes,
1701
- edges,
1724
+ edges: edges.map((edge) => ({
1725
+ ...edge,
1726
+ kind: edge.taskLinks.some((link) => link?.type === 'sequence') ? 'sequential' : 'dependency',
1727
+ })),
1702
1728
  };
1703
1729
  }
1704
1730
 
@@ -1706,6 +1732,54 @@ export function getDagOfDags() {
1706
1732
  return getGlobalDagOfDags();
1707
1733
  }
1708
1734
 
1735
+ export function getEpicDependencies() {
1736
+ ensureLoaded();
1737
+ const map = ensureEpicDependenciesMap();
1738
+ return Object.entries(map).map(([epicId, dependencies]) => ({
1739
+ epicId,
1740
+ dependencies: uniqueStringList(Array.isArray(dependencies) ? dependencies : []),
1741
+ }));
1742
+ }
1743
+
1744
+ export function setEpicDependencies(epicId, dependencies = []) {
1745
+ ensureLoaded();
1746
+ const normalizedEpicId = String(epicId || '').trim();
1747
+ if (!normalizedEpicId) return null;
1748
+ const cleaned = uniqueStringList((Array.isArray(dependencies) ? dependencies : [])
1749
+ .map((entry) => String(entry || '').trim())
1750
+ .filter((entry) => entry && entry !== normalizedEpicId));
1751
+ const map = ensureEpicDependenciesMap();
1752
+ if (cleaned.length > 0) map[normalizedEpicId] = cleaned;
1753
+ else delete map[normalizedEpicId];
1754
+ saveStore();
1755
+ return { epicId: normalizedEpicId, dependencies: [...(map[normalizedEpicId] || [])] };
1756
+ }
1757
+
1758
+ export function addEpicDependency(epicId, dependencyEpicId) {
1759
+ ensureLoaded();
1760
+ const normalizedEpicId = String(epicId || '').trim();
1761
+ const normalizedDependency = String(dependencyEpicId || '').trim();
1762
+ if (!normalizedEpicId || !normalizedDependency || normalizedEpicId === normalizedDependency) return null;
1763
+ const map = ensureEpicDependenciesMap();
1764
+ const next = uniqueStringList([...(map[normalizedEpicId] || []), normalizedDependency]);
1765
+ map[normalizedEpicId] = next;
1766
+ saveStore();
1767
+ return { epicId: normalizedEpicId, dependencies: [...next] };
1768
+ }
1769
+
1770
+ export function removeEpicDependency(epicId, dependencyEpicId) {
1771
+ ensureLoaded();
1772
+ const normalizedEpicId = String(epicId || '').trim();
1773
+ const normalizedDependency = String(dependencyEpicId || '').trim();
1774
+ if (!normalizedEpicId || !normalizedDependency) return null;
1775
+ const map = ensureEpicDependenciesMap();
1776
+ const next = uniqueStringList((map[normalizedEpicId] || []).filter((entry) => entry !== normalizedDependency));
1777
+ if (next.length > 0) map[normalizedEpicId] = next;
1778
+ else delete map[normalizedEpicId];
1779
+ saveStore();
1780
+ return { epicId: normalizedEpicId, dependencies: [...(map[normalizedEpicId] || [])] };
1781
+ }
1782
+
1709
1783
  export function canStartTask(taskId, options = {}) {
1710
1784
  return canTaskStart(taskId, options);
1711
1785
  }
@@ -1721,6 +1795,7 @@ export function canTaskStart(taskId, options = {}) {
1721
1795
  blockingTaskIds: [],
1722
1796
  missingDependencyTaskIds: [],
1723
1797
  blockingSprintIds: [],
1798
+ blockingEpicIds: [],
1724
1799
  sprintOrderMode,
1725
1800
  };
1726
1801
  }
@@ -1732,6 +1807,7 @@ export function canTaskStart(taskId, options = {}) {
1732
1807
  blockingTaskIds: [],
1733
1808
  missingDependencyTaskIds: [],
1734
1809
  blockingSprintIds: [],
1810
+ blockingEpicIds: [],
1735
1811
  sprintOrderMode,
1736
1812
  };
1737
1813
  }
@@ -1756,6 +1832,7 @@ export function canTaskStart(taskId, options = {}) {
1756
1832
  blockingTaskIds: uniqueStringList(blockingTaskIds),
1757
1833
  missingDependencyTaskIds: uniqueStringList(missingDependencyTaskIds),
1758
1834
  blockingSprintIds: [],
1835
+ blockingEpicIds: [],
1759
1836
  sprintOrderMode,
1760
1837
  };
1761
1838
  }
@@ -1787,6 +1864,42 @@ export function canTaskStart(taskId, options = {}) {
1787
1864
  blockingTaskIds: uniqueStringList(blockingTaskIds),
1788
1865
  missingDependencyTaskIds: [],
1789
1866
  blockingSprintIds: [sprintId],
1867
+ blockingEpicIds: [],
1868
+ sprintOrderMode,
1869
+ sprintTaskOrderMode,
1870
+ };
1871
+ }
1872
+ }
1873
+ }
1874
+
1875
+ const taskEpicId = String(task?.epicId || task?.meta?.epicId || '').trim();
1876
+ if (taskEpicId) {
1877
+ const epicDependenciesMap = ensureEpicDependenciesMap();
1878
+ const requiredEpics = uniqueStringList(epicDependenciesMap[taskEpicId] || []);
1879
+ if (requiredEpics.length > 0) {
1880
+ const blockingEpicIds = [];
1881
+ const blockingEpicTaskIds = [];
1882
+ for (const requiredEpicId of requiredEpics) {
1883
+ let hasIncomplete = false;
1884
+ for (const candidate of Object.values(_store.tasks)) {
1885
+ if (!candidate) continue;
1886
+ const candidateEpicId = String(candidate?.epicId || candidate?.meta?.epicId || '').trim();
1887
+ if (candidateEpicId !== requiredEpicId) continue;
1888
+ if (!isTaskTerminal(candidate)) {
1889
+ hasIncomplete = true;
1890
+ blockingEpicTaskIds.push(candidate.id);
1891
+ }
1892
+ }
1893
+ if (hasIncomplete) blockingEpicIds.push(requiredEpicId);
1894
+ }
1895
+ if (blockingEpicIds.length > 0) {
1896
+ return {
1897
+ canStart: false,
1898
+ reason: "epic_dependencies_unresolved",
1899
+ blockingTaskIds: uniqueStringList(blockingEpicTaskIds),
1900
+ missingDependencyTaskIds: [],
1901
+ blockingSprintIds: [],
1902
+ blockingEpicIds: uniqueStringList(blockingEpicIds),
1790
1903
  sprintOrderMode,
1791
1904
  sprintTaskOrderMode,
1792
1905
  };
@@ -1818,6 +1931,7 @@ export function canTaskStart(taskId, options = {}) {
1818
1931
  blockingTaskIds: uniqueStringList(blockingTaskIds),
1819
1932
  missingDependencyTaskIds: [],
1820
1933
  blockingSprintIds: uniqueStringList(blockingSprintIds),
1934
+ blockingEpicIds: [],
1821
1935
  sprintOrderMode,
1822
1936
  sprintTaskOrderMode,
1823
1937
  };
@@ -1831,6 +1945,7 @@ export function canTaskStart(taskId, options = {}) {
1831
1945
  blockingTaskIds: [],
1832
1946
  missingDependencyTaskIds: [],
1833
1947
  blockingSprintIds: [],
1948
+ blockingEpicIds: [],
1834
1949
  sprintOrderMode,
1835
1950
  };
1836
1951
  }
package/ui/demo.html CHANGED
@@ -2964,6 +2964,30 @@
2964
2964
  }
2965
2965
  return { ok: true, data: { nodes, edges } };
2966
2966
  }
2967
+ if (route === '/api/tasks/epic-dependencies') {
2968
+ const map = STATE.epicDependencies && typeof STATE.epicDependencies === 'object'
2969
+ ? STATE.epicDependencies
2970
+ : (STATE.epicDependencies = {});
2971
+ if (method === 'GET') {
2972
+ const data = Object.entries(map).map(([epicId, dependencies]) => ({
2973
+ epicId,
2974
+ dependencies: Array.isArray(dependencies)
2975
+ ? dependencies.map((value) => String(value || '').trim()).filter(Boolean)
2976
+ : [],
2977
+ }));
2978
+ return { ok: true, source: 'demo', data };
2979
+ }
2980
+ if (method === 'PUT') {
2981
+ const epicId = String(body?.epicId || body?.id || '').trim();
2982
+ if (!epicId) return { ok: false, error: 'epicId required' };
2983
+ const dependencies = Array.isArray(body?.dependencies)
2984
+ ? body.dependencies.map((value) => String(value || '').trim()).filter(Boolean).filter((value) => value !== epicId)
2985
+ : [];
2986
+ if (dependencies.length) map[epicId] = [...new Set(dependencies)];
2987
+ else delete map[epicId];
2988
+ return { ok: true, source: 'demo', data: { epicId, dependencies: map[epicId] || [] } };
2989
+ }
2990
+ }
2967
2991
  if (route === '/api/tasks/attachments/upload') {
2968
2992
  const taskId = body?.taskId || body?.id || params.get('taskId');
2969
2993
  const t = findTask(taskId);
@@ -3117,7 +3141,8 @@
3117
3141
  data: t,
3118
3142
  dag: { sprintId: t.sprintId || null, sprint: { nodes: [] }, global: { nodes: [] } },
3119
3143
  };
3120
- } if (route === '/api/tasks/retry') {
3144
+ }
3145
+ if (route === '/api/tasks/retry') {
3121
3146
  const t = findTask(body?.taskId);
3122
3147
  if (t) { t.status = 'todo'; t.updated = Date.now(); addLog('info', 'task-executor', `Task retried: ${t.title}`); }
3123
3148
  return { ok: true, data: t || null };
@@ -5461,14 +5461,49 @@ select.input {
5461
5461
  radial-gradient(circle at 10% 20%, rgba(249, 115, 22, 0.12), transparent 40%),
5462
5462
  radial-gradient(circle at 85% 80%, rgba(56, 189, 248, 0.08), transparent 40%),
5463
5463
  var(--bg-card);
5464
- overflow: auto;
5465
- padding: 10px;
5464
+ overflow: hidden;
5465
+ padding: 8px;
5466
+ min-height: 460px;
5467
+ touch-action: none;
5468
+ cursor: grab;
5469
+ }
5470
+
5471
+ .task-dag-canvas-wrap.is-panning {
5472
+ cursor: grabbing;
5466
5473
  }
5467
5474
 
5468
5475
  .task-dag-canvas {
5469
5476
  width: 100%;
5470
5477
  min-width: 680px;
5471
- height: 420px;
5478
+ height: 460px;
5479
+ }
5480
+
5481
+ .task-dag-header-row {
5482
+ display: flex;
5483
+ flex-wrap: wrap;
5484
+ align-items: center;
5485
+ justify-content: space-between;
5486
+ gap: 8px;
5487
+ margin-bottom: 8px;
5488
+ }
5489
+
5490
+ .task-dag-controls {
5491
+ display: inline-flex;
5492
+ align-items: center;
5493
+ gap: 6px;
5494
+ flex-wrap: wrap;
5495
+ }
5496
+
5497
+ .task-dag-zoom-pill,
5498
+ .task-dag-wire-pill {
5499
+ display: inline-flex;
5500
+ align-items: center;
5501
+ border: 1px solid var(--border);
5502
+ border-radius: 999px;
5503
+ background: var(--bg-card-hover);
5504
+ padding: 3px 8px;
5505
+ font-size: 11px;
5506
+ color: var(--text-secondary);
5472
5507
  }
5473
5508
 
5474
5509
  .dag-node rect {
@@ -5480,6 +5515,11 @@ select.input {
5480
5515
  filter: drop-shadow(0 6px 12px rgba(0, 0, 0, 0.35));
5481
5516
  }
5482
5517
 
5518
+ .dag-node-selected rect {
5519
+ stroke: var(--accent);
5520
+ filter: drop-shadow(0 0 0 2px rgba(56, 189, 248, 0.25));
5521
+ }
5522
+
5483
5523
  @media (max-width: 900px) {
5484
5524
  .task-comment-composer {
5485
5525
  grid-template-columns: 1fr;