mdboard 1.1.0 → 1.2.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/api.js +752 -0
- package/bin.js +47 -2
- package/config.js +1 -1
- package/index.html +642 -51
- package/init.js +9 -0
- package/package.json +7 -2
- package/scanner.js +491 -0
- package/server.js +238 -630
- package/watcher.js +131 -0
- package/workspace.js +220 -0
- package/yaml.js +129 -0
package/index.html
CHANGED
|
@@ -25,9 +25,23 @@ a{color:var(--accent);text-decoration:none}
|
|
|
25
25
|
|
|
26
26
|
/* ── Layout ──────────────────────────────────────────────── */
|
|
27
27
|
.app{display:flex;height:100vh;overflow:hidden}
|
|
28
|
+
.source-rail{width:56px;background:var(--bg);border-right:1px solid var(--border);display:flex;flex-direction:column;align-items:center;padding:12px 0;gap:8px;flex-shrink:0;overflow-y:auto}
|
|
29
|
+
.source-rail:empty{display:none}
|
|
30
|
+
.rail-icon{width:40px;height:40px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:18px;cursor:pointer;transition:all .15s;position:relative;color:var(--text2);background:var(--surface2);border:2px solid transparent;flex-shrink:0}
|
|
31
|
+
.rail-icon:hover{border-radius:10px;background:var(--surface);color:var(--text)}
|
|
32
|
+
.rail-icon.active{border-color:var(--accent);border-radius:10px;color:var(--text)}
|
|
33
|
+
.rail-icon.active::before{content:'';position:absolute;left:-14px;top:50%;transform:translateY(-50%);width:4px;height:24px;border-radius:0 4px 4px 0;background:var(--accent)}
|
|
34
|
+
.rail-icon img{width:24px;height:24px;border-radius:4px;object-fit:contain}
|
|
35
|
+
.rail-icon svg{width:20px;height:20px}
|
|
36
|
+
.rail-divider{width:24px;height:2px;background:var(--border);border-radius:1px;flex-shrink:0}
|
|
37
|
+
.rail-icon[data-tooltip]{position:relative}
|
|
38
|
+
.rail-icon[data-tooltip]:hover::after{content:attr(data-tooltip);position:absolute;left:calc(100% + 10px);top:50%;transform:translateY(-50%);background:var(--surface);border:1px solid var(--border);color:var(--text);padding:4px 10px;border-radius:var(--radius-sm);font-size:12px;font-weight:500;white-space:nowrap;z-index:50;pointer-events:none;box-shadow:0 4px 16px rgba(0,0,0,.3)}
|
|
28
39
|
.sidebar{width:var(--sidebar-w);background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0}
|
|
29
|
-
.sidebar-logo{padding:20px 16px 16px;font-
|
|
30
|
-
.sidebar-logo
|
|
40
|
+
.sidebar-logo{padding:20px 16px 16px;font-size:14px;font-weight:700;color:var(--text);cursor:pointer;display:flex;align-items:center;gap:10px;transition:opacity .15s}
|
|
41
|
+
.sidebar-logo:hover{opacity:.8}
|
|
42
|
+
.sidebar-logo-icon{width:28px;height:28px;border-radius:var(--radius-sm);display:flex;align-items:center;justify-content:center;font-size:16px;color:var(--accent);background:var(--accent-dim);flex-shrink:0}
|
|
43
|
+
.sidebar-logo img{width:28px;height:28px;border-radius:var(--radius-sm);object-fit:contain}
|
|
44
|
+
.sidebar-logo-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
31
45
|
#sidebar-nav{flex:1;padding:8px}
|
|
32
46
|
#sidebar-nav a{display:flex;align-items:center;gap:10px;padding:8px 12px;border-radius:var(--radius-sm);color:var(--text2);font-size:13px;font-weight:500;transition:all .15s;border-left:2px solid transparent;margin-bottom:2px}
|
|
33
47
|
#sidebar-nav a:hover{color:var(--text);background:var(--surface2)}
|
|
@@ -39,9 +53,6 @@ a{color:var(--accent);text-decoration:none}
|
|
|
39
53
|
|
|
40
54
|
/* ── Header ──────────────────────────────────────────────── */
|
|
41
55
|
.header{padding:16px 24px;border-bottom:1px solid var(--border);background:var(--surface);display:flex;align-items:center;gap:24px;flex-wrap:wrap}
|
|
42
|
-
.header-title{flex-shrink:0}
|
|
43
|
-
.header-title h1{font-size:18px;font-weight:700;line-height:1.3}
|
|
44
|
-
.header-title p{font-size:12px;color:var(--text2);margin-top:2px}
|
|
45
56
|
.header-section{display:flex;flex-direction:column;gap:4px;min-width:120px}
|
|
46
57
|
.header-section-label{font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:var(--text3);font-weight:600}
|
|
47
58
|
.header-section-value{font-size:13px;font-weight:600}
|
|
@@ -207,11 +218,39 @@ th.sorted .sort-arrow{opacity:1;color:var(--accent)}
|
|
|
207
218
|
.loading-container{padding:20px}
|
|
208
219
|
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
|
|
209
220
|
|
|
221
|
+
/* ── Workspace: Source Icon (reused in header chips) ──────── */
|
|
222
|
+
.source-icon{width:20px;height:20px;border-radius:var(--radius-sm);display:flex;align-items:center;justify-content:center;font-size:12px;flex-shrink:0;font-weight:700;line-height:1}
|
|
223
|
+
|
|
224
|
+
/* ── Link Chips ──────────────────────────────────────────── */
|
|
225
|
+
.link-chip{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:500;cursor:pointer;border:1px solid var(--border);background:var(--surface2);color:var(--text2);transition:all .15s;white-space:nowrap}
|
|
226
|
+
.link-chip:hover{background:var(--border);color:var(--text)}
|
|
227
|
+
.link-chip-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}
|
|
228
|
+
.link-chips{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}
|
|
229
|
+
|
|
230
|
+
/* ── CRUD Buttons ────────────────────────────────────────── */
|
|
231
|
+
.btn-sm{padding:5px 12px;font-size:12px;border-radius:var(--radius-sm)}
|
|
232
|
+
.btn-create{background:var(--accent);border-color:var(--accent);color:#fff;font-weight:600}
|
|
233
|
+
.btn-create:hover{opacity:.9}
|
|
234
|
+
.card-readonly{opacity:.75;cursor:default}
|
|
235
|
+
.card-readonly:hover{border-color:var(--border);background:var(--bg)}
|
|
236
|
+
|
|
237
|
+
/* ── Overview: Tracked Milestones ────────────────────────── */
|
|
238
|
+
.tracked-ms{margin-top:12px;padding:12px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm)}
|
|
239
|
+
.tracked-ms-header{font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--text3);font-weight:600;margin-bottom:8px}
|
|
240
|
+
.tracked-ms-item{display:flex;align-items:center;gap:8px;padding:4px 0}
|
|
241
|
+
.tracked-ms-item .dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
|
|
242
|
+
.tracked-ms-item .progress{flex:1;max-width:120px}
|
|
243
|
+
.tracked-ms-item span{font-size:12px}
|
|
244
|
+
.tracked-ms-pct{font-family:var(--mono);font-weight:600;font-size:12px;min-width:36px;text-align:right}
|
|
245
|
+
|
|
210
246
|
/* ── Responsive ──────────────────────────────────────────── */
|
|
211
247
|
@media(max-width:1024px){
|
|
248
|
+
.source-rail{width:48px;padding:8px 0;gap:6px}
|
|
249
|
+
.rail-icon{width:34px;height:34px;font-size:15px;border-radius:10px}
|
|
250
|
+
.rail-icon.active::before{left:-12px;height:18px}
|
|
212
251
|
.sidebar{width:56px}
|
|
213
|
-
.sidebar-logo
|
|
214
|
-
.sidebar-logo{padding:16px 12px;
|
|
252
|
+
.sidebar-logo-text,.sidebar-footer,#sidebar-nav a span{display:none}
|
|
253
|
+
.sidebar-logo{padding:16px 12px;justify-content:center}
|
|
215
254
|
#sidebar-nav a{justify-content:center;padding:10px}
|
|
216
255
|
.header{padding:12px 16px}
|
|
217
256
|
.content{padding:16px}
|
|
@@ -229,9 +268,15 @@ th.sorted .sort-arrow{opacity:1;color:var(--accent)}
|
|
|
229
268
|
</head>
|
|
230
269
|
<body>
|
|
231
270
|
<div class="app">
|
|
271
|
+
<!-- Source Rail (Discord/Slack style) -->
|
|
272
|
+
<nav class="source-rail" id="source-rail"></nav>
|
|
273
|
+
|
|
232
274
|
<!-- Sidebar -->
|
|
233
275
|
<aside class="sidebar">
|
|
234
|
-
<div class="sidebar-logo"
|
|
276
|
+
<div class="sidebar-logo" id="sidebar-logo">
|
|
277
|
+
<span class="sidebar-logo-icon" id="sidebar-logo-icon">■</span>
|
|
278
|
+
<span class="sidebar-logo-text" id="sidebar-logo-text">mdboard</span>
|
|
279
|
+
</div>
|
|
235
280
|
<nav id="sidebar-nav">
|
|
236
281
|
<a href="#board" data-view="board" class="active">
|
|
237
282
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
|
|
@@ -256,10 +301,6 @@ th.sorted .sort-arrow{opacity:1;color:var(--accent)}
|
|
|
256
301
|
<div class="main">
|
|
257
302
|
<!-- Header -->
|
|
258
303
|
<header class="header">
|
|
259
|
-
<div class="header-title">
|
|
260
|
-
<h1 id="h-project-name">Loading...</h1>
|
|
261
|
-
<p id="h-project-desc"></p>
|
|
262
|
-
</div>
|
|
263
304
|
<div class="header-section" id="h-milestone-wrap" style="display:none">
|
|
264
305
|
<span class="header-section-label">Milestone</span>
|
|
265
306
|
<span class="header-section-value" id="h-milestone-name"></span>
|
|
@@ -291,6 +332,9 @@ th.sorted .sort-arrow{opacity:1;color:var(--accent)}
|
|
|
291
332
|
<div id="view-metrics" class="view">
|
|
292
333
|
<div id="metrics-container"></div>
|
|
293
334
|
</div>
|
|
335
|
+
<div id="view-overview" class="view">
|
|
336
|
+
<div id="overview-container"></div>
|
|
337
|
+
</div>
|
|
294
338
|
</div>
|
|
295
339
|
</div>
|
|
296
340
|
</div>
|
|
@@ -371,7 +415,8 @@ function milestoneIcon(status) {
|
|
|
371
415
|
══════════════════════════════════════════════════════════════ */
|
|
372
416
|
var D = {
|
|
373
417
|
config: null, project: null, milestones: [], epics: [], tasks: [],
|
|
374
|
-
sprints: [], allSprints: [], metrics: null, health: null, loaded: false
|
|
418
|
+
sprints: [], allSprints: [], metrics: null, health: null, loaded: false,
|
|
419
|
+
sources: [], activeSource: null, overviewLinks: null
|
|
375
420
|
};
|
|
376
421
|
|
|
377
422
|
/* ── Helpers ─────────────────────────────────────────────── */
|
|
@@ -421,7 +466,8 @@ async function fetchJson(url) {
|
|
|
421
466
|
|
|
422
467
|
async function patchItem(type, id, updates) {
|
|
423
468
|
try {
|
|
424
|
-
var
|
|
469
|
+
var base = apiBase();
|
|
470
|
+
var r = await fetch(base + '/' + type + '/' + encodeURIComponent(id), {
|
|
425
471
|
method: 'PATCH',
|
|
426
472
|
headers: { 'Content-Type': 'application/json' },
|
|
427
473
|
body: JSON.stringify(updates),
|
|
@@ -440,22 +486,82 @@ async function patchItem(type, id, updates) {
|
|
|
440
486
|
}
|
|
441
487
|
}
|
|
442
488
|
|
|
489
|
+
/* ── CRUD API helpers ─────────────────────────────────────── */
|
|
490
|
+
function apiBase() {
|
|
491
|
+
if (D.activeSource) return '/api/sources/' + encodeURIComponent(D.activeSource);
|
|
492
|
+
return '/api';
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
async function createItem(collection, data) {
|
|
496
|
+
try {
|
|
497
|
+
var url = apiBase() + '/' + collection;
|
|
498
|
+
var r = await fetch(url, {
|
|
499
|
+
method: 'POST',
|
|
500
|
+
headers: { 'Content-Type': 'application/json' },
|
|
501
|
+
body: JSON.stringify(data),
|
|
502
|
+
});
|
|
503
|
+
var result = await r.json();
|
|
504
|
+
if (result && result.ok) {
|
|
505
|
+
showToast('Created ' + (result.id || ''), 'success');
|
|
506
|
+
return result;
|
|
507
|
+
} else {
|
|
508
|
+
showToast('Error: ' + (result.error || 'Unknown'), 'error');
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
} catch (e) {
|
|
512
|
+
showToast('Error: ' + e.message, 'error');
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async function deleteItem(collection, id) {
|
|
518
|
+
try {
|
|
519
|
+
var url = apiBase() + '/' + collection + '/' + encodeURIComponent(id);
|
|
520
|
+
var r = await fetch(url, { method: 'DELETE' });
|
|
521
|
+
var result = await r.json();
|
|
522
|
+
if (result && result.ok) {
|
|
523
|
+
showToast('Archived successfully', 'success');
|
|
524
|
+
return true;
|
|
525
|
+
} else {
|
|
526
|
+
showToast('Error: ' + (result.error || 'Unknown'), 'error');
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
} catch (e) {
|
|
530
|
+
showToast('Error: ' + e.message, 'error');
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/* ── Data Loading ────────────────────────────────────────── */
|
|
443
536
|
async function loadAll() {
|
|
537
|
+
var base = apiBase();
|
|
444
538
|
var results = await Promise.all([
|
|
445
|
-
fetchJson('/
|
|
446
|
-
fetchJson('/
|
|
447
|
-
fetchJson('/
|
|
539
|
+
fetchJson(base + '/project'), fetchJson(base + '/milestones'), fetchJson(base + '/epics'),
|
|
540
|
+
fetchJson(base + '/tasks'), fetchJson(base + '/sprint'), fetchJson(base + '/metrics'),
|
|
541
|
+
fetchJson(base + '/health'), fetchJson(base + '/sprints'), fetchJson('/api/config'),
|
|
542
|
+
fetchJson('/api/sources')
|
|
448
543
|
]);
|
|
449
544
|
D.project = results[0]; D.milestones = results[1] || []; D.epics = results[2] || [];
|
|
450
545
|
D.tasks = results[3] || [];
|
|
451
|
-
if (results[4]) D.sprints = [results[4]];
|
|
546
|
+
if (results[4]) D.sprints = [results[4]]; else D.sprints = [];
|
|
452
547
|
D.metrics = results[5]; D.health = results[6];
|
|
453
548
|
D.allSprints = results[7] || [];
|
|
454
549
|
D.config = results[8];
|
|
550
|
+
D.sources = results[9] || [];
|
|
455
551
|
D.loaded = true;
|
|
456
552
|
}
|
|
457
553
|
|
|
458
|
-
function refreshData() {
|
|
554
|
+
function refreshData() {
|
|
555
|
+
loadAll().then(function() {
|
|
556
|
+
initFromConfig();
|
|
557
|
+
renderAll();
|
|
558
|
+
if (hasWorkspace()) buildSourceRail();
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function hasWorkspace() {
|
|
563
|
+
return D.config && D.config.workspace && D.config.workspace.sources && D.config.workspace.sources.length > 0;
|
|
564
|
+
}
|
|
459
565
|
|
|
460
566
|
/* ── Config-driven initialization ────────────────────────── */
|
|
461
567
|
function initFromConfig() {
|
|
@@ -531,6 +637,119 @@ function generateDynamicStyles() {
|
|
|
531
637
|
if (styleEl) styleEl.textContent = css;
|
|
532
638
|
}
|
|
533
639
|
|
|
640
|
+
/* ══════════════════════════════════════════════════════════════
|
|
641
|
+
WORKSPACE — Source switching & sidebar
|
|
642
|
+
══════════════════════════════════════════════════════════════ */
|
|
643
|
+
function renderSidebarLogo() {
|
|
644
|
+
var p = D.project || {};
|
|
645
|
+
var logoEl = document.getElementById('sidebar-logo');
|
|
646
|
+
var iconEl = document.getElementById('sidebar-logo-icon');
|
|
647
|
+
var textEl = document.getElementById('sidebar-logo-text');
|
|
648
|
+
|
|
649
|
+
textEl.textContent = p.name || 'Project';
|
|
650
|
+
|
|
651
|
+
if (D.config && D.config.logo) {
|
|
652
|
+
var logoSrc = D.config.logo.startsWith('http') ? D.config.logo : '/logo';
|
|
653
|
+
iconEl.innerHTML = '<img src="' + escHtml(logoSrc) + '" alt="">';
|
|
654
|
+
} else {
|
|
655
|
+
var initial = (p.name || 'P').charAt(0).toUpperCase();
|
|
656
|
+
iconEl.textContent = initial;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
logoEl.onclick = function() {
|
|
660
|
+
if (hasWorkspace()) {
|
|
661
|
+
switchSource('overview');
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function buildSourceRail() {
|
|
667
|
+
var rail = document.getElementById('source-rail');
|
|
668
|
+
if (!rail) return;
|
|
669
|
+
rail.innerHTML = '';
|
|
670
|
+
|
|
671
|
+
if (!hasWorkspace()) return;
|
|
672
|
+
|
|
673
|
+
// Home icon (overview)
|
|
674
|
+
var home = document.createElement('div');
|
|
675
|
+
home.className = 'rail-icon' + (D.activeSource === 'overview' ? ' active' : '');
|
|
676
|
+
home.dataset.source = 'overview';
|
|
677
|
+
home.setAttribute('data-tooltip', 'Overview');
|
|
678
|
+
home.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>';
|
|
679
|
+
home.onclick = function() { switchSource('overview'); };
|
|
680
|
+
rail.appendChild(home);
|
|
681
|
+
|
|
682
|
+
// Divider
|
|
683
|
+
var divider = document.createElement('div');
|
|
684
|
+
divider.className = 'rail-divider';
|
|
685
|
+
rail.appendChild(divider);
|
|
686
|
+
|
|
687
|
+
// Source icons
|
|
688
|
+
var wsSources = D.config.workspace.sources;
|
|
689
|
+
for (var i = 0; i < wsSources.length; i++) {
|
|
690
|
+
var s = wsSources[i];
|
|
691
|
+
if (s.name === 'overview') continue;
|
|
692
|
+
|
|
693
|
+
var color = s.color || 'var(--accent)';
|
|
694
|
+
var icon = document.createElement('div');
|
|
695
|
+
icon.className = 'rail-icon' + (D.activeSource === s.name ? ' active' : '');
|
|
696
|
+
icon.dataset.source = s.name;
|
|
697
|
+
icon.setAttribute('data-tooltip', s.label || s.name);
|
|
698
|
+
icon.style.background = color + '20';
|
|
699
|
+
icon.style.color = color;
|
|
700
|
+
icon.innerHTML = escHtml(s.icon || s.name.charAt(0).toUpperCase());
|
|
701
|
+
icon.onclick = (function(name) {
|
|
702
|
+
return function() { switchSource(name); };
|
|
703
|
+
})(s.name);
|
|
704
|
+
rail.appendChild(icon);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function switchSource(sourceName) {
|
|
709
|
+
if (sourceName === D.activeSource) return;
|
|
710
|
+
|
|
711
|
+
// Update active source marker
|
|
712
|
+
D.activeSource = sourceName;
|
|
713
|
+
D.loaded = false;
|
|
714
|
+
|
|
715
|
+
// Persist selection
|
|
716
|
+
try { localStorage.setItem('mdboard-source', sourceName); } catch(e) {}
|
|
717
|
+
|
|
718
|
+
// Update source rail active state
|
|
719
|
+
document.querySelectorAll('.rail-icon').forEach(function(el) {
|
|
720
|
+
el.classList.toggle('active', el.dataset.source === sourceName);
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
// If switching to overview, show overview tabs; otherwise show normal tabs
|
|
724
|
+
var isOverview = sourceName === 'overview';
|
|
725
|
+
var viewTabs = document.querySelectorAll('#sidebar-nav a[data-view]');
|
|
726
|
+
viewTabs.forEach(function(tab) {
|
|
727
|
+
var view = tab.dataset.view;
|
|
728
|
+
if (isOverview) {
|
|
729
|
+
tab.style.display = (view === 'overview' || view === 'metrics') ? '' : 'none';
|
|
730
|
+
} else {
|
|
731
|
+
tab.style.display = (view === 'overview') ? 'none' : '';
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
// Switch to appropriate view
|
|
736
|
+
if (isOverview) {
|
|
737
|
+
switchView('overview');
|
|
738
|
+
} else {
|
|
739
|
+
var currentView = document.querySelector('.view.active');
|
|
740
|
+
if (currentView && currentView.id === 'view-overview') {
|
|
741
|
+
switchView('board');
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Reload data for this source
|
|
746
|
+
loadAll().then(function() {
|
|
747
|
+
initFromConfig();
|
|
748
|
+
renderAll();
|
|
749
|
+
buildSourceRail();
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
|
|
534
753
|
/* ── SSE — Hot Reload ────────────────────────────────────── */
|
|
535
754
|
function connectSSE() {
|
|
536
755
|
try {
|
|
@@ -551,12 +770,17 @@ function connectSSE() {
|
|
|
551
770
|
/* ══════════════════════════════════════════════════════════════
|
|
552
771
|
RENDER
|
|
553
772
|
══════════════════════════════════════════════════════════════ */
|
|
554
|
-
function renderAll() {
|
|
773
|
+
function renderAll() {
|
|
774
|
+
renderHeader(); renderBoardFilters(); renderBoard(); renderTableControls(); renderTableBody(); renderMsFilters(); renderMilestones(); renderMetrics();
|
|
775
|
+
// Render overview if active
|
|
776
|
+
var overviewView = document.getElementById('view-overview');
|
|
777
|
+
if (overviewView && overviewView.classList.contains('active')) {
|
|
778
|
+
renderOverview();
|
|
779
|
+
}
|
|
780
|
+
}
|
|
555
781
|
|
|
556
782
|
function renderHeader() {
|
|
557
|
-
|
|
558
|
-
document.getElementById('h-project-name').textContent = p.name || 'Project';
|
|
559
|
-
document.getElementById('h-project-desc').textContent = p.description || '';
|
|
783
|
+
renderSidebarLogo();
|
|
560
784
|
|
|
561
785
|
var mw = document.getElementById('h-milestone-wrap');
|
|
562
786
|
var am = D.milestones.find(function(m) { return m.status === 'active'; });
|
|
@@ -610,7 +834,11 @@ function renderBoardFilters() {
|
|
|
610
834
|
arr.map(function(v) { return '<option value="' + escHtml(v) + '"' + (boardFilters[key] === v ? ' selected' : '') + '>' + escHtml(v) + '</option>'; }).join('') + '</select>';
|
|
611
835
|
}
|
|
612
836
|
|
|
613
|
-
|
|
837
|
+
var createBtn = isSourceWritable() ?
|
|
838
|
+
'<button class="btn btn-sm btn-create" id="board-create-btn">+ New ' + escHtml(ENTITY_NAMES.task ? ENTITY_NAMES.task.singular : 'Task') + '</button>' : '';
|
|
839
|
+
|
|
840
|
+
c.innerHTML = createBtn +
|
|
841
|
+
'<input type="text" data-filter="search" placeholder="Search cards..." value="' + escHtml(boardFilters.search) + '">' +
|
|
614
842
|
'<select data-filter="priority"><option value="">All Priorities</option>' +
|
|
615
843
|
PRIORITIES.map(function(p) { return '<option value="' + p + '"' + (boardFilters.priority === p ? ' selected' : '') + '>' + (PRIORITY_LABELS[p] || p) + '</option>'; }).join('') + '</select>' +
|
|
616
844
|
opts(ep, 'epic', epicPlural) + opts(ms, 'milestone', msPlural);
|
|
@@ -621,6 +849,9 @@ function renderBoardFilters() {
|
|
|
621
849
|
c.querySelectorAll('select[data-filter]').forEach(function(el) {
|
|
622
850
|
el.addEventListener('change', function() { boardFilters[el.dataset.filter] = el.value; renderBoard(); });
|
|
623
851
|
});
|
|
852
|
+
|
|
853
|
+
var btn = document.getElementById('board-create-btn');
|
|
854
|
+
if (btn) btn.addEventListener('click', function() { openCreateDialog('tasks'); });
|
|
624
855
|
}
|
|
625
856
|
|
|
626
857
|
function getFilteredBoardTasks() {
|
|
@@ -769,7 +1000,11 @@ function renderTableControls() {
|
|
|
769
1000
|
return '<select data-tf="' + filter + '"><option value="">All ' + label + '</option>' +
|
|
770
1001
|
arr.map(function(v) { return '<option value="' + escHtml(v) + '"' + (tableFilters[filter] === v ? ' selected' : '') + '>' + escHtml(v) + '</option>'; }).join('') + '</select>';
|
|
771
1002
|
}
|
|
772
|
-
|
|
1003
|
+
var createBtn = isSourceWritable() ?
|
|
1004
|
+
'<button class="btn btn-sm btn-create" id="table-create-btn">+ New ' + escHtml(ENTITY_NAMES.task ? ENTITY_NAMES.task.singular : 'Task') + '</button>' : '';
|
|
1005
|
+
|
|
1006
|
+
c.innerHTML = createBtn +
|
|
1007
|
+
'<input type="text" data-tf="search" placeholder="Search ID or title..." value="' + escHtml(tableFilters.search) + '">' +
|
|
773
1008
|
opts(ms, 'milestone', msPlural) + opts(ep, 'epic', epicPlural) + opts(st, 'status', 'Statuses') + opts(sp, 'sprint', sprintPlural) +
|
|
774
1009
|
'<select data-tf="priority"><option value="">All Priorities</option>' +
|
|
775
1010
|
PRIORITIES.map(function(p) { return '<option value="' + p + '"' + (tableFilters.priority === p ? ' selected' : '') + '>' + (PRIORITY_LABELS[p] || p) + '</option>'; }).join('') + '</select>';
|
|
@@ -778,6 +1013,9 @@ function renderTableControls() {
|
|
|
778
1013
|
c.querySelectorAll('select[data-tf]').forEach(function(sel) {
|
|
779
1014
|
sel.addEventListener('change', function() { tableFilters[sel.dataset.tf] = sel.value; renderTableBody(); });
|
|
780
1015
|
});
|
|
1016
|
+
|
|
1017
|
+
var btn = document.getElementById('table-create-btn');
|
|
1018
|
+
if (btn) btn.addEventListener('click', function() { openCreateDialog('tasks'); });
|
|
781
1019
|
}
|
|
782
1020
|
|
|
783
1021
|
function getFilteredTasks() {
|
|
@@ -962,12 +1200,162 @@ function renderMetrics() {
|
|
|
962
1200
|
}
|
|
963
1201
|
|
|
964
1202
|
/* ══════════════════════════════════════════════════════════════
|
|
965
|
-
|
|
1203
|
+
CRUD HELPERS
|
|
1204
|
+
══════════════════════════════════════════════════════════════ */
|
|
1205
|
+
function isSourceWritable() {
|
|
1206
|
+
if (!hasWorkspace() || !D.activeSource) return true; // legacy mode = writable
|
|
1207
|
+
var src = D.config.workspace.sources.find(function(s) { return s.name === D.activeSource; });
|
|
1208
|
+
return src ? !src.readonly : true;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function openCreateDialog(collection) {
|
|
1212
|
+
var item = { _isNew: true };
|
|
1213
|
+
|
|
1214
|
+
if (collection === 'tasks') {
|
|
1215
|
+
// Pre-fill milestone and epic from available ones
|
|
1216
|
+
var ms = D.milestones.length > 0 ? (D.milestones[0].id || D.milestones[0]._dir || '') : '';
|
|
1217
|
+
var ep = D.epics.length > 0 ? (D.epics[0]._dir || D.epics[0].id || '') : '';
|
|
1218
|
+
item.title = '';
|
|
1219
|
+
item.status = 'backlog';
|
|
1220
|
+
item.priority = '';
|
|
1221
|
+
item.points = null;
|
|
1222
|
+
item.assigned = '';
|
|
1223
|
+
item.sprint = '';
|
|
1224
|
+
item.milestone = ms;
|
|
1225
|
+
item.epic = ep;
|
|
1226
|
+
item.content = '';
|
|
1227
|
+
} else if (collection === 'milestones') {
|
|
1228
|
+
item.title = '';
|
|
1229
|
+
item.status = 'planned';
|
|
1230
|
+
item.deadline = '';
|
|
1231
|
+
item.content = '';
|
|
1232
|
+
} else if (collection === 'epics') {
|
|
1233
|
+
var ms = D.milestones.length > 0 ? (D.milestones[0]._dir || D.milestones[0].id || '') : '';
|
|
1234
|
+
item.title = '';
|
|
1235
|
+
item.status = 'active';
|
|
1236
|
+
item.priority = '';
|
|
1237
|
+
item.milestone = ms;
|
|
1238
|
+
item.content = '';
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
panelState = { open: true, type: collection, item: item, isCreate: true };
|
|
1242
|
+
renderPanel();
|
|
1243
|
+
document.getElementById('detail-panel').classList.add('open');
|
|
1244
|
+
document.getElementById('panel-overlay').classList.add('open');
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
/* ══════════════════════════════════════════════════════════════
|
|
1248
|
+
OVERVIEW VIEW
|
|
1249
|
+
══════════════════════════════════════════════════════════════ */
|
|
1250
|
+
async function renderOverview() {
|
|
1251
|
+
var c = document.getElementById('overview-container');
|
|
1252
|
+
if (!D.loaded) { c.innerHTML = '<div class="loading-container"><div class="skeleton skeleton-card" style="height:200px"></div></div>'; return; }
|
|
1253
|
+
|
|
1254
|
+
// Fetch overview data
|
|
1255
|
+
var overviewMs = await fetchJson('/api/overview/milestones') || [];
|
|
1256
|
+
var overviewLinks = await fetchJson('/api/overview/links');
|
|
1257
|
+
var overviewMetrics = await fetchJson('/api/overview/metrics');
|
|
1258
|
+
|
|
1259
|
+
D.overviewLinks = overviewLinks;
|
|
1260
|
+
|
|
1261
|
+
var html = '<h2 style="margin-bottom:16px;font-size:16px;font-weight:700">Workspace Overview</h2>';
|
|
1262
|
+
|
|
1263
|
+
// Global Milestones
|
|
1264
|
+
html += '<h3 style="font-size:13px;text-transform:uppercase;letter-spacing:.04em;color:var(--text2);font-weight:600;margin:16px 0 8px">Global Milestones</h3>';
|
|
1265
|
+
if (overviewMs.length > 0) {
|
|
1266
|
+
html += overviewMs.map(function(ms) {
|
|
1267
|
+
var pct = ms.combinedProgress != null ? ms.combinedProgress : (ms.progress || 0);
|
|
1268
|
+
var tracked = ms.tracked || [];
|
|
1269
|
+
var trackedHtml = '';
|
|
1270
|
+
if (tracked.length > 0) {
|
|
1271
|
+
trackedHtml = '<div class="tracked-ms"><div class="tracked-ms-header">Tracked Sub-Milestones</div>' +
|
|
1272
|
+
tracked.map(function(t) {
|
|
1273
|
+
return '<div class="tracked-ms-item"><span class="dot" style="background:' + (t.sourceColor || 'var(--accent)') + '"></span>' +
|
|
1274
|
+
'<span style="font-size:12px;flex:1">' + escHtml(t.title || t.id || '') + '</span>' +
|
|
1275
|
+
'<div class="progress progress-accent" style="width:80px"><div class="progress-fill" style="width:' + t.progress + '%;background:' + (t.sourceColor || 'var(--accent)') + '"></div></div>' +
|
|
1276
|
+
'<span class="tracked-ms-pct">' + t.progress + '%</span></div>';
|
|
1277
|
+
}).join('') + '</div>';
|
|
1278
|
+
}
|
|
1279
|
+
return '<div class="ms-card">' +
|
|
1280
|
+
'<div class="ms-header"><h2>' + milestoneIcon(ms.status) + ' ' + escHtml(ms.title || ms.id || '') + '</h2>' +
|
|
1281
|
+
'<span class="badge ' + badgeClass('badge', ms.status) + '">' + escHtml(ms.status || '') + '</span>' +
|
|
1282
|
+
(ms.deadline ? '<span class="ms-deadline">' + fmtDate(ms.deadline) + '</span>' : '') + '</div>' +
|
|
1283
|
+
'<div class="ms-progress"><div class="progress-label"><span>' + (ms.completedCount || 0) + ' / ' + (ms.featureCount || 0) + ' tasks</span><span>' + pct + '%</span></div>' +
|
|
1284
|
+
'<div class="progress progress-lg progress-success"><div class="progress-fill" style="width:' + pct + '%"></div></div></div>' +
|
|
1285
|
+
trackedHtml + '</div>';
|
|
1286
|
+
}).join('');
|
|
1287
|
+
} else {
|
|
1288
|
+
html += '<div style="color:var(--text3);padding:8px 0">No milestones found across sources.</div>';
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// Cross-project links
|
|
1292
|
+
if (overviewLinks && overviewLinks.links && overviewLinks.links.length > 0) {
|
|
1293
|
+
html += '<h3 style="font-size:13px;text-transform:uppercase;letter-spacing:.04em;color:var(--text2);font-weight:600;margin:24px 0 8px">Cross-Project Links</h3>';
|
|
1294
|
+
html += '<div class="metrics-grid">';
|
|
1295
|
+
var linkGroups = {};
|
|
1296
|
+
overviewLinks.links.forEach(function(l) {
|
|
1297
|
+
var key = (l.fromSource || 'unknown');
|
|
1298
|
+
if (!linkGroups[key]) linkGroups[key] = [];
|
|
1299
|
+
linkGroups[key].push(l);
|
|
1300
|
+
});
|
|
1301
|
+
Object.keys(linkGroups).forEach(function(src) {
|
|
1302
|
+
var links = linkGroups[src];
|
|
1303
|
+
html += '<div class="metric-card"><h3>' + escHtml(src) + ' Links</h3>';
|
|
1304
|
+
html += links.map(function(l) {
|
|
1305
|
+
return '<div class="health-row"><span style="font-size:12px;font-family:var(--mono)">' + escHtml(l.from || '') + '</span>' +
|
|
1306
|
+
'<span style="color:var(--text3);margin:0 4px">→</span>' +
|
|
1307
|
+
'<span class="link-chip" data-link="' + escHtml(l.to || '') + '">' +
|
|
1308
|
+
'<span class="link-chip-dot" style="background:var(--accent)"></span>' + escHtml(l.to || '') + '</span></div>';
|
|
1309
|
+
}).join('');
|
|
1310
|
+
html += '</div>';
|
|
1311
|
+
});
|
|
1312
|
+
html += '</div>';
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// Source metrics
|
|
1316
|
+
if (overviewMetrics && overviewMetrics.sources) {
|
|
1317
|
+
html += '<h3 style="font-size:13px;text-transform:uppercase;letter-spacing:.04em;color:var(--text2);font-weight:600;margin:24px 0 8px">Source Metrics</h3>';
|
|
1318
|
+
html += '<div class="metrics-grid">';
|
|
1319
|
+
Object.keys(overviewMetrics.sources).forEach(function(key) {
|
|
1320
|
+
var m = overviewMetrics.sources[key];
|
|
1321
|
+
var pct = m.totalTasks > 0 ? Math.round((m.completedTasks / m.totalTasks) * 100) : 0;
|
|
1322
|
+
html += '<div class="metric-card" style="border-left:3px solid ' + (m.color || 'var(--accent)') + '">' +
|
|
1323
|
+
'<h3>' + escHtml(m.label || key) + '</h3>' +
|
|
1324
|
+
'<div class="health-row"><div class="health-label">Tasks</div><div class="health-val">' + m.totalTasks + '</div></div>' +
|
|
1325
|
+
'<div class="health-row"><div class="health-label">Completed</div><div class="health-val">' + m.completedTasks + '</div></div>' +
|
|
1326
|
+
'<div class="health-row"><div class="health-label">Points</div><div class="health-val">' + m.totalPoints + '</div></div>' +
|
|
1327
|
+
'<div class="progress progress-lg progress-success" style="margin-top:8px"><div class="progress-fill" style="width:' + pct + '%;background:' + (m.color || 'var(--success)') + '"></div></div>' +
|
|
1328
|
+
'</div>';
|
|
1329
|
+
});
|
|
1330
|
+
html += '</div>';
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
c.innerHTML = html;
|
|
1334
|
+
|
|
1335
|
+
// Click handlers for link chips
|
|
1336
|
+
c.querySelectorAll('.link-chip[data-link]').forEach(function(chip) {
|
|
1337
|
+
chip.addEventListener('click', function() {
|
|
1338
|
+
var ref = chip.dataset.link;
|
|
1339
|
+
var parts = ref.split(':');
|
|
1340
|
+
if (parts.length === 2) {
|
|
1341
|
+
switchSource(parts[0]);
|
|
1342
|
+
// After switch, try to find and open the item
|
|
1343
|
+
setTimeout(function() {
|
|
1344
|
+
var task = D.tasks.find(function(t) { return t.id === ref || t.id === parts[1]; });
|
|
1345
|
+
if (task) openPanel('tasks', task);
|
|
1346
|
+
}, 500);
|
|
1347
|
+
}
|
|
1348
|
+
});
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
/* ══════════════════════════════════════════════════════════════
|
|
1353
|
+
DETAIL PANEL — Edit/Create tasks, epics, milestones
|
|
966
1354
|
══════════════════════════════════════════════════════════════ */
|
|
967
|
-
var panelState = { open: false, type: null, item: null };
|
|
1355
|
+
var panelState = { open: false, type: null, item: null, isCreate: false };
|
|
968
1356
|
|
|
969
1357
|
function openPanel(type, item) {
|
|
970
|
-
panelState = { open: true, type: type, item: JSON.parse(JSON.stringify(item)) };
|
|
1358
|
+
panelState = { open: true, type: type, item: JSON.parse(JSON.stringify(item)), isCreate: false };
|
|
971
1359
|
renderPanel();
|
|
972
1360
|
document.getElementById('detail-panel').classList.add('open');
|
|
973
1361
|
document.getElementById('panel-overlay').classList.add('open');
|
|
@@ -983,23 +1371,31 @@ function renderPanel() {
|
|
|
983
1371
|
var panel = document.getElementById('detail-panel');
|
|
984
1372
|
var t = panelState.type;
|
|
985
1373
|
var item = panelState.item;
|
|
1374
|
+
var isCreate = panelState.isCreate;
|
|
986
1375
|
if (!item) return;
|
|
987
1376
|
|
|
1377
|
+
var isReadonly = !isCreate && item.readonly;
|
|
1378
|
+
|
|
988
1379
|
var typeLabel;
|
|
989
1380
|
if (t === 'tasks') typeLabel = ENTITY_NAMES.task ? ENTITY_NAMES.task.singular : 'Task';
|
|
990
1381
|
else if (t === 'epics') typeLabel = ENTITY_NAMES.epic ? ENTITY_NAMES.epic.singular : 'Epic';
|
|
991
1382
|
else typeLabel = ENTITY_NAMES.milestone ? ENTITY_NAMES.milestone.singular : 'Milestone';
|
|
992
1383
|
|
|
993
1384
|
var html = '<div class="panel-header">' +
|
|
994
|
-
'<span class="panel-type">' + escHtml(typeLabel) + '</span>' +
|
|
995
|
-
'<span class="panel-item-id">' + escHtml(item.id || '') + '</span>'
|
|
996
|
-
|
|
997
|
-
|
|
1385
|
+
'<span class="panel-type">' + escHtml(isCreate ? 'New ' + typeLabel : typeLabel) + '</span>' +
|
|
1386
|
+
'<span class="panel-item-id">' + escHtml(isCreate ? '' : (item.id || '')) + '</span>';
|
|
1387
|
+
|
|
1388
|
+
// Source badge
|
|
1389
|
+
if (item.source && item.sourceColor) {
|
|
1390
|
+
html += '<span class="pill" style="background:' + item.sourceColor + '20;color:' + item.sourceColor + ';font-size:10px">' + escHtml(item.sourceLabel || item.source) + '</span>';
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
html += '<button class="panel-close" id="panel-close-btn">×</button></div>';
|
|
998
1394
|
|
|
999
1395
|
html += '<div class="panel-body">';
|
|
1000
1396
|
|
|
1001
1397
|
// Title
|
|
1002
|
-
html += '<div class="panel-field"><label>Title</label><input type="text" id="p-title" value="' + escHtml(item.title || '') + '"></div>';
|
|
1398
|
+
html += '<div class="panel-field"><label>Title</label><input type="text" id="p-title" value="' + escHtml(item.title || '') + '"' + (isReadonly ? ' disabled' : '') + '></div>';
|
|
1003
1399
|
|
|
1004
1400
|
// Status + Priority row
|
|
1005
1401
|
html += '<div class="panel-props">';
|
|
@@ -1027,14 +1423,14 @@ function renderPanel() {
|
|
|
1027
1423
|
}
|
|
1028
1424
|
|
|
1029
1425
|
html += '<div class="panel-field"><label>Status</label><div class="field-with-icon"><span id="p-status-icon">' + statusIcon(item.status) + '</span>' +
|
|
1030
|
-
'<select id="p-status">' + statusOptions.map(function(s) {
|
|
1426
|
+
'<select id="p-status"' + (isReadonly ? ' disabled' : '') + '>' + statusOptions.map(function(s) {
|
|
1031
1427
|
return '<option value="' + s + '"' + (item.status === s ? ' selected' : '') + '>' + (statusLabels[s] || s) + '</option>';
|
|
1032
1428
|
}).join('') + '</select></div></div>';
|
|
1033
1429
|
|
|
1034
1430
|
// Priority
|
|
1035
1431
|
if (t === 'tasks' || t === 'epics') {
|
|
1036
1432
|
html += '<div class="panel-field"><label>Priority</label><div class="field-with-icon"><span id="p-priority-icon">' + priorityIcon(item.priority) + '</span>' +
|
|
1037
|
-
'<select id="p-priority"><option value="">None</option>' + PRIORITIES.map(function(p) {
|
|
1433
|
+
'<select id="p-priority"' + (isReadonly ? ' disabled' : '') + '><option value="">None</option>' + PRIORITIES.map(function(p) {
|
|
1038
1434
|
return '<option value="' + p + '"' + (item.priority === p ? ' selected' : '') + '>' + (PRIORITY_LABELS[p] || p) + '</option>';
|
|
1039
1435
|
}).join('') + '</select></div></div>';
|
|
1040
1436
|
}
|
|
@@ -1044,43 +1440,113 @@ function renderPanel() {
|
|
|
1044
1440
|
// Type-specific fields
|
|
1045
1441
|
if (t === 'tasks') {
|
|
1046
1442
|
html += '<div class="panel-props">';
|
|
1047
|
-
html += '<div class="panel-field"><label>Points</label><input type="number" id="p-points" min="0" max="100" value="' + (item.points != null ? item.points : '') + '"></div>';
|
|
1443
|
+
html += '<div class="panel-field"><label>Points</label><input type="number" id="p-points" min="0" max="100" value="' + (item.points != null ? item.points : '') + '"' + (isReadonly ? ' disabled' : '') + '></div>';
|
|
1048
1444
|
|
|
1049
1445
|
var assignedVal = Array.isArray(item.assigned) ? item.assigned.join(', ') : (item.assigned || '');
|
|
1050
|
-
html += '<div class="panel-field"><label>Assigned</label><input type="text" id="p-assigned" value="' + escHtml(assignedVal) + '" placeholder="agent-name"></div>';
|
|
1446
|
+
html += '<div class="panel-field"><label>Assigned</label><input type="text" id="p-assigned" value="' + escHtml(assignedVal) + '" placeholder="agent-name"' + (isReadonly ? ' disabled' : '') + '></div>';
|
|
1051
1447
|
html += '</div>';
|
|
1052
1448
|
|
|
1053
1449
|
html += '<div class="panel-props">';
|
|
1054
1450
|
// Sprint
|
|
1055
|
-
html += '<div class="panel-field"><label>Sprint</label><select id="p-sprint"><option value="">None</option>' +
|
|
1451
|
+
html += '<div class="panel-field"><label>Sprint</label><select id="p-sprint"' + (isReadonly ? ' disabled' : '') + '><option value="">None</option>' +
|
|
1056
1452
|
D.allSprints.map(function(s) { return '<option value="' + escHtml(s.id || '') + '"' + (item.sprint === s.id ? ' selected' : '') + '>' + escHtml(s.id || '') + '</option>'; }).join('') +
|
|
1057
1453
|
'</select></div>';
|
|
1058
1454
|
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1455
|
+
if (isCreate) {
|
|
1456
|
+
// Milestone and Epic as editable dropdowns for create mode
|
|
1457
|
+
html += '<div class="panel-field"><label>Milestone</label><select id="p-milestone">' +
|
|
1458
|
+
D.milestones.map(function(m) { return '<option value="' + escHtml(m._dir || m.id || '') + '"' + (item.milestone === (m._dir || m.id) ? ' selected' : '') + '>' + escHtml(m.title || m.id || '') + '</option>'; }).join('') +
|
|
1459
|
+
'</select></div>';
|
|
1460
|
+
html += '</div>';
|
|
1461
|
+
html += '<div class="panel-field"><label>Epic</label><select id="p-epic-select">' +
|
|
1462
|
+
D.epics.map(function(e) { return '<option value="' + escHtml(e._dir || e.id || '') + '"' + (item.epic === (e._dir || e.id) ? ' selected' : '') + '>' + escHtml(e.title || e.id || '') + '</option>'; }).join('') +
|
|
1463
|
+
'</select></div>';
|
|
1464
|
+
} else {
|
|
1465
|
+
// Epic (read-only info)
|
|
1466
|
+
html += '<div class="panel-field"><label>Epic</label><input type="text" id="p-epic" value="' + escHtml(item.epic || '') + '" disabled style="opacity:.6" title="Epic is determined by file location"></div>';
|
|
1467
|
+
html += '</div>';
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
// Links rendering
|
|
1471
|
+
if (!isCreate && item.links && Array.isArray(item.links) && item.links.length > 0) {
|
|
1472
|
+
html += '<div class="panel-field"><label>Links</label><div class="link-chips">';
|
|
1473
|
+
item.links.forEach(function(link) {
|
|
1474
|
+
var parts = String(link).split(':');
|
|
1475
|
+
var srcName = parts.length === 2 ? parts[0] : null;
|
|
1476
|
+
var srcColor = 'var(--accent)';
|
|
1477
|
+
if (srcName && D.config && D.config.workspace && D.config.workspace.sources) {
|
|
1478
|
+
var src = D.config.workspace.sources.find(function(s) { return s.name === srcName; });
|
|
1479
|
+
if (src && src.color) srcColor = src.color;
|
|
1480
|
+
}
|
|
1481
|
+
html += '<span class="link-chip" data-link="' + escHtml(String(link)) + '">' +
|
|
1482
|
+
'<span class="link-chip-dot" style="background:' + srcColor + '"></span>' +
|
|
1483
|
+
escHtml(String(link)) + '</span>';
|
|
1484
|
+
});
|
|
1485
|
+
html += '</div></div>';
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
// Reverse links
|
|
1489
|
+
if (!isCreate && D.overviewLinks && D.overviewLinks.reverseLinks && item.id) {
|
|
1490
|
+
var reverseRefs = D.overviewLinks.reverseLinks[item.id] || [];
|
|
1491
|
+
if (reverseRefs.length > 0) {
|
|
1492
|
+
html += '<div class="panel-field"><label>Referenced By</label><div class="link-chips">';
|
|
1493
|
+
reverseRefs.forEach(function(ref) {
|
|
1494
|
+
var srcColor = ref.sourceColor || 'var(--accent)';
|
|
1495
|
+
html += '<span class="link-chip" data-link="' + escHtml(ref.from || '') + '">' +
|
|
1496
|
+
'<span class="link-chip-dot" style="background:' + srcColor + '"></span>' +
|
|
1497
|
+
escHtml(ref.from || '') + '</span>';
|
|
1498
|
+
});
|
|
1499
|
+
html += '</div></div>';
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1062
1502
|
}
|
|
1063
1503
|
|
|
1064
1504
|
if (t === 'milestones') {
|
|
1065
|
-
html += '<div class="panel-field"><label>Deadline</label><input type="date" id="p-deadline" value="' + fmtDate(item.deadline) + '"></div>';
|
|
1505
|
+
html += '<div class="panel-field"><label>Deadline</label><input type="date" id="p-deadline" value="' + fmtDate(item.deadline) + '"' + (isReadonly ? ' disabled' : '') + '></div>';
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
if (isCreate && (t === 'epics' || t === 'milestones')) {
|
|
1509
|
+
if (t === 'epics') {
|
|
1510
|
+
html += '<div class="panel-field"><label>Milestone</label><select id="p-milestone">' +
|
|
1511
|
+
D.milestones.map(function(m) { return '<option value="' + escHtml(m._dir || m.id || '') + '">' + escHtml(m.title || m.id || '') + '</option>'; }).join('') +
|
|
1512
|
+
'</select></div>';
|
|
1513
|
+
}
|
|
1066
1514
|
}
|
|
1067
1515
|
|
|
1068
1516
|
// Description / Content
|
|
1069
|
-
html += '<div class="panel-field"><label>Description</label><textarea id="p-content">' + escHtml(item.content || '') + '</textarea></div>';
|
|
1517
|
+
html += '<div class="panel-field"><label>Description</label><textarea id="p-content"' + (isReadonly ? ' disabled' : '') + '>' + escHtml(item.content || '') + '</textarea></div>';
|
|
1070
1518
|
|
|
1071
1519
|
html += '</div>'; // close panel-body
|
|
1072
1520
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
'</
|
|
1521
|
+
// Footer with buttons
|
|
1522
|
+
html += '<div class="panel-footer">';
|
|
1523
|
+
if (!isCreate && !isReadonly && isSourceWritable()) {
|
|
1524
|
+
html += '<button class="btn btn-danger" id="panel-archive-btn">Archive</button>';
|
|
1525
|
+
}
|
|
1526
|
+
html += '<span style="flex:1"></span>';
|
|
1527
|
+
html += '<button class="btn" id="panel-cancel-btn">Cancel</button>';
|
|
1528
|
+
if (!isReadonly) {
|
|
1529
|
+
html += '<button class="btn btn-primary" id="panel-save-btn">' + (isCreate ? 'Create' : 'Save Changes') + '</button>';
|
|
1530
|
+
}
|
|
1531
|
+
html += '</div>';
|
|
1077
1532
|
|
|
1078
1533
|
panel.innerHTML = html;
|
|
1079
1534
|
|
|
1080
1535
|
// Event listeners
|
|
1081
1536
|
document.getElementById('panel-close-btn').addEventListener('click', closePanel);
|
|
1082
1537
|
document.getElementById('panel-cancel-btn').addEventListener('click', closePanel);
|
|
1083
|
-
document.getElementById('panel-save-btn')
|
|
1538
|
+
var saveBtn = document.getElementById('panel-save-btn');
|
|
1539
|
+
if (saveBtn) saveBtn.addEventListener('click', isCreate ? saveCreatePanel : savePanel);
|
|
1540
|
+
|
|
1541
|
+
var archiveBtn = document.getElementById('panel-archive-btn');
|
|
1542
|
+
if (archiveBtn) {
|
|
1543
|
+
archiveBtn.addEventListener('click', function() {
|
|
1544
|
+
if (!confirm('Archive this ' + typeLabel.toLowerCase() + '? It will be moved to the archive directory.')) return;
|
|
1545
|
+
deleteItem(t, item.id).then(function(ok) {
|
|
1546
|
+
if (ok) { closePanel(); refreshData(); }
|
|
1547
|
+
});
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1084
1550
|
|
|
1085
1551
|
// Live icon updates
|
|
1086
1552
|
var statusSel = document.getElementById('p-status');
|
|
@@ -1097,6 +1563,22 @@ function renderPanel() {
|
|
|
1097
1563
|
if (iconEl) iconEl.innerHTML = priorityIcon(prioritySel.value);
|
|
1098
1564
|
});
|
|
1099
1565
|
}
|
|
1566
|
+
|
|
1567
|
+
// Link chip click handlers
|
|
1568
|
+
panel.querySelectorAll('.link-chip[data-link]').forEach(function(chip) {
|
|
1569
|
+
chip.addEventListener('click', function() {
|
|
1570
|
+
var ref = chip.dataset.link;
|
|
1571
|
+
var parts = ref.split(':');
|
|
1572
|
+
if (parts.length === 2 && hasWorkspace()) {
|
|
1573
|
+
closePanel();
|
|
1574
|
+
switchSource(parts[0]);
|
|
1575
|
+
setTimeout(function() {
|
|
1576
|
+
var task = D.tasks.find(function(t) { return t.id === ref || t.id === parts[1]; });
|
|
1577
|
+
if (task) openPanel('tasks', task);
|
|
1578
|
+
}, 500);
|
|
1579
|
+
}
|
|
1580
|
+
});
|
|
1581
|
+
});
|
|
1100
1582
|
}
|
|
1101
1583
|
|
|
1102
1584
|
async function savePanel() {
|
|
@@ -1156,6 +1638,60 @@ async function savePanel() {
|
|
|
1156
1638
|
}
|
|
1157
1639
|
}
|
|
1158
1640
|
|
|
1641
|
+
async function saveCreatePanel() {
|
|
1642
|
+
var t = panelState.type;
|
|
1643
|
+
if (!t) return;
|
|
1644
|
+
|
|
1645
|
+
var data = {};
|
|
1646
|
+
|
|
1647
|
+
var titleEl = document.getElementById('p-title');
|
|
1648
|
+
if (titleEl) data.title = titleEl.value || 'Untitled';
|
|
1649
|
+
|
|
1650
|
+
var statusEl = document.getElementById('p-status');
|
|
1651
|
+
if (statusEl) data.status = statusEl.value;
|
|
1652
|
+
|
|
1653
|
+
if (t === 'tasks' || t === 'epics') {
|
|
1654
|
+
var priorityEl = document.getElementById('p-priority');
|
|
1655
|
+
if (priorityEl && priorityEl.value) data.priority = priorityEl.value;
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
if (t === 'tasks') {
|
|
1659
|
+
var pointsEl = document.getElementById('p-points');
|
|
1660
|
+
if (pointsEl && pointsEl.value) data.points = Number(pointsEl.value);
|
|
1661
|
+
|
|
1662
|
+
var assignedEl = document.getElementById('p-assigned');
|
|
1663
|
+
if (assignedEl && assignedEl.value.trim()) data.assigned = assignedEl.value.trim();
|
|
1664
|
+
|
|
1665
|
+
var sprintEl = document.getElementById('p-sprint');
|
|
1666
|
+
if (sprintEl && sprintEl.value) data.sprint = sprintEl.value;
|
|
1667
|
+
|
|
1668
|
+
var milestoneEl = document.getElementById('p-milestone');
|
|
1669
|
+
if (milestoneEl) data.milestone = milestoneEl.value;
|
|
1670
|
+
|
|
1671
|
+
var epicEl = document.getElementById('p-epic-select');
|
|
1672
|
+
if (epicEl) data.epic = epicEl.value;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
if (t === 'epics' || t === 'milestones') {
|
|
1676
|
+
var milestoneEl = document.getElementById('p-milestone');
|
|
1677
|
+
if (milestoneEl) data.milestone = milestoneEl.value;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
if (t === 'milestones') {
|
|
1681
|
+
var deadlineEl = document.getElementById('p-deadline');
|
|
1682
|
+
if (deadlineEl && deadlineEl.value) data.deadline = deadlineEl.value;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
var contentEl = document.getElementById('p-content');
|
|
1686
|
+
if (contentEl && contentEl.value) data.content = contentEl.value;
|
|
1687
|
+
|
|
1688
|
+
var result = await createItem(t, data);
|
|
1689
|
+
if (result) {
|
|
1690
|
+
closePanel();
|
|
1691
|
+
refreshData();
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1159
1695
|
// Close panel on overlay click or Escape
|
|
1160
1696
|
document.getElementById('panel-overlay').addEventListener('click', closePanel);
|
|
1161
1697
|
document.addEventListener('keydown', function(e) {
|
|
@@ -1167,11 +1703,17 @@ document.addEventListener('keydown', function(e) {
|
|
|
1167
1703
|
══════════════════════════════════════════════════════════════ */
|
|
1168
1704
|
function switchView(name) {
|
|
1169
1705
|
document.querySelectorAll('.view').forEach(function(v) { v.classList.remove('active'); });
|
|
1170
|
-
document.querySelectorAll('#sidebar-nav a').forEach(function(a) { a.classList.remove('active'); });
|
|
1706
|
+
document.querySelectorAll('#sidebar-nav a[data-view]').forEach(function(a) { a.classList.remove('active'); });
|
|
1171
1707
|
var t = document.getElementById('view-' + name);
|
|
1172
1708
|
if (t) t.classList.add('active');
|
|
1173
1709
|
var l = document.querySelector('#sidebar-nav a[data-view="' + name + '"]');
|
|
1174
1710
|
if (l) l.classList.add('active');
|
|
1711
|
+
// Persist view selection
|
|
1712
|
+
try { localStorage.setItem('mdboard-view', name); } catch(e) {}
|
|
1713
|
+
// Trigger overview rendering when switching to it
|
|
1714
|
+
if (name === 'overview' && D.loaded) {
|
|
1715
|
+
renderOverview();
|
|
1716
|
+
}
|
|
1175
1717
|
}
|
|
1176
1718
|
|
|
1177
1719
|
document.getElementById('sidebar-nav').addEventListener('click', function(e) {
|
|
@@ -1183,8 +1725,12 @@ document.getElementById('sidebar-nav').addEventListener('click', function(e) {
|
|
|
1183
1725
|
});
|
|
1184
1726
|
|
|
1185
1727
|
function handleHash() {
|
|
1186
|
-
var hash = window.location.hash.replace('#', '')
|
|
1187
|
-
|
|
1728
|
+
var hash = window.location.hash.replace('#', '');
|
|
1729
|
+
if (!hash) {
|
|
1730
|
+
try { hash = localStorage.getItem('mdboard-view') || ''; } catch(e) {}
|
|
1731
|
+
}
|
|
1732
|
+
hash = hash || 'board';
|
|
1733
|
+
var valid = ['board','table','milestones','metrics','overview'];
|
|
1188
1734
|
switchView(valid.indexOf(hash) !== -1 ? hash : 'board');
|
|
1189
1735
|
}
|
|
1190
1736
|
window.addEventListener('hashchange', handleHash);
|
|
@@ -1196,8 +1742,53 @@ window.addEventListener('hashchange', handleHash);
|
|
|
1196
1742
|
handleHash();
|
|
1197
1743
|
await loadAll();
|
|
1198
1744
|
initFromConfig();
|
|
1745
|
+
|
|
1746
|
+
// Sidebar logo always shows project name
|
|
1747
|
+
renderSidebarLogo();
|
|
1748
|
+
|
|
1749
|
+
// Workspace detection: build source rail + restore last source
|
|
1750
|
+
if (hasWorkspace()) {
|
|
1751
|
+
// Create overview tab (hidden by default)
|
|
1752
|
+
var overviewTab = document.querySelector('#sidebar-nav a[data-view="overview"]');
|
|
1753
|
+
if (!overviewTab) {
|
|
1754
|
+
var nav = document.getElementById('sidebar-nav');
|
|
1755
|
+
var metricsLink = document.querySelector('#sidebar-nav a[data-view="metrics"]');
|
|
1756
|
+
var overviewLink = document.createElement('a');
|
|
1757
|
+
overviewLink.href = '#overview';
|
|
1758
|
+
overviewLink.dataset.view = 'overview';
|
|
1759
|
+
overviewLink.style.display = 'none';
|
|
1760
|
+
overviewLink.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="9"/><path d="M12 3v18M3 12h18"/></svg><span>Overview</span>';
|
|
1761
|
+
if (metricsLink && metricsLink.nextSibling) {
|
|
1762
|
+
nav.insertBefore(overviewLink, metricsLink.nextSibling);
|
|
1763
|
+
} else {
|
|
1764
|
+
nav.appendChild(overviewLink);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
// Restore last source or default to overview
|
|
1769
|
+
var savedSource = null;
|
|
1770
|
+
try { savedSource = localStorage.getItem('mdboard-source'); } catch(e) {}
|
|
1771
|
+
var validSource = savedSource && (savedSource === 'overview' || D.config.workspace.sources.some(function(s) { return s.name === savedSource; }));
|
|
1772
|
+
var initialSource = validSource ? savedSource : 'overview';
|
|
1773
|
+
|
|
1774
|
+
buildSourceRail();
|
|
1775
|
+
// switchSource skips if same as current, so set activeSource first then trigger
|
|
1776
|
+
D.activeSource = null;
|
|
1777
|
+
switchSource(initialSource);
|
|
1778
|
+
// Wait for switchSource data reload before continuing
|
|
1779
|
+
await loadAll();
|
|
1780
|
+
initFromConfig();
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1199
1783
|
renderAll();
|
|
1200
1784
|
connectSSE();
|
|
1785
|
+
|
|
1786
|
+
// Load overview links in background for reverse link display
|
|
1787
|
+
if (hasWorkspace()) {
|
|
1788
|
+
fetchJson('/api/overview/links').then(function(data) {
|
|
1789
|
+
D.overviewLinks = data;
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1201
1792
|
})();
|
|
1202
1793
|
</script>
|
|
1203
1794
|
</body>
|