mdboard 1.0.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}
@@ -89,12 +100,6 @@ a{color:var(--accent);text-decoration:none}
89
100
  .card:hover{border-color:var(--border2);background:var(--surface2)}
90
101
  .card:active{cursor:grabbing}
91
102
  .card.dragging{opacity:.4;transform:scale(.97)}
92
- .card[data-status="backlog"]{border-left-color:var(--text3)}
93
- .card[data-status="todo"]{border-left-color:var(--accent)}
94
- .card[data-status="in-progress"]{border-left-color:var(--warning)}
95
- .card[data-status="in-review"]{border-left-color:var(--purple)}
96
- .card[data-status="done"]{border-left-color:var(--success)}
97
- .card[data-status="blocked"]{border-left-color:var(--danger)}
98
103
  .card-header{display:flex;align-items:center;gap:6px;margin-bottom:4px}
99
104
  .card-id{font-family:var(--mono);font-size:11px;color:var(--text3)}
100
105
  .card-priority{display:flex;align-items:center}
@@ -107,6 +112,7 @@ a{color:var(--accent);text-decoration:none}
107
112
  .pill-points{background:var(--surface2);color:var(--text2);font-family:var(--mono)}
108
113
  .pill-epic{color:#fff;font-size:10px;font-weight:600}
109
114
  .badge{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:var(--radius-sm);font-size:11px;font-weight:600;text-transform:capitalize}
115
+ /* Fallback badge styles — overridden by dynamic styles once config loads */
110
116
  .badge-backlog{background:var(--surface2);color:var(--text2)}
111
117
  .badge-todo{background:var(--accent-dim);color:var(--accent)}
112
118
  .badge-in-progress{background:var(--warning-dim);color:var(--warning)}
@@ -212,11 +218,39 @@ th.sorted .sort-arrow{opacity:1;color:var(--accent)}
212
218
  .loading-container{padding:20px}
213
219
  @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
214
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
+
215
246
  /* ── Responsive ──────────────────────────────────────────── */
216
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}
217
251
  .sidebar{width:56px}
218
- .sidebar-logo span,.sidebar-footer,#sidebar-nav a span{display:none}
219
- .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}
220
254
  #sidebar-nav a{justify-content:center;padding:10px}
221
255
  .header{padding:12px 16px}
222
256
  .content{padding:16px}
@@ -229,12 +263,20 @@ th.sorted .sort-arrow{opacity:1;color:var(--accent)}
229
263
  .panel-props{grid-template-columns:1fr}
230
264
  }
231
265
  </style>
266
+ <style id="dynamic-styles"></style>
267
+ <link id="custom-theme" rel="stylesheet" href="/mdboard.css">
232
268
  </head>
233
269
  <body>
234
270
  <div class="app">
271
+ <!-- Source Rail (Discord/Slack style) -->
272
+ <nav class="source-rail" id="source-rail"></nav>
273
+
235
274
  <!-- Sidebar -->
236
275
  <aside class="sidebar">
237
- <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>
238
280
  <nav id="sidebar-nav">
239
281
  <a href="#board" data-view="board" class="active">
240
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>
@@ -259,10 +301,6 @@ th.sorted .sort-arrow{opacity:1;color:var(--accent)}
259
301
  <div class="main">
260
302
  <!-- Header -->
261
303
  <header class="header">
262
- <div class="header-title">
263
- <h1 id="h-project-name">Loading...</h1>
264
- <p id="h-project-desc"></p>
265
- </div>
266
304
  <div class="header-section" id="h-milestone-wrap" style="display:none">
267
305
  <span class="header-section-label">Milestone</span>
268
306
  <span class="header-section-value" id="h-milestone-name"></span>
@@ -294,6 +332,9 @@ th.sorted .sort-arrow{opacity:1;color:var(--accent)}
294
332
  <div id="view-metrics" class="view">
295
333
  <div id="metrics-container"></div>
296
334
  </div>
335
+ <div id="view-overview" class="view">
336
+ <div id="overview-container"></div>
337
+ </div>
297
338
  </div>
298
339
  </div>
299
340
  </div>
@@ -304,37 +345,45 @@ th.sorted .sort-arrow{opacity:1;color:var(--accent)}
304
345
 
305
346
  <script>
306
347
  /* ══════════════════════════════════════════════════════════════
307
- ICONSLinear-style status, priority, and milestone icons
348
+ ICON SHAPES SVG generators by icon name
308
349
  ══════════════════════════════════════════════════════════════ */
