mdboard 1.2.0 → 1.3.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/bin.js +44 -16
- package/build.js +44 -0
- package/index.html +1835 -216
- package/package.json +7 -10
- package/src/cli/cli.js +362 -0
- package/src/cli/init.js +123 -0
- package/src/cli/status.js +150 -0
- package/src/cli/sync.js +194 -0
- package/src/cli/theme.js +142 -0
- package/src/client/app.js +266 -0
- package/src/client/board.js +157 -0
- package/src/client/core.js +331 -0
- package/src/client/editor.js +318 -0
- package/src/client/history.js +137 -0
- package/src/client/metrics.js +38 -0
- package/src/client/milestones.js +77 -0
- package/src/client/notes.js +183 -0
- package/src/client/overview.js +104 -0
- package/src/client/panel.js +637 -0
- package/src/client/styles.css +471 -0
- package/src/client/table.js +111 -0
- package/src/client/template.html +144 -0
- package/src/client/themes.js +261 -0
- package/src/client/workspace.js +164 -0
- package/src/core/agent-scanner.js +260 -0
- package/{config.js → src/core/config.js} +27 -2
- package/src/core/history.js +130 -0
- package/{scanner.js → src/core/scanner.js} +141 -21
- package/{yaml.js → src/core/yaml.js} +5 -1
- package/{api.js → src/server/api.js} +150 -9
- package/{server.js → src/server/server.js} +105 -32
- package/{watcher.js → src/server/watcher.js} +40 -9
- package/init.js +0 -109
- /package/{workspace.js → src/core/workspace.js} +0 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/* ══════════════════════════════════════════════════════════════
|
|
2
|
+
EDITOR.JS WRAPPER — md↔blocks conversion + Notion-like UX
|
|
3
|
+
══════════════════════════════════════════════════════════════ */
|
|
4
|
+
|
|
5
|
+
/* ── Inline Markdown ↔ HTML ──────────────────────────────── */
|
|
6
|
+
function inlineMdToHtml(text) {
|
|
7
|
+
if (!text) return '';
|
|
8
|
+
text = text.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>');
|
|
9
|
+
text = text.replace(/\*(.+?)\*/g, '<i>$1</i>');
|
|
10
|
+
text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
11
|
+
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
|
12
|
+
return text;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function inlineHtmlToMd(html) {
|
|
16
|
+
if (!html) return '';
|
|
17
|
+
var text = html.replace(/<br\s*\/?>/gi, '\n');
|
|
18
|
+
text = text.replace(/<a[^>]+href="([^"]*)"[^>]*>([^<]*)<\/a>/gi, '[$2]($1)');
|
|
19
|
+
text = text.replace(/<b>([^<]*)<\/b>/gi, '**$1**');
|
|
20
|
+
text = text.replace(/<strong>([^<]*)<\/strong>/gi, '**$1**');
|
|
21
|
+
text = text.replace(/<i>([^<]*)<\/i>/gi, '*$1*');
|
|
22
|
+
text = text.replace(/<em>([^<]*)<\/em>/gi, '*$1*');
|
|
23
|
+
text = text.replace(/<code>([^<]*)<\/code>/gi, '`$1`');
|
|
24
|
+
text = text.replace(/<mark[^>]*>([^<]*)<\/mark>/gi, '==$1==');
|
|
25
|
+
text = text.replace(/<[^>]+>/g, '');
|
|
26
|
+
return text;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* ── Markdown → Editor.js Blocks ────────────────────────── */
|
|
30
|
+
function markdownToBlocks(md) {
|
|
31
|
+
if (!md || !md.trim()) return [{ type: 'paragraph', data: { text: '' } }];
|
|
32
|
+
|
|
33
|
+
var lines = md.split('\n');
|
|
34
|
+
var blocks = [];
|
|
35
|
+
var i = 0;
|
|
36
|
+
|
|
37
|
+
while (i < lines.length) {
|
|
38
|
+
var line = lines[i];
|
|
39
|
+
|
|
40
|
+
if (line.trim() === '') { i++; continue; }
|
|
41
|
+
|
|
42
|
+
if (/^---\s*$/.test(line.trim()) || /^\*\*\*\s*$/.test(line.trim())) {
|
|
43
|
+
blocks.push({ type: 'delimiter', data: {} });
|
|
44
|
+
i++; continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
var hMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
48
|
+
if (hMatch) {
|
|
49
|
+
var lvl = Math.min(hMatch[1].length, 6);
|
|
50
|
+
blocks.push({ type: 'header', data: { text: inlineMdToHtml(hMatch[2]), level: lvl } });
|
|
51
|
+
i++; continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (line.trim().startsWith('```')) {
|
|
55
|
+
var code = [];
|
|
56
|
+
i++;
|
|
57
|
+
while (i < lines.length && !lines[i].trim().startsWith('```')) {
|
|
58
|
+
code.push(lines[i]);
|
|
59
|
+
i++;
|
|
60
|
+
}
|
|
61
|
+
blocks.push({ type: 'code', data: { code: code.join('\n') } });
|
|
62
|
+
i++; continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (line.trim().startsWith('> ')) {
|
|
66
|
+
var quoteLines = [];
|
|
67
|
+
while (i < lines.length && lines[i].trim().startsWith('> ')) {
|
|
68
|
+
quoteLines.push(lines[i].trim().substring(2));
|
|
69
|
+
i++;
|
|
70
|
+
}
|
|
71
|
+
blocks.push({ type: 'quote', data: { text: inlineMdToHtml(quoteLines.join('<br>')), caption: '' } });
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (/^\s*-\s+\[[ x]\]\s+/.test(line)) {
|
|
76
|
+
var checkItems = [];
|
|
77
|
+
while (i < lines.length && /^\s*-\s+\[[ x]\]\s+/.test(lines[i])) {
|
|
78
|
+
var cm = lines[i].match(/^\s*-\s+\[([ x])\]\s+(.+)$/);
|
|
79
|
+
if (cm) checkItems.push({ content: inlineMdToHtml(cm[2]), meta: { checked: cm[1] === 'x' }, items: [] });
|
|
80
|
+
i++;
|
|
81
|
+
}
|
|
82
|
+
blocks.push({ type: 'list', data: { style: 'checklist', items: checkItems } });
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (/^\s*[-*]\s+/.test(line) && !/^\s*-\s+\[/.test(line)) {
|
|
87
|
+
var listItems = [];
|
|
88
|
+
while (i < lines.length && /^\s*[-*]\s+/.test(lines[i]) && !/^\s*-\s+\[/.test(lines[i])) {
|
|
89
|
+
var lm = lines[i].match(/^\s*[-*]\s+(.+)$/);
|
|
90
|
+
if (lm) listItems.push({ content: inlineMdToHtml(lm[1]), items: [] });
|
|
91
|
+
i++;
|
|
92
|
+
}
|
|
93
|
+
blocks.push({ type: 'list', data: { style: 'unordered', items: listItems } });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (/^\s*\d+\.\s+/.test(line)) {
|
|
98
|
+
var olItems = [];
|
|
99
|
+
while (i < lines.length && /^\s*\d+\.\s+/.test(lines[i])) {
|
|
100
|
+
var om = lines[i].match(/^\s*\d+\.\s+(.+)$/);
|
|
101
|
+
if (om) olItems.push({ content: inlineMdToHtml(om[1]), items: [] });
|
|
102
|
+
i++;
|
|
103
|
+
}
|
|
104
|
+
blocks.push({ type: 'list', data: { style: 'ordered', items: olItems } });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
var paraLines = [];
|
|
109
|
+
while (i < lines.length && lines[i].trim() !== '' &&
|
|
110
|
+
!/^#{1,6}\s/.test(lines[i]) && !/^```/.test(lines[i]) &&
|
|
111
|
+
!/^>\s/.test(lines[i]) && !/^\s*[-*]\s/.test(lines[i]) &&
|
|
112
|
+
!/^\s*\d+\.\s/.test(lines[i]) && !/^---\s*$/.test(lines[i].trim()) &&
|
|
113
|
+
!/^\*\*\*\s*$/.test(lines[i].trim())) {
|
|
114
|
+
paraLines.push(lines[i]);
|
|
115
|
+
i++;
|
|
116
|
+
}
|
|
117
|
+
if (paraLines.length > 0) {
|
|
118
|
+
blocks.push({ type: 'paragraph', data: { text: inlineMdToHtml(paraLines.join('<br>')) } });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return blocks.length > 0 ? blocks : [{ type: 'paragraph', data: { text: '' } }];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* ── Editor.js Blocks → Markdown ────────────────────────── */
|
|
126
|
+
function blocksToMarkdown(blocks) {
|
|
127
|
+
if (!blocks || blocks.length === 0) return '';
|
|
128
|
+
|
|
129
|
+
var parts = [];
|
|
130
|
+
for (var i = 0; i < blocks.length; i++) {
|
|
131
|
+
var b = blocks[i];
|
|
132
|
+
switch (b.type) {
|
|
133
|
+
case 'header':
|
|
134
|
+
var prefix = '';
|
|
135
|
+
for (var h = 0; h < (b.data.level || 2); h++) prefix += '#';
|
|
136
|
+
parts.push(prefix + ' ' + inlineHtmlToMd(b.data.text));
|
|
137
|
+
break;
|
|
138
|
+
case 'paragraph':
|
|
139
|
+
parts.push(inlineHtmlToMd(b.data.text));
|
|
140
|
+
break;
|
|
141
|
+
case 'list':
|
|
142
|
+
var items = b.data.items || [];
|
|
143
|
+
var listLines = [];
|
|
144
|
+
for (var li = 0; li < items.length; li++) {
|
|
145
|
+
var itemText = typeof items[li] === 'string' ? items[li] : (items[li].content || items[li].text || '');
|
|
146
|
+
if (b.data.style === 'checklist') {
|
|
147
|
+
var chk = (typeof items[li] === 'object' && items[li].meta && items[li].meta.checked) ? 'x' : ' ';
|
|
148
|
+
listLines.push('- [' + chk + '] ' + inlineHtmlToMd(itemText));
|
|
149
|
+
} else if (b.data.style === 'ordered') {
|
|
150
|
+
listLines.push((li + 1) + '. ' + inlineHtmlToMd(itemText));
|
|
151
|
+
} else {
|
|
152
|
+
listLines.push('- ' + inlineHtmlToMd(itemText));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
parts.push(listLines.join('\n'));
|
|
156
|
+
break;
|
|
157
|
+
case 'checklist':
|
|
158
|
+
var cItems = b.data.items || [];
|
|
159
|
+
var checkLines = [];
|
|
160
|
+
for (var ci = 0; ci < cItems.length; ci++) {
|
|
161
|
+
var check = cItems[ci].checked ? 'x' : ' ';
|
|
162
|
+
checkLines.push('- [' + check + '] ' + inlineHtmlToMd(cItems[ci].text || cItems[ci].content || ''));
|
|
163
|
+
}
|
|
164
|
+
parts.push(checkLines.join('\n'));
|
|
165
|
+
break;
|
|
166
|
+
case 'code':
|
|
167
|
+
parts.push('```\n' + (b.data.code || '') + '\n```');
|
|
168
|
+
break;
|
|
169
|
+
case 'quote':
|
|
170
|
+
var qText = inlineHtmlToMd(b.data.text || '');
|
|
171
|
+
var qLines = qText.split('\n');
|
|
172
|
+
var quoteParts = [];
|
|
173
|
+
for (var qi = 0; qi < qLines.length; qi++) {
|
|
174
|
+
quoteParts.push('> ' + qLines[qi]);
|
|
175
|
+
}
|
|
176
|
+
parts.push(quoteParts.join('\n'));
|
|
177
|
+
break;
|
|
178
|
+
case 'delimiter':
|
|
179
|
+
parts.push('---');
|
|
180
|
+
break;
|
|
181
|
+
default:
|
|
182
|
+
if (b.data && b.data.text) parts.push(inlineHtmlToMd(b.data.text));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return parts.join('\n\n');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* ── Editor.js Instance Management ──────────────────────── */
|
|
189
|
+
function initEditor(holderId, markdown, options) {
|
|
190
|
+
if (typeof EditorJS === 'undefined') {
|
|
191
|
+
var el = document.getElementById(holderId);
|
|
192
|
+
if (el) {
|
|
193
|
+
var ta = document.createElement('textarea');
|
|
194
|
+
ta.value = markdown || '';
|
|
195
|
+
ta.className = 'editor-fallback-textarea';
|
|
196
|
+
ta.placeholder = (options && options.placeholder) || 'Type / for commands...';
|
|
197
|
+
ta.id = holderId + '-fallback';
|
|
198
|
+
el.appendChild(ta);
|
|
199
|
+
}
|
|
200
|
+
return { _fallback: true, _holderId: holderId };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
var opts = options || {};
|
|
204
|
+
var tools = {};
|
|
205
|
+
|
|
206
|
+
if (typeof Header !== 'undefined') {
|
|
207
|
+
tools.header = {
|
|
208
|
+
class: Header,
|
|
209
|
+
inlineToolbar: true,
|
|
210
|
+
config: { levels: [1, 2, 3, 4], defaultLevel: 2 },
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
if (typeof EditorjsList !== 'undefined') tools.list = { class: EditorjsList, inlineToolbar: true, config: { maxLevel: 3 } };
|
|
214
|
+
if (typeof CodeTool !== 'undefined') tools.code = { class: CodeTool };
|
|
215
|
+
if (typeof Quote !== 'undefined') tools.quote = { class: Quote, inlineToolbar: true, config: { quotePlaceholder: 'Write a quote...', captionPlaceholder: '' } };
|
|
216
|
+
if (typeof Delimiter !== 'undefined') tools.delimiter = { class: Delimiter };
|
|
217
|
+
if (typeof Marker !== 'undefined') tools.marker = { class: Marker };
|
|
218
|
+
if (typeof InlineCode !== 'undefined') tools.inlineCode = { class: InlineCode };
|
|
219
|
+
|
|
220
|
+
var blocks = markdownToBlocks(markdown || '');
|
|
221
|
+
|
|
222
|
+
var editor = new EditorJS({
|
|
223
|
+
holder: holderId,
|
|
224
|
+
tools: tools,
|
|
225
|
+
data: { blocks: blocks },
|
|
226
|
+
placeholder: opts.placeholder || 'Type / for commands...',
|
|
227
|
+
minHeight: opts.minHeight || 0,
|
|
228
|
+
autofocus: opts.autofocus || false,
|
|
229
|
+
onChange: opts.onChange || function() {},
|
|
230
|
+
defaultBlock: 'paragraph',
|
|
231
|
+
inlineToolbar: ['bold', 'italic', 'link', 'marker', 'inlineCode'],
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
return editor;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function getEditorMarkdown(editorInstance) {
|
|
238
|
+
if (!editorInstance) return '';
|
|
239
|
+
if (editorInstance._fallback) {
|
|
240
|
+
var ta = document.getElementById(editorInstance._holderId + '-fallback');
|
|
241
|
+
return ta ? ta.value : '';
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
var data = await editorInstance.save();
|
|
245
|
+
return blocksToMarkdown(data.blocks);
|
|
246
|
+
} catch (e) {
|
|
247
|
+
return '';
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function destroyEditor(editorInstance) {
|
|
252
|
+
if (!editorInstance) return;
|
|
253
|
+
if (editorInstance._fallback) return;
|
|
254
|
+
if (typeof editorInstance.destroy === 'function') {
|
|
255
|
+
try { editorInstance.destroy(); } catch (e) { /* ignore */ }
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/* ── Simple Markdown → HTML Renderer (readonly mode) ──── */
|
|
260
|
+
function simpleMarkdownRender(md) {
|
|
261
|
+
if (!md) return '';
|
|
262
|
+
var lines = md.split('\n');
|
|
263
|
+
var html = '';
|
|
264
|
+
var inCode = false;
|
|
265
|
+
var inList = false;
|
|
266
|
+
var listType = '';
|
|
267
|
+
|
|
268
|
+
for (var i = 0; i < lines.length; i++) {
|
|
269
|
+
var line = lines[i];
|
|
270
|
+
|
|
271
|
+
if (line.trim().startsWith('```')) {
|
|
272
|
+
if (inCode) { html += '</code></pre>'; inCode = false; }
|
|
273
|
+
else { html += '<pre><code>'; inCode = true; }
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (inCode) { html += escHtml(line) + '\n'; continue; }
|
|
277
|
+
|
|
278
|
+
if (inList && !/^\s*[-*]\s/.test(line) && !/^\s*\d+\.\s/.test(line)) {
|
|
279
|
+
html += '</' + listType + '>'; inList = false;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (line.trim() === '') { if (!inList) html += '<br>'; continue; }
|
|
283
|
+
if (/^---\s*$/.test(line.trim())) { html += '<hr>'; continue; }
|
|
284
|
+
|
|
285
|
+
var hm = line.match(/^(#{1,6})\s+(.+)$/);
|
|
286
|
+
if (hm) { html += '<h' + hm[1].length + '>' + inlineMdToHtml(hm[2]) + '</h' + hm[1].length + '>'; continue; }
|
|
287
|
+
|
|
288
|
+
if (/^>\s/.test(line)) { html += '<blockquote>' + inlineMdToHtml(line.substring(2)) + '</blockquote>'; continue; }
|
|
289
|
+
|
|
290
|
+
if (/^\s*-\s+\[[ x]\]\s/.test(line)) {
|
|
291
|
+
var chm = line.match(/^\s*-\s+\[([ x])\]\s+(.+)$/);
|
|
292
|
+
if (chm) {
|
|
293
|
+
var checked = chm[1] === 'x';
|
|
294
|
+
html += '<div class="md-check"><span class="md-check-box' + (checked ? ' checked' : '') + '">' + (checked ? '✓' : '') + '</span> ' + inlineMdToHtml(chm[2]) + '</div>';
|
|
295
|
+
}
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (/^\s*[-*]\s+/.test(line)) {
|
|
300
|
+
if (!inList || listType !== 'ul') { if (inList) html += '</' + listType + '>'; html += '<ul>'; inList = true; listType = 'ul'; }
|
|
301
|
+
var lmatch = line.match(/^\s*[-*]\s+(.+)$/);
|
|
302
|
+
if (lmatch) html += '<li>' + inlineMdToHtml(lmatch[1]) + '</li>';
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (/^\s*\d+\.\s+/.test(line)) {
|
|
306
|
+
if (!inList || listType !== 'ol') { if (inList) html += '</' + listType + '>'; html += '<ol>'; inList = true; listType = 'ol'; }
|
|
307
|
+
var omatch = line.match(/^\s*\d+\.\s+(.+)$/);
|
|
308
|
+
if (omatch) html += '<li>' + inlineMdToHtml(omatch[1]) + '</li>';
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
html += '<p>' + inlineMdToHtml(line) + '</p>';
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (inCode) html += '</code></pre>';
|
|
316
|
+
if (inList) html += '</' + listType + '>';
|
|
317
|
+
return html;
|
|
318
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/* ══════════════════════════════════════════════════════════════
|
|
2
|
+
HISTORY — Project switch modal
|
|
3
|
+
══════════════════════════════════════════════════════════════ */
|
|
4
|
+
|
|
5
|
+
function openHistoryModal() {
|
|
6
|
+
var overlay = document.getElementById('history-overlay');
|
|
7
|
+
var modal = document.getElementById('history-modal');
|
|
8
|
+
if (!overlay || !modal) return;
|
|
9
|
+
|
|
10
|
+
overlay.classList.add('open');
|
|
11
|
+
modal.classList.add('open');
|
|
12
|
+
modal.innerHTML = '<div class="history-header"><h2>Switch Project</h2><button class="panel-close" id="history-close">×</button></div><div class="history-body"><div class="history-loading">Loading...</div></div>';
|
|
13
|
+
|
|
14
|
+
document.getElementById('history-close').onclick = closeHistoryModal;
|
|
15
|
+
overlay.onclick = closeHistoryModal;
|
|
16
|
+
|
|
17
|
+
fetchJson('/api/history').then(function(entries) {
|
|
18
|
+
renderHistoryList(entries || []);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function closeHistoryModal() {
|
|
23
|
+
var overlay = document.getElementById('history-overlay');
|
|
24
|
+
var modal = document.getElementById('history-modal');
|
|
25
|
+
if (overlay) overlay.classList.remove('open');
|
|
26
|
+
if (modal) modal.classList.remove('open');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function renderHistoryList(entries) {
|
|
30
|
+
var modal = document.getElementById('history-modal');
|
|
31
|
+
if (!modal) return;
|
|
32
|
+
|
|
33
|
+
var body = modal.querySelector('.history-body');
|
|
34
|
+
if (!body) return;
|
|
35
|
+
|
|
36
|
+
if (entries.length === 0) {
|
|
37
|
+
body.innerHTML = '<div class="history-empty">No projects in history yet.<br>Open mdboard from a project directory to add it.</div>';
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Sort: current first, then by lastOpened descending
|
|
42
|
+
entries.sort(function(a, b) {
|
|
43
|
+
if (a.isCurrent && !b.isCurrent) return -1;
|
|
44
|
+
if (!a.isCurrent && b.isCurrent) return 1;
|
|
45
|
+
return new Date(b.lastOpened) - new Date(a.lastOpened);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
var html = '';
|
|
49
|
+
for (var i = 0; i < entries.length; i++) {
|
|
50
|
+
var e = entries[i];
|
|
51
|
+
var initial = (e.name || 'P').charAt(0).toUpperCase();
|
|
52
|
+
var relTime = timeAgo(e.lastOpened);
|
|
53
|
+
var currentClass = e.isCurrent ? ' history-item-current' : '';
|
|
54
|
+
var currentBadge = e.isCurrent ? '<span class="history-current-badge">Current</span>' : '';
|
|
55
|
+
|
|
56
|
+
html += '<div class="history-item' + currentClass + '" data-path="' + escHtml(e.path) + '">' +
|
|
57
|
+
'<div class="history-item-icon">' + escHtml(initial) + '</div>' +
|
|
58
|
+
'<div class="history-item-info">' +
|
|
59
|
+
'<div class="history-item-name">' + escHtml(e.name) + currentBadge + '</div>' +
|
|
60
|
+
'<div class="history-item-path">' + escHtml(e.path) + '</div>' +
|
|
61
|
+
'</div>' +
|
|
62
|
+
'<div class="history-item-time">' + escHtml(relTime) + '</div>' +
|
|
63
|
+
'</div>';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
body.innerHTML = html;
|
|
67
|
+
|
|
68
|
+
// Attach click handlers
|
|
69
|
+
var items = body.querySelectorAll('.history-item');
|
|
70
|
+
for (var j = 0; j < items.length; j++) {
|
|
71
|
+
items[j].addEventListener('click', function() {
|
|
72
|
+
var itemPath = this.getAttribute('data-path');
|
|
73
|
+
if (this.classList.contains('history-item-current')) return;
|
|
74
|
+
switchProject(itemPath);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function switchProject(projectPath) {
|
|
80
|
+
var body = document.querySelector('#history-modal .history-body');
|
|
81
|
+
if (body) body.innerHTML = '<div class="history-loading">Switching project...</div>';
|
|
82
|
+
|
|
83
|
+
fetch('/api/history/switch', {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: { 'Content-Type': 'application/json' },
|
|
86
|
+
body: JSON.stringify({ path: projectPath }),
|
|
87
|
+
})
|
|
88
|
+
.then(function(r) { return r.json(); })
|
|
89
|
+
.then(function(data) {
|
|
90
|
+
if (data && data.ok) {
|
|
91
|
+
closeHistoryModal();
|
|
92
|
+
// Reload everything — the SSE event will also trigger for other tabs
|
|
93
|
+
D.activeSource = null;
|
|
94
|
+
D.loaded = false;
|
|
95
|
+
loadAll().then(function() {
|
|
96
|
+
initFromConfig();
|
|
97
|
+
if (hasWorkspace()) {
|
|
98
|
+
buildSourceRail();
|
|
99
|
+
D.activeSource = null;
|
|
100
|
+
switchSource('overview');
|
|
101
|
+
} else {
|
|
102
|
+
renderAll();
|
|
103
|
+
renderSidebarLogo();
|
|
104
|
+
// Hide source rail if no workspace
|
|
105
|
+
var rail = document.getElementById('source-rail');
|
|
106
|
+
if (rail) rail.innerHTML = '';
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
showToast('Switched to ' + (data.name || 'project'), 'success');
|
|
110
|
+
} else {
|
|
111
|
+
showToast('Error: ' + (data.error || 'Switch failed'), 'error');
|
|
112
|
+
closeHistoryModal();
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
.catch(function(err) {
|
|
116
|
+
showToast('Error: ' + err.message, 'error');
|
|
117
|
+
closeHistoryModal();
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function checkAutoOpenHistory() {
|
|
122
|
+
if (D.config && D.config.hasProject === false) {
|
|
123
|
+
openHistoryModal();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function timeAgo(dateStr) {
|
|
128
|
+
if (!dateStr) return '';
|
|
129
|
+
var now = Date.now();
|
|
130
|
+
var then = new Date(dateStr).getTime();
|
|
131
|
+
var diff = Math.floor((now - then) / 1000);
|
|
132
|
+
if (diff < 60) return 'just now';
|
|
133
|
+
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
|
|
134
|
+
if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
|
|
135
|
+
if (diff < 604800) return Math.floor(diff / 86400) + 'd ago';
|
|
136
|
+
return new Date(dateStr).toLocaleDateString();
|
|
137
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/* ══════════════════════════════════════════════════════════════
|
|
2
|
+
METRICS VIEW
|
|
3
|
+
══════════════════════════════════════════════════════════════ */
|
|
4
|
+
function renderMetrics() {
|
|
5
|
+
var c = document.getElementById('metrics-container');
|
|
6
|
+
var taskPlural = ENTITY_NAMES.task ? ENTITY_NAMES.task.plural : 'Tasks';
|
|
7
|
+
if (!D.loaded) { c.innerHTML = '<div class="metrics-grid">' + '<div class="skeleton skeleton-card" style="height:200px"></div>'.repeat(4) + '</div>'; return; }
|
|
8
|
+
|
|
9
|
+
var msProg = D.milestones.length ? D.milestones.map(function(ms) {
|
|
10
|
+
var pct = ms.progress || 0;
|
|
11
|
+
var clr = pct >= 100 ? 'var(--success)' : pct > 50 ? 'var(--warning)' : 'var(--accent)';
|
|
12
|
+
return '<div class="health-row"><div class="health-dot" style="background:' + clr + '"></div>' + milestoneIcon(ms.status) + '<div class="health-label">' + escHtml(ms.title || ms.id || '') + '</div><div class="health-val">' + pct + '%</div></div>';
|
|
13
|
+
}).join('') : '<div style="color:var(--text3);padding:8px 0">No milestones.</div>';
|
|
14
|
+
|
|
15
|
+
var statuses = {};
|
|
16
|
+
D.tasks.forEach(function(f) { var s = f.status || 'unknown'; statuses[s] = (statuses[s] || 0) + 1; });
|
|
17
|
+
var statusHtml = Object.keys(statuses).map(function(s) {
|
|
18
|
+
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>';
|
|
19
|
+
}).join('') || '<div style="color:var(--text3);padding:8px 0">No data.</div>';
|
|
20
|
+
|
|
21
|
+
var priorities = {};
|
|
22
|
+
D.tasks.forEach(function(f) { var p = f.priority || 'none'; priorities[p] = (priorities[p] || 0) + 1; });
|
|
23
|
+
var priorityHtml = Object.keys(priorities).map(function(p) {
|
|
24
|
+
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>';
|
|
25
|
+
}).join('') || '<div style="color:var(--text3);padding:8px 0">No data.</div>';
|
|
26
|
+
|
|
27
|
+
var h = D.health || {};
|
|
28
|
+
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>' +
|
|
29
|
+
'<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>' +
|
|
30
|
+
'<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>' +
|
|
31
|
+
'<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>';
|
|
32
|
+
|
|
33
|
+
c.innerHTML = '<div class="metrics-grid">' +
|
|
34
|
+
'<div class="metric-card"><h3>Milestone Progress</h3>' + msProg + '</div>' +
|
|
35
|
+
'<div class="metric-card"><h3>Status Breakdown</h3>' + statusHtml + '</div>' +
|
|
36
|
+
'<div class="metric-card"><h3>Priority Breakdown</h3>' + priorityHtml + '</div>' +
|
|
37
|
+
'<div class="metric-card"><h3>Project Health</h3>' + qualHtml + '</div></div>';
|
|
38
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/* ══════════════════════════════════════════════════════════════
|
|
2
|
+
MILESTONES VIEW
|
|
3
|
+
══════════════════════════════════════════════════════════════ */
|
|
4
|
+
var msFilter = { status: '' };
|
|
5
|
+
|
|
6
|
+
function renderMsFilters() {
|
|
7
|
+
var c = document.getElementById('ms-filters');
|
|
8
|
+
var msStatuses = (D.config && D.config.statuses && D.config.statuses.milestone) ?
|
|
9
|
+
D.config.statuses.milestone.map(function(s) { return s; }) :
|
|
10
|
+
[{key:'planned',label:'Planned'},{key:'active',label:'Active'},{key:'completed',label:'Completed'}];
|
|
11
|
+
|
|
12
|
+
c.innerHTML = '<label>Filter:</label>' +
|
|
13
|
+
'<select data-msf="status"><option value="">All Statuses</option>' +
|
|
14
|
+
msStatuses.map(function(s) {
|
|
15
|
+
return '<option value="' + s.key + '"' + (msFilter.status === s.key ? ' selected' : '') + '>' + escHtml(s.label) + '</option>';
|
|
16
|
+
}).join('') + '</select>';
|
|
17
|
+
c.querySelector('select[data-msf]').addEventListener('change', function(e) { msFilter.status = e.target.value; renderMilestones(); });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function renderMilestones() {
|
|
21
|
+
var c = document.getElementById('milestones-container');
|
|
22
|
+
var taskPlural = ENTITY_NAMES.task ? ENTITY_NAMES.task.plural.toLowerCase() : 'tasks';
|
|
23
|
+
var msDir = ENTITY_NAMES.milestone ? ENTITY_NAMES.milestone.dir : 'milestones';
|
|
24
|
+
var msPlural = ENTITY_NAMES.milestone ? ENTITY_NAMES.milestone.plural.toLowerCase() : 'milestones';
|
|
25
|
+
if (!D.loaded) { c.innerHTML = '<div class="loading-container"><div class="skeleton skeleton-card" style="height:200px"></div></div>'; return; }
|
|
26
|
+
|
|
27
|
+
var milestones = D.milestones;
|
|
28
|
+
if (msFilter.status) milestones = milestones.filter(function(m) { return m.status === msFilter.status; });
|
|
29
|
+
|
|
30
|
+
if (!milestones.length) {
|
|
31
|
+
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>';
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
c.innerHTML = milestones.map(function(ms) {
|
|
36
|
+
var pct = ms.progress || 0;
|
|
37
|
+
var fc = ms.featureCount || 0, cc = ms.completedCount || 0;
|
|
38
|
+
var msEpics = D.epics.filter(function(e) { return e.milestone === (ms.id || ms._dir); });
|
|
39
|
+
var epicCards = msEpics.length ? msEpics.map(function(e) {
|
|
40
|
+
var ep = e.progress || 0;
|
|
41
|
+
var ec = epicColor(e.id || e.title || '');
|
|
42
|
+
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>' +
|
|
43
|
+
'<div class="epic-counts">' + (e.completedCount || 0) + ' / ' + (e.featureCount || 0) + ' ' + escHtml(taskPlural) + ' · ' + (e.totalPoints || 0) + ' pts</div>' +
|
|
44
|
+
'<div class="progress progress-accent"><div class="progress-fill" style="width:' + ep + '%;background:' + ec + '"></div></div>' +
|
|
45
|
+
'<div class="epic-meta">' +
|
|
46
|
+
(e.status ? '<span class="badge ' + badgeClass('badge', e.status) + '">' + statusIcon(e.status) + ' ' + escHtml(e.status) + '</span>' : '') +
|
|
47
|
+
(e.priority ? '<span class="badge ' + badgeClass('badge', e.priority) + '">' + priorityIcon(e.priority) + ' ' + escHtml(e.priority) + '</span>' : '') +
|
|
48
|
+
'</div></div>';
|
|
49
|
+
}).join('') : '<div style="color:var(--text3);font-size:.82rem">No epics yet.</div>';
|
|
50
|
+
|
|
51
|
+
return '<div class="ms-card" data-ms-id="' + escHtml(ms.id || '') + '">' +
|
|
52
|
+
'<div class="ms-header"><h2>' + milestoneIcon(ms.status) + ' ' + escHtml(ms.title || ms.id || '') + '</h2>' +
|
|
53
|
+
'<span class="badge ' + badgeClass('badge', ms.status) + '">' + escHtml(ms.status || '') + '</span>' +
|
|
54
|
+
(ms.deadline ? '<span class="ms-deadline">' + fmtDate(ms.deadline) + '</span>' : '') + '</div>' +
|
|
55
|
+
'<div class="ms-progress"><div class="progress-label"><span>' + cc + ' / ' + fc + ' ' + escHtml(taskPlural) + '</span><span>' + pct + '%</span></div>' +
|
|
56
|
+
'<div class="progress progress-lg progress-success"><div class="progress-fill" style="width:' + pct + '%"></div></div></div>' +
|
|
57
|
+
'<div class="ms-epics">' + epicCards + '</div></div>';
|
|
58
|
+
}).join('');
|
|
59
|
+
|
|
60
|
+
// Click handlers for milestones
|
|
61
|
+
c.querySelectorAll('.ms-card[data-ms-id]').forEach(function(card) {
|
|
62
|
+
card.addEventListener('click', function(e) {
|
|
63
|
+
if (e.target.closest('.ms-epic')) return;
|
|
64
|
+
var ms = D.milestones.find(function(m) { return m.id === card.dataset.msId; });
|
|
65
|
+
if (ms) openPanel('milestones', ms);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Click handlers for epics
|
|
70
|
+
c.querySelectorAll('.ms-epic[data-epic-id]').forEach(function(card) {
|
|
71
|
+
card.addEventListener('click', function(e) {
|
|
72
|
+
e.stopPropagation();
|
|
73
|
+
var epic = D.epics.find(function(ep) { return ep.id === card.dataset.epicId; });
|
|
74
|
+
if (epic) openPanel('epics', epic);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|