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/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-family:var(--mono);font-size:15px;font-weight:700;color:var(--text);letter-spacing:-.02em}
30
- .sidebar-logo span{color:var(--accent)}
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 span,.sidebar-footer,#sidebar-nav a span{display:none}
214
- .sidebar-logo{padding:16px 12px;text-align:center}
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"><span>&#9632;</span> mdboard</div>
276
+ <div class="sidebar-logo" id="sidebar-logo">
277
+ <span class="sidebar-logo-icon" id="sidebar-logo-icon">&#9632;</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 r = await fetch('/api/' + type + '/' + encodeURIComponent(id), {
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('/api/project'), fetchJson('/api/milestones'), fetchJson('/api/epics'),
446
- fetchJson('/api/tasks'), fetchJson('/api/sprint'), fetchJson('/api/metrics'),
447
- fetchJson('/api/health'), fetchJson('/api/sprints'), fetchJson('/api/config')
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() { loadAll().then(function() { initFromConfig(); renderAll(); }); }
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() { renderHeader(); renderBoardFilters(); renderBoard(); renderTableControls(); renderTableBody(); renderMsFilters(); renderMilestones(); renderMetrics(); }
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
- var p = D.project || {};
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
- c.innerHTML = '<input type="text" data-filter="search" placeholder="Search cards..." value="' + escHtml(boardFilters.search) + '">' +
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
- c.innerHTML = '<input type="text" data-tf="search" placeholder="Search ID or title..." value="' + escHtml(tableFilters.search) + '">' +
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
- DETAIL PANEL — Edit tasks, epics, milestones
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">&rarr;</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
- '<button class="panel-close" id="panel-close-btn">&times;</button>' +
997
- '</div>';
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">&times;</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
- // Epic (read-only info)
1060
- 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>';
1061
- html += '</div>';
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
- html += '<div class="panel-footer">' +
1074
- '<button class="btn" id="panel-cancel-btn">Cancel</button>' +
1075
- '<button class="btn btn-primary" id="panel-save-btn">Save Changes</button>' +
1076
- '</div>';
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').addEventListener('click', savePanel);
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('#', '') || 'board';
1187
- var valid = ['board','table','milestones','metrics'];
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>