350
+ var ICON_SHAPES = {
351
+ 'dashed-circle': function(c) { return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="' + c + '" stroke-width="1.5" stroke-dasharray="3 2"/></svg>'; },
352
+ 'circle': function(c) { return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="' + c + '" stroke-width="1.5"/></svg>'; },
353
+ 'half-circle': function(c) { return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="' + c + '" stroke-width="1.5"/><path d="M8 2A6 6 0 0 1 8 14Z" fill="' + c + '"/></svg>'; },
354
+ 'three-quarter-circle': function(c) { return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="' + c + '" stroke-width="1.5"/><path d="M8 2A6 6 0 1 1 2 8L8 8Z" fill="' + c + '"/></svg>'; },
355
+ 'check-circle': function(c) { return '<svg class="icon" viewBox="0 0 16 16"><circle cx="8" cy="8" r="7" fill="' + c + '"/><path d="M5 8.5L7 10.5L11 5.5" stroke="#fff" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>'; },
356
+ 'x-circle': function(c) { return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="' + c + '" stroke-width="1.5"/><path d="M5.5 5.5L10.5 10.5M10.5 5.5L5.5 10.5" stroke="' + c + '" stroke-width="1.5" stroke-linecap="round"/></svg>'; },
357
+ 'slash-circle': function(c) { return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="' + c + '" stroke-width="1.5"/><path d="M4.5 11.5L11.5 4.5" stroke="' + c + '" stroke-width="1.5" stroke-linecap="round"/></svg>'; }
358
+ };
359
+
360
+ /* Config-driven maps — populated by initFromConfig() */
361
+ var STATUS_ICON_MAP = {};
362
+ var STATUS_COLOR_MAP = {};
363
+ var ENTITY_NAMES = {};
364
+
309
365
  function statusIcon(status) {
310
- switch (status) {
311
- case 'backlog':
312
- return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="#5A5A63" stroke-width="1.5" stroke-dasharray="3 2"/></svg>';
313
- case 'todo':
314
- return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="#8B8B93" stroke-width="1.5"/></svg>';
315
- case 'in-progress':
316
- return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="#D4A72C" stroke-width="1.5"/><path d="M8 2A6 6 0 0 1 8 14Z" fill="#D4A72C"/></svg>';
317
- case 'in-review':
318
- return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="#8B5CF6" stroke-width="1.5"/><path d="M8 2A6 6 0 1 1 2 8L8 8Z" fill="#8B5CF6"/></svg>';
319
- case 'done':
320
- return '<svg class="icon" viewBox="0 0 16 16"><circle cx="8" cy="8" r="7" fill="#2EA043"/><path d="M5 8.5L7 10.5L11 5.5" stroke="#fff" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>';
321
- case 'blocked':
322
- return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="#DA3633" stroke-width="1.5"/><path d="M5.5 5.5L10.5 10.5M10.5 5.5L5.5 10.5" stroke="#DA3633" stroke-width="1.5" stroke-linecap="round"/></svg>';
323
- case 'cancelled':
324
- return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="#5A5A63" stroke-width="1.5"/><path d="M4.5 11.5L11.5 4.5" stroke="#5A5A63" stroke-width="1.5" stroke-linecap="round"/></svg>';
325
- default:
326
- return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="#5A5A63" stroke-width="1.5" stroke-dasharray="3 2"/></svg>';
366
+ var entry = STATUS_ICON_MAP[status];
367
+ if (entry) {
368
+ var fn = ICON_SHAPES[entry.icon];
369
+ if (fn) return fn(entry.color);
327
370
  }
371
+ // Fallback: dashed circle
372
+ return ICON_SHAPES['dashed-circle']('#5A5A63');
328
373
  }
329
374
 
330
375
  function priorityIcon(priority) {
331
- var c, n;
332
- switch (priority) {
333
- case 'urgent': c = '#F97316'; n = 4; break;
334
- case 'high': c = '#F97316'; n = 3; break;
335
- case 'medium': c = '#D4A72C'; n = 2; break;
336
- case 'low': c = '#5B6EF5'; n = 1; break;
337
- default: c = '#3A3A40'; n = 0; break;
376
+ var c = '#3A3A40', n = 0;
377
+ if (D.config && D.config.priorities) {
378
+ var entry = D.config.priorities.find(function(p) { return p.key === priority; });
379
+ if (entry) { c = entry.color; n = entry.bars; }
380
+ } else {
381
+ switch (priority) {
382
+ case 'urgent': c = '#F97316'; n = 4; break;
383
+ case 'high': c = '#F97316'; n = 3; break;
384
+ case 'medium': c = '#D4A72C'; n = 2; break;
385
+ case 'low': c = '#5B6EF5'; n = 1; break;
386
+ }
338
387
  }
339
388
  var bars = '';
340
389
  for (var i = 0; i < 4; i++) {
@@ -347,13 +396,17 @@ function priorityIcon(priority) {
347
396
  }
348
397
 
349
398
  function milestoneIcon(status) {
399
+ var msStatuses = D.config && D.config.statuses ? D.config.statuses.milestone : null;
400
+ var entry = msStatuses ? msStatuses.find(function(s) { return s.key === status; }) : null;
401
+ var c = entry ? entry.color : '#8B8B93';
402
+
350
403
  switch (status) {
351
404
  case 'active':
352
- return '<svg class="icon" viewBox="0 0 16 16"><path d="M8 1L15 8L8 15L1 8Z" fill="none" stroke="#5B6EF5" stroke-width="1.5"/><path d="M8 5L11 8L8 11L5 8Z" fill="#5B6EF5"/></svg>';
405
+ return '<svg class="icon" viewBox="0 0 16 16"><path d="M8 1L15 8L8 15L1 8Z" fill="none" stroke="' + c + '" stroke-width="1.5"/><path d="M8 5L11 8L8 11L5 8Z" fill="' + c + '"/></svg>';
353
406
  case 'completed':
354
- return '<svg class="icon" viewBox="0 0 16 16"><path d="M8 1L15 8L8 15L1 8Z" fill="#2EA043"/><path d="M5.5 8L7 9.5L10.5 6" stroke="#fff" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>';
407
+ return '<svg class="icon" viewBox="0 0 16 16"><path d="M8 1L15 8L8 15L1 8Z" fill="' + c + '"/><path d="M5.5 8L7 9.5L10.5 6" stroke="#fff" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>';
355
408
  default:
356
- return '<svg class="icon" viewBox="0 0 16 16"><path d="M8 1L15 8L8 15L1 8Z" fill="none" stroke="#8B8B93" stroke-width="1.5"/></svg>';
409
+ return '<svg class="icon" viewBox="0 0 16 16"><path d="M8 1L15 8L8 15L1 8Z" fill="none" stroke="' + c + '" stroke-width="1.5"/></svg>';
357
410
  }
358
411
  }
359
412
 
@@ -361,8 +414,9 @@ function milestoneIcon(status) {
361
414
  DATA STORE
362
415
  ══════════════════════════════════════════════════════════════ */
363
416
  var D = {
364
- project: null, milestones: [], epics: [], features: [],
365
- sprints: [], allSprints: [], metrics: null, health: null, loaded: false
417
+ config: null, project: null, milestones: [], epics: [], tasks: [],
418
+ sprints: [], allSprints: [], metrics: null, health: null, loaded: false,
419
+ sources: [], activeSource: null, overviewLinks: null
366
420
  };
367
421
 
368
422
  /* ── Helpers ─────────────────────────────────────────────── */
@@ -392,6 +446,8 @@ var STATUSES = ['backlog','todo','in-progress','in-review','done','blocked','can
392
446
  var PRIORITIES = ['urgent','high','medium','low'];
393
447
  var STATUS_LABELS = {backlog:'Backlog',todo:'Todo','in-progress':'In Progress','in-review':'In Review',done:'Done',blocked:'Blocked',cancelled:'Cancelled'};
394
448
  var PRIORITY_LABELS = {urgent:'Urgent',high:'High',medium:'Medium',low:'Low'};
449
+ var BOARD_COLS = ['backlog','todo','in-progress','in-review','done'];
450
+ var COMPLETED_STATUS = 'done';
395
451
 
396
452
  /* ── Toast ───────────────────────────────────────────────── */
397
453
  function showToast(msg, type) {
@@ -410,7 +466,8 @@ async function fetchJson(url) {
410
466
 
411
467
  async function patchItem(type, id, updates) {
412
468
  try {
413
- var r = await fetch('/api/' + type + '/' + encodeURIComponent(id), {
469
+ var base = apiBase();
470
+ var r = await fetch(base + '/' + type + '/' + encodeURIComponent(id), {
414
471
  method: 'PATCH',
415
472
  headers: { 'Content-Type': 'application/json' },
416
473
  body: JSON.stringify(updates),
@@ -429,27 +486,283 @@ async function patchItem(type, id, updates) {
429
486
  }
430
487
  }
431
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 ────────────────────────────────────────── */
432
536
  async function loadAll() {
537
+ var base = apiBase();
433
538
  var results = await Promise.all([
434
- fetchJson('/api/project'), fetchJson('/api/milestones'), fetchJson('/api/epics'),
435
- fetchJson('/api/features'), fetchJson('/api/sprint'), fetchJson('/api/metrics'),
436
- fetchJson('/api/health'), fetchJson('/api/sprints')
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')
437
543
  ]);
438
544
  D.project = results[0]; D.milestones = results[1] || []; D.epics = results[2] || [];
439
- D.features = results[3] || [];
440
- if (results[4]) D.sprints = [results[4]];
545
+ D.tasks = results[3] || [];
546
+ if (results[4]) D.sprints = [results[4]]; else D.sprints = [];
441
547
  D.metrics = results[5]; D.health = results[6];
442
548
  D.allSprints = results[7] || [];
549
+ D.config = results[8];
550
+ D.sources = results[9] || [];
443
551
  D.loaded = true;
444
552
  }
445
553
 
446
- function refreshData() { loadAll().then(function() { 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
+ }
565
+
566
+ /* ── Config-driven initialization ────────────────────────── */
567
+ function initFromConfig() {
568
+ if (!D.config) return;
569
+
570
+ var cfg = D.config;
571
+
572
+ // Derive entity names
573
+ ENTITY_NAMES = cfg.entities || {};
574
+
575
+ // Derive statuses, labels, board columns
576
+ if (cfg.statuses && cfg.statuses.task) {
577
+ STATUSES = cfg.statuses.task.map(function(s) { return s.key; });
578
+ STATUS_LABELS = {};
579
+ cfg.statuses.task.forEach(function(s) { STATUS_LABELS[s.key] = s.label; });
580
+ }
581
+
582
+ // Build icon/color maps for all status types
583
+ STATUS_ICON_MAP = {};
584
+ STATUS_COLOR_MAP = {};
585
+ if (cfg.statuses) {
586
+ Object.keys(cfg.statuses).forEach(function(entityType) {
587
+ cfg.statuses[entityType].forEach(function(s) {
588
+ if (!STATUS_ICON_MAP[s.key]) {
589
+ STATUS_ICON_MAP[s.key] = { icon: s.icon || 'dashed-circle', color: s.color || '#5A5A63' };
590
+ STATUS_COLOR_MAP[s.key] = s.color || '#5A5A63';
591
+ }
592
+ });
593
+ });
594
+ }
595
+
596
+ // Derive priorities
597
+ if (cfg.priorities) {
598
+ PRIORITIES = cfg.priorities.map(function(p) { return p.key; });
599
+ PRIORITY_LABELS = {};
600
+ cfg.priorities.forEach(function(p) { PRIORITY_LABELS[p.key] = p.label; });
601
+ }
602
+
603
+ // Board columns
604
+ if (cfg.boardColumns) BOARD_COLS = cfg.boardColumns;
605
+
606
+ // Completed status
607
+ if (cfg.completedStatus) COMPLETED_STATUS = cfg.completedStatus;
608
+
609
+ generateDynamicStyles();
610
+ }
611
+
612
+ function generateDynamicStyles() {
613
+ var css = '';
614
+ if (D.config && D.config.statuses && D.config.statuses.task) {
615
+ D.config.statuses.task.forEach(function(s) {
616
+ var key = s.key;
617
+ var color = s.color;
618
+ css += '.badge-' + key + '{background:' + color + '20;color:' + color + '}\n';
619
+ css += '.card[data-status="' + key + '"]{border-left-color:' + color + '}\n';
620
+ });
621
+ }
622
+ // Milestone/epic/sprint statuses
623
+ ['milestone', 'epic', 'sprint'].forEach(function(entityType) {
624
+ if (D.config && D.config.statuses && D.config.statuses[entityType]) {
625
+ D.config.statuses[entityType].forEach(function(s) {
626
+ css += '.badge-' + s.key + '{background:' + s.color + '20;color:' + s.color + '}\n';
627
+ });
628
+ }
629
+ });
630
+ // Priority badges
631
+ if (D.config && D.config.priorities) {
632
+ D.config.priorities.forEach(function(p) {
633
+ css += '.badge-' + p.key + '{background:' + p.color + '20;color:' + p.color + '}\n';
634
+ });
635
+ }
636
+ var styleEl = document.getElementById('dynamic-styles');
637
+ if (styleEl) styleEl.textContent = css;
638
+ }
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
+ }
447
752
 
448
753
  /* ── SSE — Hot Reload ────────────────────────────────────── */
449
754
  function connectSSE() {
450
755
  try {
451
756
  var src = new EventSource('/api/events');
452
- src.onmessage = function() { refreshData(); };
757
+ src.onmessage = function(e) {
758
+ var data;
759
+ try { data = JSON.parse(e.data); } catch { data = {}; }
760
+ if (data.cssReload) {
761
+ var link = document.getElementById('custom-theme');
762
+ if (link) link.href = '/mdboard.css?' + Date.now();
763
+ }
764
+ refreshData();
765
+ };
453
766
  src.onerror = function() { setTimeout(connectSSE, 5000); src.close(); };
454
767
  } catch { /* no SSE support */ }
455
768
  }
@@ -457,12 +770,17 @@ function connectSSE() {
457
770
  /* ══════════════════════════════════════════════════════════════
458
771
  RENDER
459
772
  ══════════════════════════════════════════════════════════════ */
460
- 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
+ }
461
781
 
462
782
  function renderHeader() {
463
- var p = D.project || {};
464
- document.getElementById('h-project-name').textContent = p.name || 'Project';
465
- document.getElementById('h-project-desc').textContent = p.description || '';
783
+ renderSidebarLogo();
466
784
 
467
785
  var mw = document.getElementById('h-milestone-wrap');
468
786
  var am = D.milestones.find(function(m) { return m.status === 'active'; });
@@ -481,12 +799,15 @@ function renderHeader() {
481
799
  document.getElementById('h-sprint-days').textContent = dr !== null ? (dr >= 0 ? dr + ' days left' : Math.abs(dr) + ' days over') : '';
482
800
  } else { sw.style.display = 'none'; }
483
801
 
484
- var total = D.features.length;
485
- var done = D.features.filter(function(f) { return f.status === 'done'; }).length;
486
- var inProg = D.features.filter(function(f) { return f.status === 'in-progress'; }).length;
802
+ var taskPlural = ENTITY_NAMES.task ? ENTITY_NAMES.task.plural : 'Tasks';
803
+ var total = D.tasks.length;
804
+ var done = D.tasks.filter(function(f) { return f.status === COMPLETED_STATUS; }).length;
805
+ var inProgStatus = (D.config && D.config.statuses && D.config.statuses.task) ?
806
+ ((D.config.statuses.task.find(function(s) { return s.icon === 'half-circle'; }) || {}).key || 'in-progress') : 'in-progress';
807
+ var inProg = D.tasks.filter(function(f) { return f.status === inProgStatus; }).length;
487
808
  var vel = D.health && D.health.velocity != null ? D.health.velocity : (total ? Math.round(done / total * 100) : 0);
488
809
  document.getElementById('h-stats').innerHTML =
489
- '<div class="stat"><span class="stat-val">' + total + '</span><span class="stat-label">Features</span></div>' +
810
+ '<div class="stat"><span class="stat-val">' + total + '</span><span class="stat-label">' + escHtml(taskPlural) + '</span></div>' +
490
811
  '<div class="stat"><span class="stat-val" style="color:var(--success)">' + done + '</span><span class="stat-label">Done</span></div>' +
491
812
  '<div class="stat"><span class="stat-val" style="color:var(--warning)">' + inProg + '</span><span class="stat-label">In Progress</span></div>' +
492
813
  '<div class="stat"><span class="stat-val" style="color:var(--accent)">' + vel + '%</span><span class="stat-label">Velocity</span></div>';
@@ -495,26 +816,32 @@ function renderHeader() {
495
816
  /* ══════════════════════════════════════════════════════════════
496
817
  BOARD VIEW
497
818
  ══════════════════════════════════════════════════════════════ */
498
- var BOARD_COLS = ['backlog','todo','in-progress','in-review','done'];
499
819
  var boardFilters = { search: '', priority: '', epic: '', milestone: '' };
500
820
 
501
821
  function renderBoardFilters() {
502
822
  var c = document.getElementById('board-filters');
503
823
  var ep = [], ms = [];
504
- D.features.forEach(function(f) {
824
+ D.tasks.forEach(function(f) {
505
825
  if (f.epic && ep.indexOf(f.epic) === -1) ep.push(f.epic);
506
826
  if (f.milestone && ms.indexOf(f.milestone) === -1) ms.push(f.milestone);
507
827
  });
508
828
 
829
+ var epicPlural = ENTITY_NAMES.epic ? ENTITY_NAMES.epic.plural : 'Epics';
830
+ var msPlural = ENTITY_NAMES.milestone ? ENTITY_NAMES.milestone.plural : 'Milestones';
831
+
509
832
  function opts(arr, key, label) {
510
833
  return '<select data-filter="' + key + '"><option value="">All ' + label + '</option>' +
511
834
  arr.map(function(v) { return '<option value="' + escHtml(v) + '"' + (boardFilters[key] === v ? ' selected' : '') + '>' + escHtml(v) + '</option>'; }).join('') + '</select>';
512
835
  }
513
836
 
514
- 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) + '">' +
515
842
  '<select data-filter="priority"><option value="">All Priorities</option>' +
516
843
  PRIORITIES.map(function(p) { return '<option value="' + p + '"' + (boardFilters.priority === p ? ' selected' : '') + '>' + (PRIORITY_LABELS[p] || p) + '</option>'; }).join('') + '</select>' +
517
- opts(ep, 'epic', 'Epics') + opts(ms, 'milestone', 'Milestones');
844
+ opts(ep, 'epic', epicPlural) + opts(ms, 'milestone', msPlural);
518
845
 
519
846
  c.querySelectorAll('input[data-filter]').forEach(function(el) {
520
847
  el.addEventListener('input', function() { boardFilters[el.dataset.filter] = el.value; renderBoard(); });
@@ -522,10 +849,13 @@ function renderBoardFilters() {
522
849
  c.querySelectorAll('select[data-filter]').forEach(function(el) {
523
850
  el.addEventListener('change', function() { boardFilters[el.dataset.filter] = el.value; renderBoard(); });
524
851
  });
852
+
853
+ var btn = document.getElementById('board-create-btn');
854
+ if (btn) btn.addEventListener('click', function() { openCreateDialog('tasks'); });
525
855
  }
526
856
 
527
- function getFilteredBoardFeatures() {
528
- var list = D.features.slice();
857
+ function getFilteredBoardTasks() {
858
+ var list = D.tasks.slice();
529
859
  var f = boardFilters;
530
860
  if (f.search) { var q = f.search.toLowerCase(); list = list.filter(function(x) { return (x.id || '').toLowerCase().indexOf(q) !== -1 || (x.title || '').toLowerCase().indexOf(q) !== -1; }); }
531
861
  if (f.priority) list = list.filter(function(x) { return x.priority === f.priority; });
@@ -558,15 +888,16 @@ function renderBoard() {
558
888
  return;
559
889
  }
560
890
 
561
- var filteredFeatures = getFilteredBoardFeatures();
891
+ var filteredTasks = getFilteredBoardTasks();
892
+ var taskSingular = ENTITY_NAMES.task ? ENTITY_NAMES.task.singular.toLowerCase() : 'task';
562
893
 
563
- if (!D.features.length) {
564
- board.innerHTML = '<div class="empty"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="3"/><path d="M9 12h6M12 9v6"/></svg><p>No features yet. Create feature files in your project/ directory to get started.</p></div>';
894
+ if (!D.tasks.length) {
895
+ board.innerHTML = '<div class="empty"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="3"/><path d="M9 12h6M12 9v6"/></svg><p>No ' + escHtml(taskSingular) + 's yet. Create ' + escHtml(taskSingular) + ' files in your project/ directory to get started.</p></div>';
565
896
  return;
566
897
  }
567
898
 
568
899
  board.innerHTML = BOARD_COLS.map(function(status) {
569
- var cards = filteredFeatures.filter(function(f) { return f.status === status; });
900
+ var cards = filteredTasks.filter(function(f) { return f.status === status; });
570
901
  return '<div class="column" data-status="' + status + '">' +
571
902
  '<div class="column-header"><span class="col-title">' + statusIcon(status) + ' ' + (STATUS_LABELS[status] || status) + '</span><span class="col-count">' + cards.length + '</span></div>' +
572
903
  '<div class="column-body">' + cards.map(renderCard).join('') + '</div></div>';
@@ -605,8 +936,8 @@ function setupDragDrop() {
605
936
  });
606
937
  card.addEventListener('click', function(e) {
607
938
  if (e.defaultPrevented) return;
608
- var feat = D.features.find(function(f) { return f.id === card.dataset.id; });
609
- if (feat) openPanel('features', feat);
939
+ var task = D.tasks.find(function(f) { return f.id === card.dataset.id; });
940
+ if (task) openPanel('tasks', task);
610
941
  });
611
942
  });
612
943
 
@@ -624,16 +955,16 @@ function setupDragDrop() {
624
955
  col.addEventListener('drop', function(e) {
625
956
  e.preventDefault();
626
957
  col.classList.remove('drag-over');
627
- var featureId = e.dataTransfer.getData('text/plain');
958
+ var taskId = e.dataTransfer.getData('text/plain');
628
959
  var newStatus = col.dataset.status;
629
- if (!featureId || !newStatus) return;
960
+ if (!taskId || !newStatus) return;
630
961
 
631
962
  // Optimistic update
632
- var feat = D.features.find(function(f) { return f.id === featureId; });
633
- if (feat && feat.status !== newStatus) {
634
- feat.status = newStatus;
963
+ var task = D.tasks.find(function(f) { return f.id === taskId; });
964
+ if (task && task.status !== newStatus) {
965
+ task.status = newStatus;
635
966
  renderBoard();
636
- patchItem('features', featureId, { status: newStatus });
967
+ patchItem('tasks', taskId, { status: newStatus });
637
968
  }
638
969
  });
639
970
  });
@@ -654,18 +985,27 @@ var TABLE_COLS = [
654
985
  function renderTableControls() {
655
986
  var c = document.getElementById('table-controls');
656
987
  var ms = [], ep = [], st = [], sp = [];
657
- D.features.forEach(function(f) {
988
+ D.tasks.forEach(function(f) {
658
989
  if (f.milestone && ms.indexOf(f.milestone) === -1) ms.push(f.milestone);
659
990
  if (f.epic && ep.indexOf(f.epic) === -1) ep.push(f.epic);
660
991
  if (f.status && st.indexOf(f.status) === -1) st.push(f.status);
661
992
  if (f.sprint && sp.indexOf(f.sprint) === -1) sp.push(f.sprint);
662
993
  });
994
+
995
+ var msPlural = ENTITY_NAMES.milestone ? ENTITY_NAMES.milestone.plural : 'Milestones';
996
+ var epicPlural = ENTITY_NAMES.epic ? ENTITY_NAMES.epic.plural : 'Epics';
997
+ var sprintPlural = ENTITY_NAMES.sprint ? ENTITY_NAMES.sprint.plural : 'Sprints';
998
+
663
999
  function opts(arr, filter, label) {
664
1000
  return '<select data-tf="' + filter + '"><option value="">All ' + label + '</option>' +
665
1001
  arr.map(function(v) { return '<option value="' + escHtml(v) + '"' + (tableFilters[filter] === v ? ' selected' : '') + '>' + escHtml(v) + '</option>'; }).join('') + '</select>';
666
1002
  }
667
- c.innerHTML = '<input type="text" data-tf="search" placeholder="Search ID or title..." value="' + escHtml(tableFilters.search) + '">' +
668
- opts(ms, 'milestone', 'Milestones') + opts(ep, 'epic', 'Epics') + opts(st, 'status', 'Statuses') + opts(sp, 'sprint', 'Sprints') +
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) + '">' +
1008
+ opts(ms, 'milestone', msPlural) + opts(ep, 'epic', epicPlural) + opts(st, 'status', 'Statuses') + opts(sp, 'sprint', sprintPlural) +
669
1009
  '<select data-tf="priority"><option value="">All Priorities</option>' +
670
1010
  PRIORITIES.map(function(p) { return '<option value="' + p + '"' + (tableFilters.priority === p ? ' selected' : '') + '>' + (PRIORITY_LABELS[p] || p) + '</option>'; }).join('') + '</select>';
671
1011
 
@@ -673,10 +1013,13 @@ function renderTableControls() {
673
1013
  c.querySelectorAll('select[data-tf]').forEach(function(sel) {
674
1014
  sel.addEventListener('change', function() { tableFilters[sel.dataset.tf] = sel.value; renderTableBody(); });
675
1015
  });
1016
+
1017
+ var btn = document.getElementById('table-create-btn');
1018
+ if (btn) btn.addEventListener('click', function() { openCreateDialog('tasks'); });
676
1019
  }
677
1020
 
678
- function getFilteredFeatures() {
679
- var list = D.features.slice();
1021
+ function getFilteredTasks() {
1022
+ var list = D.tasks.slice();
680
1023
  var f = tableFilters;
681
1024
  if (f.search) { var q = f.search.toLowerCase(); list = list.filter(function(x) { return (x.id || '').toLowerCase().indexOf(q) !== -1 || (x.title || '').toLowerCase().indexOf(q) !== -1; }); }
682
1025
  if (f.milestone) list = list.filter(function(x) { return x.milestone === f.milestone; });
@@ -695,10 +1038,11 @@ function getFilteredFeatures() {
695
1038
 
696
1039
  function renderTableBody() {
697
1040
  var w = document.getElementById('table-wrapper');
1041
+ var taskPlural = ENTITY_NAMES.task ? ENTITY_NAMES.task.plural.toLowerCase() : 'tasks';
698
1042
  if (!D.loaded) { w.innerHTML = '<div class="loading-container">' + '<div class="skeleton skeleton-line" style="width:100%;height:32px"></div>'.repeat(6) + '</div>'; return; }
699
- var features = getFilteredFeatures();
700
- if (!features.length) {
701
- w.innerHTML = '<div class="empty"><p>' + (D.features.length ? 'No features match filters.' : 'No features yet.') + '</p></div>';
1043
+ var tasks = getFilteredTasks();
1044
+ if (!tasks.length) {
1045
+ w.innerHTML = '<div class="empty"><p>' + (D.tasks.length ? 'No ' + escHtml(taskPlural) + ' match filters.' : 'No ' + escHtml(taskPlural) + ' yet.') + '</p></div>';
702
1046
  return;
703
1047
  }
704
1048
  var html = '<table class="ftable"><thead><tr>';
@@ -707,7 +1051,7 @@ function renderTableBody() {
707
1051
  html += '<th class="' + (sorted ? 'sorted' : '') + '" data-col="' + c.key + '">' + c.label + '<span class="sort-arrow">' + (sorted ? (tableSort.asc ? '\u25B2' : '\u25BC') : '') + '</span></th>';
708
1052
  });
709
1053
  html += '</tr></thead><tbody>';
710
- features.forEach(function(f) {
1054
+ tasks.forEach(function(f) {
711
1055
  var assigned = Array.isArray(f.assigned) ? f.assigned.join(', ') : (f.assigned || '');
712
1056
  html += '<tr class="clickable-row" data-fid="' + escHtml(f.id || '') + '">' +
713
1057
  '<td class="td-id">' + escHtml(f.id || '') + '</td>' +
@@ -732,8 +1076,8 @@ function renderTableBody() {
732
1076
  });
733
1077
  w.querySelectorAll('.clickable-row').forEach(function(tr) {
734
1078
  tr.addEventListener('click', function() {
735
- var feat = D.features.find(function(f) { return f.id === tr.dataset.fid; });
736
- if (feat) openPanel('features', feat);
1079
+ var task = D.tasks.find(function(f) { return f.id === tr.dataset.fid; });
1080
+ if (task) openPanel('tasks', task);
737
1081
  });
738
1082
  });
739
1083
  }
@@ -745,23 +1089,30 @@ var msFilter = { status: '' };
745
1089
 
746
1090
  function renderMsFilters() {
747
1091
  var c = document.getElementById('ms-filters');
1092
+ var msStatuses = (D.config && D.config.statuses && D.config.statuses.milestone) ?
1093
+ D.config.statuses.milestone.map(function(s) { return s; }) :
1094
+ [{key:'planned',label:'Planned'},{key:'active',label:'Active'},{key:'completed',label:'Completed'}];
1095
+
748
1096
  c.innerHTML = '<label>Filter:</label>' +
749
1097
  '<select data-msf="status"><option value="">All Statuses</option>' +
750
- ['planned','active','completed'].map(function(s) {
751
- return '<option value="' + s + '"' + (msFilter.status === s ? ' selected' : '') + '>' + s.charAt(0).toUpperCase() + s.slice(1) + '</option>';
1098
+ msStatuses.map(function(s) {
1099
+ return '<option value="' + s.key + '"' + (msFilter.status === s.key ? ' selected' : '') + '>' + escHtml(s.label) + '</option>';
752
1100
  }).join('') + '</select>';
753
1101
  c.querySelector('select[data-msf]').addEventListener('change', function(e) { msFilter.status = e.target.value; renderMilestones(); });
754
1102
  }
755
1103
 
756
1104
  function renderMilestones() {
757
1105
  var c = document.getElementById('milestones-container');
1106
+ var taskPlural = ENTITY_NAMES.task ? ENTITY_NAMES.task.plural.toLowerCase() : 'tasks';
1107
+ var msDir = ENTITY_NAMES.milestone ? ENTITY_NAMES.milestone.dir : 'milestones';
1108
+ var msPlural = ENTITY_NAMES.milestone ? ENTITY_NAMES.milestone.plural.toLowerCase() : 'milestones';
758
1109
  if (!D.loaded) { c.innerHTML = '<div class="loading-container"><div class="skeleton skeleton-card" style="height:200px"></div></div>'; return; }
759
1110
 
760
1111
  var milestones = D.milestones;
761
1112
  if (msFilter.status) milestones = milestones.filter(function(m) { return m.status === msFilter.status; });
762
1113
 
763
1114
  if (!milestones.length) {
764
- c.innerHTML = '<div class="empty"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" y1="22" x2="4" y2="15"/></svg><p>' + (D.milestones.length ? 'No milestones match filter.' : 'No milestones yet. Create milestone directories under project/milestones/ to get started.') + '</p></div>';
1115
+ c.innerHTML = '<div class="empty"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" y1="22" x2="4" y2="15"/></svg><p>' + (D.milestones.length ? 'No ' + escHtml(msPlural) + ' match filter.' : 'No ' + escHtml(msPlural) + ' yet. Create milestone directories under project/' + escHtml(msDir) + '/ to get started.') + '</p></div>';
765
1116
  return;
766
1117
  }
767
1118
 
@@ -773,7 +1124,7 @@ function renderMilestones() {
773
1124
  var ep = e.progress || 0;
774
1125
  var ec = epicColor(e.id || e.title || '');
775
1126
  return '<div class="ms-epic" data-epic-id="' + escHtml(e.id || '') + '"><div class="epic-name"><span class="dot" style="background:' + ec + '"></span>' + escHtml(e.title || e.id || '') + '</div>' +
776
- '<div class="epic-counts">' + (e.completedCount || 0) + ' / ' + (e.featureCount || 0) + ' features &middot; ' + (e.totalPoints || 0) + ' pts</div>' +
1127
+ '<div class="epic-counts">' + (e.completedCount || 0) + ' / ' + (e.featureCount || 0) + ' ' + escHtml(taskPlural) + ' &middot; ' + (e.totalPoints || 0) + ' pts</div>' +
777
1128
  '<div class="progress progress-accent"><div class="progress-fill" style="width:' + ep + '%;background:' + ec + '"></div></div>' +
778
1129
  '<div class="epic-meta">' +
779
1130
  (e.status ? '<span class="badge ' + badgeClass('badge', e.status) + '">' + statusIcon(e.status) + ' ' + escHtml(e.status) + '</span>' : '') +
@@ -785,7 +1136,7 @@ function renderMilestones() {
785
1136
  '<div class="ms-header"><h2>' + milestoneIcon(ms.status) + ' ' + escHtml(ms.title || ms.id || '') + '</h2>' +
786
1137
  '<span class="badge ' + badgeClass('badge', ms.status) + '">' + escHtml(ms.status || '') + '</span>' +
787
1138
  (ms.deadline ? '<span class="ms-deadline">' + fmtDate(ms.deadline) + '</span>' : '') + '</div>' +
788
- '<div class="ms-progress"><div class="progress-label"><span>' + cc + ' / ' + fc + ' features</span><span>' + pct + '%</span></div>' +
1139
+ '<div class="ms-progress"><div class="progress-label"><span>' + cc + ' / ' + fc + ' ' + escHtml(taskPlural) + '</span><span>' + pct + '%</span></div>' +
789
1140
  '<div class="progress progress-lg progress-success"><div class="progress-fill" style="width:' + pct + '%"></div></div></div>' +
790
1141
  '<div class="ms-epics">' + epicCards + '</div></div>';
791
1142
  }).join('');
@@ -814,6 +1165,7 @@ function renderMilestones() {
814
1165
  ══════════════════════════════════════════════════════════════ */
815
1166
  function renderMetrics() {
816
1167
  var c = document.getElementById('metrics-container');
1168
+ var taskPlural = ENTITY_NAMES.task ? ENTITY_NAMES.task.plural : 'Tasks';
817
1169
  if (!D.loaded) { c.innerHTML = '<div class="metrics-grid">' + '<div class="skeleton skeleton-card" style="height:200px"></div>'.repeat(4) + '</div>'; return; }
818
1170
 
819
1171
  var msProg = D.milestones.length ? D.milestones.map(function(ms) {
@@ -823,19 +1175,19 @@ function renderMetrics() {
823
1175
  }).join('') : '<div style="color:var(--text3);padding:8px 0">No milestones.</div>';
824
1176
 
825
1177
  var statuses = {};
826
- D.features.forEach(function(f) { var s = f.status || 'unknown'; statuses[s] = (statuses[s] || 0) + 1; });
1178
+ D.tasks.forEach(function(f) { var s = f.status || 'unknown'; statuses[s] = (statuses[s] || 0) + 1; });
827
1179
  var statusHtml = Object.keys(statuses).map(function(s) {
828
- return '<div class="health-row">' + statusIcon(s) + '<div class="health-label" style="text-transform:capitalize">' + escHtml(s) + '</div><div class="health-val">' + statuses[s] + '</div></div>';
1180
+ return '<div class="health-row">' + statusIcon(s) + '<div class="health-label" style="text-transform:capitalize">' + escHtml(STATUS_LABELS[s] || s) + '</div><div class="health-val">' + statuses[s] + '</div></div>';
829
1181
  }).join('') || '<div style="color:var(--text3);padding:8px 0">No data.</div>';
830
1182
 
831
1183
  var priorities = {};
832
- D.features.forEach(function(f) { var p = f.priority || 'none'; priorities[p] = (priorities[p] || 0) + 1; });
1184
+ D.tasks.forEach(function(f) { var p = f.priority || 'none'; priorities[p] = (priorities[p] || 0) + 1; });
833
1185
  var priorityHtml = Object.keys(priorities).map(function(p) {
834
- return '<div class="health-row">' + priorityIcon(p) + '<div class="health-label" style="text-transform:capitalize">' + escHtml(p) + '</div><div class="health-val">' + priorities[p] + '</div></div>';
1186
+ return '<div class="health-row">' + priorityIcon(p) + '<div class="health-label" style="text-transform:capitalize">' + escHtml(PRIORITY_LABELS[p] || p) + '</div><div class="health-val">' + priorities[p] + '</div></div>';
835
1187
  }).join('') || '<div style="color:var(--text3);padding:8px 0">No data.</div>';
836
1188
 
837
1189
  var h = D.health || {};
838
- var qualHtml = '<div class="health-row"><div class="health-dot" style="background:var(--text2)"></div><div class="health-label">Total Features</div><div class="health-val">' + (h.totalFeatures || D.features.length) + '</div></div>' +
1190
+ var qualHtml = '<div class="health-row"><div class="health-dot" style="background:var(--text2)"></div><div class="health-label">Total ' + escHtml(taskPlural) + '</div><div class="health-val">' + (h.totalFeatures || D.tasks.length) + '</div></div>' +
839
1191
  '<div class="health-row"><div class="health-dot" style="background:var(--success)"></div><div class="health-label">Completed</div><div class="health-val">' + (h.completedFeatures || 0) + '</div></div>' +
840
1192
  '<div class="health-row"><div class="health-dot" style="background:var(--warning)"></div><div class="health-label">In Progress</div><div class="health-val">' + (h.inProgressFeatures || 0) + '</div></div>' +
841
1193
  '<div class="health-row"><div class="health-dot" style="background:var(--accent)"></div><div class="health-label">Avg Velocity</div><div class="health-val">' + (h.velocity != null ? h.velocity + '%' : 'N/A') + '</div></div>';
@@ -848,12 +1200,162 @@ function renderMetrics() {
848
1200
  }
849
1201
 
850
1202
  /* ══════════════════════════════════════════════════════════════
851
- DETAIL PANEL — Edit features, epics, milestones
1203
+ CRUD HELPERS
852
1204
  ══════════════════════════════════════════════════════════════ */
853
- var panelState = { open: false, type: null, item: null };
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
1354
+ ══════════════════════════════════════════════════════════════ */
1355
+ var panelState = { open: false, type: null, item: null, isCreate: false };
854
1356
 
855
1357
  function openPanel(type, item) {
856
- 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 };
857
1359
  renderPanel();
858
1360
  document.getElementById('detail-panel').classList.add('open');
859
1361
  document.getElementById('panel-overlay').classList.add('open');
@@ -869,36 +1371,66 @@ function renderPanel() {
869
1371
  var panel = document.getElementById('detail-panel');
870
1372
  var t = panelState.type;
871
1373
  var item = panelState.item;
1374
+ var isCreate = panelState.isCreate;
872
1375
  if (!item) return;
873
1376
 
874
- var typeLabel = t === 'features' ? 'Feature' : t === 'epics' ? 'Epic' : 'Milestone';
1377
+ var isReadonly = !isCreate && item.readonly;
1378
+
1379
+ var typeLabel;
1380
+ if (t === 'tasks') typeLabel = ENTITY_NAMES.task ? ENTITY_NAMES.task.singular : 'Task';
1381
+ else if (t === 'epics') typeLabel = ENTITY_NAMES.epic ? ENTITY_NAMES.epic.singular : 'Epic';
1382
+ else typeLabel = ENTITY_NAMES.milestone ? ENTITY_NAMES.milestone.singular : 'Milestone';
875
1383
 
876
1384
  var html = '<div class="panel-header">' +
877
- '<span class="panel-type">' + typeLabel + '</span>' +
878
- '<span class="panel-item-id">' + escHtml(item.id || '') + '</span>' +
879
- '<button class="panel-close" id="panel-close-btn">&times;</button>' +
880
- '</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>';
881
1394
 
882
1395
  html += '<div class="panel-body">';
883
1396
 
884
1397
  // Title
885
- 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>';
886
1399
 
887
1400
  // Status + Priority row
888
1401
  html += '<div class="panel-props">';
889
1402
 
890
- // Status
891
- var statusOptions = t === 'features' ? STATUSES : ['planned', 'active', 'completed', 'cancelled'];
892
- var statusLabels = t === 'features' ? STATUS_LABELS : { planned: 'Planned', active: 'Active', completed: 'Completed', cancelled: 'Cancelled' };
1403
+ // Status — derived from config
1404
+ var statusOptions, statusLabels;
1405
+ if (t === 'tasks' && D.config && D.config.statuses && D.config.statuses.task) {
1406
+ statusOptions = D.config.statuses.task.map(function(s) { return s.key; });
1407
+ statusLabels = {};
1408
+ D.config.statuses.task.forEach(function(s) { statusLabels[s.key] = s.label; });
1409
+ } else if (t === 'epics' && D.config && D.config.statuses && D.config.statuses.epic) {
1410
+ statusOptions = D.config.statuses.epic.map(function(s) { return s.key; });
1411
+ statusLabels = {};
1412
+ D.config.statuses.epic.forEach(function(s) { statusLabels[s.key] = s.label; });
1413
+ } else if (t === 'milestones' && D.config && D.config.statuses && D.config.statuses.milestone) {
1414
+ statusOptions = D.config.statuses.milestone.map(function(s) { return s.key; });
1415
+ statusLabels = {};
1416
+ D.config.statuses.milestone.forEach(function(s) { statusLabels[s.key] = s.label; });
1417
+ } else if (t === 'tasks') {
1418
+ statusOptions = STATUSES;
1419
+ statusLabels = STATUS_LABELS;
1420
+ } else {
1421
+ statusOptions = ['planned', 'active', 'completed', 'cancelled'];
1422
+ statusLabels = { planned: 'Planned', active: 'Active', completed: 'Completed', cancelled: 'Cancelled' };
1423
+ }
1424
+
893
1425
  html += '<div class="panel-field"><label>Status</label><div class="field-with-icon"><span id="p-status-icon">' + statusIcon(item.status) + '</span>' +
894
- '<select id="p-status">' + statusOptions.map(function(s) {
1426
+ '<select id="p-status"' + (isReadonly ? ' disabled' : '') + '>' + statusOptions.map(function(s) {
895
1427
  return '<option value="' + s + '"' + (item.status === s ? ' selected' : '') + '>' + (statusLabels[s] || s) + '</option>';
896
1428
  }).join('') + '</select></div></div>';
897
1429
 
898
1430
  // Priority
899
- if (t === 'features' || t === 'epics') {
1431
+ if (t === 'tasks' || t === 'epics') {
900
1432
  html += '<div class="panel-field"><label>Priority</label><div class="field-with-icon"><span id="p-priority-icon">' + priorityIcon(item.priority) + '</span>' +
901
- '<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) {
902
1434
  return '<option value="' + p + '"' + (item.priority === p ? ' selected' : '') + '>' + (PRIORITY_LABELS[p] || p) + '</option>';
903
1435
  }).join('') + '</select></div></div>';
904
1436
  }
@@ -906,45 +1438,115 @@ function renderPanel() {
906
1438
  html += '</div>'; // close panel-props
907
1439
 
908
1440
  // Type-specific fields
909
- if (t === 'features') {
1441
+ if (t === 'tasks') {
910
1442
  html += '<div class="panel-props">';
911
- 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>';
912
1444
 
913
1445
  var assignedVal = Array.isArray(item.assigned) ? item.assigned.join(', ') : (item.assigned || '');
914
- 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>';
915
1447
  html += '</div>';
916
1448
 
917
1449
  html += '<div class="panel-props">';
918
1450
  // Sprint
919
- 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>' +
920
1452
  D.allSprints.map(function(s) { return '<option value="' + escHtml(s.id || '') + '"' + (item.sprint === s.id ? ' selected' : '') + '>' + escHtml(s.id || '') + '</option>'; }).join('') +
921
1453
  '</select></div>';
922
1454
 
923
- // Epic (read-only info)
924
- 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>';
925
- 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
+ }
926
1502
  }
927
1503
 
928
1504
  if (t === 'milestones') {
929
- 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
+ }
930
1514
  }
931
1515
 
932
1516
  // Description / Content
933
- 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>';
934
1518
 
935
1519
  html += '</div>'; // close panel-body
936
1520
 
937
- html += '<div class="panel-footer">' +
938
- '<button class="btn" id="panel-cancel-btn">Cancel</button>' +
939
- '<button class="btn btn-primary" id="panel-save-btn">Save Changes</button>' +
940
- '</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>';
941
1532
 
942
1533
  panel.innerHTML = html;
943
1534
 
944
1535
  // Event listeners
945
1536
  document.getElementById('panel-close-btn').addEventListener('click', closePanel);
946
1537
  document.getElementById('panel-cancel-btn').addEventListener('click', closePanel);
947
- 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
+ }
948
1550
 
949
1551
  // Live icon updates
950
1552
  var statusSel = document.getElementById('p-status');
@@ -961,6 +1563,22 @@ function renderPanel() {
961
1563
  if (iconEl) iconEl.innerHTML = priorityIcon(prioritySel.value);
962
1564
  });
963
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
+ });
964
1582
  }
965
1583
 
966
1584
  async function savePanel() {
@@ -976,12 +1594,12 @@ async function savePanel() {
976
1594
  var statusEl = document.getElementById('p-status');
977
1595
  if (statusEl && statusEl.value !== (item.status || '')) updates.status = statusEl.value;
978
1596
 
979
- if (t === 'features' || t === 'epics') {
1597
+ if (t === 'tasks' || t === 'epics') {
980
1598
  var priorityEl = document.getElementById('p-priority');
981
1599
  if (priorityEl && priorityEl.value !== (item.priority || '')) updates.priority = priorityEl.value || null;
982
1600
  }
983
1601
 
984
- if (t === 'features') {
1602
+ if (t === 'tasks') {
985
1603
  var pointsEl = document.getElementById('p-points');
986
1604
  if (pointsEl) {
987
1605
  var pv = pointsEl.value ? Number(pointsEl.value) : null;
@@ -1020,6 +1638,60 @@ async function savePanel() {
1020
1638
  }
1021
1639
  }
1022
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
+
1023
1695
  // Close panel on overlay click or Escape
1024
1696
  document.getElementById('panel-overlay').addEventListener('click', closePanel);
1025
1697
  document.addEventListener('keydown', function(e) {
@@ -1031,11 +1703,17 @@ document.addEventListener('keydown', function(e) {
1031
1703
  ══════════════════════════════════════════════════════════════ */
1032
1704
  function switchView(name) {
1033
1705
  document.querySelectorAll('.view').forEach(function(v) { v.classList.remove('active'); });
1034
- 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'); });
1035
1707
  var t = document.getElementById('view-' + name);
1036
1708
  if (t) t.classList.add('active');
1037
1709
  var l = document.querySelector('#sidebar-nav a[data-view="' + name + '"]');
1038
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
+ }
1039
1717
  }
1040
1718
 
1041
1719
  document.getElementById('sidebar-nav').addEventListener('click', function(e) {
@@ -1047,8 +1725,12 @@ document.getElementById('sidebar-nav').addEventListener('click', function(e) {
1047
1725
  });
1048
1726
 
1049
1727
  function handleHash() {
1050
- var hash = window.location.hash.replace('#', '') || 'board';
1051
- 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'];
1052
1734
  switchView(valid.indexOf(hash) !== -1 ? hash : 'board');
1053
1735
  }
1054
1736
  window.addEventListener('hashchange', handleHash);
@@ -1059,8 +1741,54 @@ window.addEventListener('hashchange', handleHash);
1059
1741
  (async function() {
1060
1742
  handleHash();
1061
1743
  await loadAll();
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
+
1062
1783
  renderAll();
1063
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
+ }
1064
1792
  })();
1065
1793
  </script>
1066
1794
  </body>