memstack-skill-loader 3.5.0__py3-none-any.whl

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.
@@ -0,0 +1,829 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>MemStack™ Dashboard</title>
7
+ <style>
8
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9
+
10
+ body {
11
+ background: #0d1117;
12
+ color: #e6edf3;
13
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
14
+ line-height: 1.5;
15
+ padding: 2rem;
16
+ min-height: 100vh;
17
+ }
18
+
19
+ header { text-align: center; margin-bottom: 1.5rem; }
20
+ header h1 { font-size: 1.6rem; font-weight: 700; }
21
+ header h1 span { color: #58a6ff; }
22
+ header p { color: #8b949e; font-size: 0.85rem; margin-top: 0.25rem; }
23
+
24
+ /* ── Tab bar ── */
25
+ .tab-bar {
26
+ display: flex; justify-content: center; gap: 0; margin-bottom: 2rem;
27
+ border-bottom: 1px solid #30363d;
28
+ }
29
+ .tab-btn {
30
+ background: none; border: none; color: #8b949e; font-size: 0.95rem;
31
+ padding: 0.6rem 1.5rem; cursor: pointer; border-bottom: 2px solid transparent;
32
+ transition: color 0.15s, border-color 0.15s;
33
+ }
34
+ .tab-btn:hover { color: #e6edf3; }
35
+ .tab-btn.active { color: #58a6ff; border-bottom-color: #58a6ff; font-weight: 600; }
36
+ .tab-content { display: none; }
37
+ .tab-content.active { display: block; }
38
+
39
+ /* ── Stats tab (existing) ── */
40
+ .cards { display: flex; gap: 0.6rem; margin-bottom: 1.5rem; }
41
+ .card {
42
+ flex: 1; min-width: 0;
43
+ background: #161b22; border: 1px solid #30363d; border-radius: 6px;
44
+ padding: 0.6rem 0.5rem; text-align: center;
45
+ }
46
+ .card .label { font-size: 0.7rem; color: #8b949e; margin-bottom: 0.2rem; }
47
+ .card .value { font-size: 1.15rem; font-weight: 700; }
48
+ .card .value.accent { color: #58a6ff; }
49
+
50
+ .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 2rem; }
51
+ .panel {
52
+ background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 1.2rem;
53
+ }
54
+ .panel h2 { font-size: 0.95rem; font-weight: 600; margin-bottom: 1rem; color: #e6edf3; }
55
+ .empty-state { color: #484f58; font-size: 0.85rem; }
56
+
57
+ .bar-row { display: flex; align-items: center; padding: 0.3rem 0; gap: 0.5rem; }
58
+ .bar-row .name { flex: 0 0 120px; font-size: 0.85rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
59
+ .bar-row .bar-wrap { flex: 1; height: 6px; background: #21262d; border-radius: 3px; }
60
+ .bar-row .bar { height: 100%; border-radius: 3px; transition: width 0.3s; }
61
+ .bar-row .count { flex: 0 0 40px; text-align: right; font-size: 0.82rem; color: #8b949e; }
62
+
63
+ .cat-row { display: flex; align-items: center; padding: 0.4rem 0; cursor: pointer; }
64
+ .cat-row:hover { background: rgba(255,255,255,0.03); border-radius: 4px; }
65
+ .cat-chevron { width: 16px; font-size: 0.7rem; color: #8b949e; flex-shrink: 0; transition: transform 0.15s; }
66
+ .cat-chevron.open { transform: rotate(90deg); }
67
+ .cat-dot { width: 10px; height: 10px; border-radius: 50%; margin-right: 0.5rem; flex-shrink: 0; }
68
+ .cat-name { flex: 1; font-size: 0.85rem; }
69
+ .cat-count { font-size: 0.82rem; color: #8b949e; }
70
+ .cat-skills { padding-left: 1.75rem; }
71
+ .cat-skills .cat-skill-row { display: flex; justify-content: space-between; padding: 0.2rem 0; font-size: 0.8rem; color: #8b949e; }
72
+ .cat-skills .cat-skill-row .fires { font-variant-numeric: tabular-nums; }
73
+
74
+ .proj-row { display: flex; justify-content: space-between; padding: 0.3rem 0; font-size: 0.85rem; }
75
+ .proj-row .fires { color: #8b949e; }
76
+
77
+
78
+ footer { text-align: center; color: #484f58; font-size: 0.75rem; margin-top: 2rem; }
79
+
80
+ /* ── Skills tab ── */
81
+ .skills-header {
82
+ display: flex; align-items: center; justify-content: space-between;
83
+ margin-bottom: 1rem; flex-wrap: wrap; gap: 0.5rem;
84
+ }
85
+ .skills-header h2 { font-size: 1rem; font-weight: 600; }
86
+ .skills-header .project-name { color: #8b949e; font-size: 0.85rem; }
87
+ .skills-summary {
88
+ font-size: 0.82rem; color: #8b949e; margin-bottom: 1rem;
89
+ }
90
+
91
+ .filter-bar {
92
+ display: flex; gap: 0.75rem; margin-bottom: 1.5rem; flex-wrap: wrap; align-items: center;
93
+ }
94
+ .filter-bar input[type="text"] {
95
+ flex: 1; min-width: 200px; padding: 0.5rem 0.75rem;
96
+ background: #0d1117; border: 1px solid #30363d; border-radius: 6px;
97
+ color: #e6edf3; font-size: 0.85rem; outline: none;
98
+ }
99
+ .filter-bar input[type="text"]:focus { border-color: #58a6ff; }
100
+ .filter-bar input[type="text"]::placeholder { color: #484f58; }
101
+ .filter-bar select {
102
+ padding: 0.5rem 0.6rem; background: #161b22; border: 1px solid #30363d;
103
+ border-radius: 6px; color: #e6edf3; font-size: 0.82rem; outline: none; cursor: pointer;
104
+ }
105
+ .filter-bar select:focus { border-color: #58a6ff; }
106
+
107
+ .skill-list { display: flex; flex-direction: column; gap: 0; }
108
+ .skill-row {
109
+ display: flex; align-items: center; gap: 1rem;
110
+ padding: 0.8rem 1rem; background: #161b22; border: 1px solid #30363d;
111
+ border-radius: 0; transition: background 0.15s;
112
+ }
113
+ .skill-row:first-child { border-radius: 8px 8px 0 0; }
114
+ .skill-row:last-child { border-radius: 0 0 8px 8px; }
115
+ .skill-row:only-child { border-radius: 8px; }
116
+ .skill-row + .skill-row { border-top: none; }
117
+ .skill-row:hover { background: #1c2129; }
118
+ .skill-row.disabled { opacity: 0.5; }
119
+
120
+ .skill-info { flex: 1; min-width: 0; }
121
+ .skill-name { font-size: 0.9rem; font-weight: 600; }
122
+ .skill-desc {
123
+ font-size: 0.78rem; color: #8b949e; margin-top: 0.15rem;
124
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
125
+ }
126
+ .skill-meta { display: flex; gap: 0.5rem; margin-top: 0.3rem; align-items: center; flex-wrap: wrap; }
127
+ .badge {
128
+ font-size: 0.7rem; padding: 0.1rem 0.45rem; border-radius: 10px;
129
+ font-weight: 500; line-height: 1.4;
130
+ }
131
+ .badge-source { background: #1f2937; color: #79c0ff; }
132
+ .badge-free { background: #0d2818; color: #3fb98c; }
133
+ .badge-pro { background: #2d1b00; color: #ffa657; }
134
+ .badge-fires { background: #161b22; color: #8b949e; border: 1px solid #30363d; }
135
+
136
+ /* Toggle switch */
137
+ .toggle { position: relative; width: 36px; height: 20px; flex-shrink: 0; }
138
+ .toggle input { opacity: 0; width: 0; height: 0; }
139
+ .toggle .slider {
140
+ position: absolute; inset: 0; background: #30363d; border-radius: 20px;
141
+ cursor: pointer; transition: background 0.2s;
142
+ }
143
+ .toggle .slider::before {
144
+ content: ''; position: absolute; left: 2px; top: 2px;
145
+ width: 16px; height: 16px; background: #e6edf3; border-radius: 50%;
146
+ transition: transform 0.2s;
147
+ }
148
+ .toggle input:checked + .slider { background: #3fb98c; }
149
+ .toggle input:checked + .slider::before { transform: translateX(16px); }
150
+
151
+ .skills-count { font-size: 0.82rem; color: #484f58; margin-top: 1rem; text-align: center; }
152
+
153
+ /* ── Discovery bar ── */
154
+ .discovery-bar { background: #21262d; border-radius: 4px; height: 8px; margin-top: 0.4rem; overflow: hidden; }
155
+ .discovery-fill { height: 100%; background: #2dd4bf; border-radius: 4px; transition: width 0.5s; }
156
+
157
+ /* ── Diary tab ── */
158
+ .diary-list { display: flex; flex-direction: column; gap: 1rem; }
159
+ .diary-card {
160
+ background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 1.2rem;
161
+ }
162
+ .diary-card .diary-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; }
163
+ .diary-card .diary-date { font-size: 0.82rem; color: #58a6ff; }
164
+ .diary-card .diary-title { font-size: 0.95rem; font-weight: 600; margin-bottom: 0.5rem; }
165
+ .diary-card .diary-body { font-size: 0.82rem; color: #8b949e; white-space: pre-wrap; line-height: 1.6; }
166
+ .diary-card .show-more {
167
+ background: none; border: 1px solid #30363d; color: #58a6ff; padding: 0.3rem 0.8rem;
168
+ border-radius: 4px; cursor: pointer; font-size: 0.78rem; margin-top: 0.5rem;
169
+ }
170
+ .diary-card .show-more:hover { border-color: #58a6ff; }
171
+
172
+ /* ── Projects tab ── */
173
+ .projects-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1rem; }
174
+ .project-card {
175
+ background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 1.2rem;
176
+ }
177
+ .project-card h3 { font-size: 1rem; font-weight: 600; margin-bottom: 0.5rem; }
178
+ .project-card .project-meta { font-size: 0.82rem; color: #8b949e; margin-bottom: 0.75rem; }
179
+ .project-card .project-meta span { margin-right: 1rem; }
180
+ .project-card .project-skills { font-size: 0.82rem; }
181
+ .project-card .project-skills li { padding: 0.15rem 0; color: #8b949e; list-style: none; }
182
+ .project-card .project-skills .skill-count { color: #2dd4bf; }
183
+
184
+ .pagination {
185
+ display: flex; justify-content: center; align-items: center; gap: 1rem;
186
+ margin-top: 1rem; padding: 0.5rem 0;
187
+ }
188
+ .pagination button {
189
+ background: #161b22; border: 1px solid #30363d; color: #e6edf3;
190
+ padding: 0.4rem 1rem; border-radius: 6px; cursor: pointer; font-size: 0.82rem;
191
+ }
192
+ .pagination button:hover:not(:disabled) { border-color: #58a6ff; color: #58a6ff; }
193
+ .pagination button:disabled { opacity: 0.3; cursor: default; }
194
+ .pagination .page-info { font-size: 0.82rem; color: #8b949e; }
195
+
196
+ @media (max-width: 600px) {
197
+ body { padding: 1rem; }
198
+ .grid { grid-template-columns: 1fr; }
199
+ .cards { flex-wrap: wrap; }
200
+ .card { flex: 1 1 calc(33% - 0.6rem); }
201
+ .filter-bar { flex-direction: column; }
202
+ .filter-bar input[type="text"] { min-width: 100%; }
203
+ }
204
+ </style>
205
+ </head>
206
+ <body>
207
+
208
+ <header>
209
+ <h1><span>MemStack</span>&trade; Dashboard</h1>
210
+ <p id="status">Loading...</p>
211
+ <p style="color:#484f58;font-size:0.78rem;margin-top:2px">Auto-refreshes every 30s &middot; <a href="#" id="refresh-btn" style="color:#58a6ff;text-decoration:none">Refresh Now</a></p>
212
+ </header>
213
+
214
+ <div class="tab-bar">
215
+ <button class="tab-btn active" data-tab="stats">Stats</button>
216
+ <button class="tab-btn" data-tab="skills">Skills</button>
217
+ <button class="tab-btn" data-tab="diary">Diary</button>
218
+ <button class="tab-btn" data-tab="projects">Projects</button>
219
+ </div>
220
+
221
+ <!-- ═══════ Stats Tab ═══════ -->
222
+ <div id="tab-stats" class="tab-content active">
223
+
224
+ <div class="cards">
225
+ <div class="card">
226
+ <div class="label">Total Skill Fires</div>
227
+ <div class="value accent" id="total-fires">-</div>
228
+ </div>
229
+ <div class="card">
230
+ <div class="label">Skills Discovered</div>
231
+ <div class="value" id="discovery-text" style="font-size:1.2rem">-</div>
232
+ <div class="discovery-bar"><div class="discovery-fill" id="discovery-fill" style="width:0%"></div></div>
233
+ </div>
234
+ <div class="card">
235
+ <div class="label">Sessions Tracked</div>
236
+ <div class="value" id="sessions">-</div>
237
+ </div>
238
+ <div class="card">
239
+ <div class="label">Unique Skills Used</div>
240
+ <div class="value" id="unique-skills">-</div>
241
+ </div>
242
+ <div class="card">
243
+ <div class="label">Most Active (7d)</div>
244
+ <div class="value" id="most-active" style="font-size:1rem">-</div>
245
+ </div>
246
+ </div>
247
+
248
+ <div class="grid">
249
+ <div class="panel">
250
+ <h2>Top Skills</h2>
251
+ <div id="top-skills"><span class="empty-state">No data yet</span></div>
252
+ </div>
253
+ <div class="panel">
254
+ <h2>Categories</h2>
255
+ <div id="categories"><span class="empty-state">No data yet</span></div>
256
+ </div>
257
+ </div>
258
+
259
+ <div class="grid" style="margin-top:1.5rem">
260
+ <div class="panel">
261
+ <h2>Projects Using MemStack™</h2>
262
+ <div id="projects"><span class="empty-state">No data yet</span></div>
263
+ </div>
264
+ </div>
265
+
266
+ </div><!-- /tab-stats -->
267
+
268
+ <!-- ═══════ Skills Tab ═══════ -->
269
+ <div id="tab-skills" class="tab-content">
270
+
271
+ <div class="skills-header">
272
+ <div>
273
+ <h2>Skill Catalog</h2>
274
+ <span class="project-name" id="skills-project"></span>
275
+ </div>
276
+ </div>
277
+ <p style="color: #8b949e; font-size: 0.82rem; margin: 0.25rem 0 0.75rem 0;">Toggle skills on or off to control which ones load in your project. Disabled skills won&rsquo;t fire during your sessions. Changes apply to the current project.</p>
278
+
279
+ <div class="skills-summary" id="skills-summary"></div>
280
+
281
+ <div class="filter-bar">
282
+ <input type="text" id="skill-search" placeholder="Search skills...">
283
+ <select id="filter-category"><option value="">All Categories</option></select>
284
+ <select id="filter-type">
285
+ <option value="">All Types</option>
286
+ <option value="free">Free</option>
287
+ <option value="pro">Pro</option>
288
+ </select>
289
+ <select id="filter-status">
290
+ <option value="">All Status</option>
291
+ <option value="enabled">Enabled</option>
292
+ <option value="disabled">Disabled</option>
293
+ <option value="never-used">Never Used</option>
294
+ </select>
295
+ <select id="sort-by">
296
+ <option value="az">A &rarr; Z</option>
297
+ <option value="za">Z &rarr; A</option>
298
+ <option value="most">Most Used</option>
299
+ <option value="least">Least Used</option>
300
+ </select>
301
+ </div>
302
+
303
+ <div class="skill-list" id="skill-list">
304
+ <span class="empty-state">Loading skills...</span>
305
+ </div>
306
+
307
+ <div class="pagination" id="pagination" style="display:none">
308
+ <button id="prev-page">Previous</button>
309
+ <span class="page-info" id="page-info"></span>
310
+ <button id="next-page">Next</button>
311
+ </div>
312
+
313
+ <div class="skills-count" id="skills-count"></div>
314
+
315
+ </div><!-- /tab-skills -->
316
+
317
+ <!-- ═══════ Diary Tab ═══════ -->
318
+ <div id="tab-diary" class="tab-content">
319
+ <h2 style="margin-bottom:1rem">Session Diary</h2>
320
+ <div class="diary-list" id="diary-list">
321
+ <span class="empty-state">Loading diary entries...</span>
322
+ </div>
323
+ </div><!-- /tab-diary -->
324
+
325
+ <!-- ═══════ Projects Tab ═══════ -->
326
+ <div id="tab-projects" class="tab-content">
327
+ <h2 style="margin-bottom:1rem">Projects</h2>
328
+ <div class="projects-grid" id="projects-grid">
329
+ <span class="empty-state">Loading projects...</span>
330
+ </div>
331
+ </div><!-- /tab-projects -->
332
+
333
+ <footer>Data stays local &middot; Never leaves your machine &middot; Auto-refreshes every 30s</footer>
334
+
335
+ <script>
336
+ /* ════════════════════════════════════════════
337
+ Tab switching
338
+ ════════════════════════════════════════════ */
339
+ document.querySelectorAll('.tab-btn').forEach(btn => {
340
+ btn.addEventListener('click', () => {
341
+ document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
342
+ document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
343
+ btn.classList.add('active');
344
+ document.getElementById('tab-' + btn.dataset.tab).classList.add('active');
345
+ if (btn.dataset.tab === 'skills' && !skillsLoaded) loadSkills();
346
+ if (btn.dataset.tab === 'diary' && !diaryLoaded) loadDiary();
347
+ if (btn.dataset.tab === 'projects' && !projectsLoaded) loadProjects();
348
+ });
349
+ });
350
+
351
+ /* ════════════════════════════════════════════
352
+ Stats tab (preserved from original)
353
+ ════════════════════════════════════════════ */
354
+ const CAT_COLORS = ['#3fb98c','#58a6ff','#d2a8ff','#f0883e','#f778ba','#ffa657','#79c0ff','#7ee787','#ff7b72','#d29922'];
355
+
356
+ function fmt(n) {
357
+ return n.toLocaleString();
358
+ }
359
+
360
+ function pluralFires(n) {
361
+ return fmt(n) + (n === 1 ? ' fire' : ' fires');
362
+ }
363
+
364
+ function renderBars(container, items, nameKey, valueKey) {
365
+ if (!items || items.length === 0) {
366
+ container.innerHTML = '<span class="empty-state">No data yet</span>';
367
+ return;
368
+ }
369
+ const max = Math.max(...items.map(i => i[valueKey]));
370
+ container.innerHTML = items.map((item, idx) => {
371
+ const pct = max > 0 ? (item[valueKey] / max * 100) : 0;
372
+ return `<div class="bar-row">
373
+ <span class="name">${item[nameKey]}</span>
374
+ <div class="bar-wrap"><div class="bar" style="width:${pct}%;background:${CAT_COLORS[idx % CAT_COLORS.length]}"></div></div>
375
+ <span class="count">${fmt(item[valueKey])}</span>
376
+ </div>`;
377
+ }).join('');
378
+ }
379
+
380
+ let categorySkillsCache = null;
381
+
382
+ function renderCategories(container, items) {
383
+ if (!items || items.length === 0) {
384
+ container.innerHTML = '<span class="empty-state">No data yet</span>';
385
+ return;
386
+ }
387
+ container.innerHTML = items.map((item, idx) =>
388
+ `<div>
389
+ <div class="cat-row" data-category="${item.category || 'unknown'}">
390
+ <span class="cat-chevron" id="chev-${idx}">▸</span>
391
+ <span class="cat-dot" style="background:${CAT_COLORS[idx % CAT_COLORS.length]}"></span>
392
+ <span class="cat-name">${item.category || 'unknown'}</span>
393
+ <span class="cat-count">${fmt(item.count)}</span>
394
+ </div>
395
+ <div class="cat-skills" id="cat-skills-${idx}" style="display:none"></div>
396
+ </div>`
397
+ ).join('');
398
+
399
+ container.querySelectorAll('.cat-row').forEach((row, idx) => {
400
+ row.addEventListener('click', () => toggleCategorySkills(row.dataset.category, idx));
401
+ });
402
+ }
403
+
404
+ async function toggleCategorySkills(category, idx) {
405
+ const panel = document.getElementById('cat-skills-' + idx);
406
+ const chevron = document.getElementById('chev-' + idx);
407
+ if (panel.style.display !== 'none') {
408
+ panel.style.display = 'none';
409
+ chevron.classList.remove('open');
410
+ return;
411
+ }
412
+ chevron.classList.add('open');
413
+ if (!categorySkillsCache) {
414
+ panel.innerHTML = '<span style="font-size:0.8rem;color:#8b949e">Loading…</span>';
415
+ panel.style.display = 'block';
416
+ try {
417
+ const res = await fetch('/api/category-skills');
418
+ categorySkillsCache = await res.json();
419
+ } catch { categorySkillsCache = {}; }
420
+ }
421
+ const skills = categorySkillsCache[category] || [];
422
+ panel.innerHTML = skills.length
423
+ ? skills.map(s => `<div class="cat-skill-row"><span>${s.name}</span><span class="fires">${fmt(s.fires)}</span></div>`).join('')
424
+ : '<span style="font-size:0.8rem;color:#8b949e">No skills fired</span>';
425
+ panel.style.display = 'block';
426
+ }
427
+
428
+ function renderStatsProjects(container, items) {
429
+ if (!items || items.length === 0) {
430
+ container.innerHTML = '<span class="empty-state">No data yet</span>';
431
+ return;
432
+ }
433
+ container.innerHTML = items.map(item =>
434
+ `<div class="proj-row">
435
+ <span>${item.project}</span>
436
+ <span class="fires">${pluralFires(item.count)}</span>
437
+ </div>`
438
+ ).join('');
439
+ }
440
+
441
+ let totalSkillsInIndex = 0;
442
+
443
+ async function refreshStats() {
444
+ try {
445
+ const res = await fetch('/api/stats');
446
+ const d = await res.json();
447
+
448
+ document.getElementById('total-fires').textContent = fmt(d.total_fires);
449
+ document.getElementById('unique-skills').textContent = fmt(d.unique_skills_used);
450
+ document.getElementById('sessions').textContent = fmt(d.total_sessions);
451
+ // Discovery progress bar — fetch total from /api/skills if not cached
452
+ if (!totalSkillsInIndex) {
453
+ try {
454
+ const sr = await fetch('/api/skills');
455
+ const sd = await sr.json();
456
+ totalSkillsInIndex = sd.display_total || sd.total || 0;
457
+ } catch(e) {}
458
+ }
459
+ if (totalSkillsInIndex > 0) {
460
+ const used = d.unique_skills_used || 0;
461
+ const pct = Math.min(100, Math.round((used / totalSkillsInIndex) * 100));
462
+ document.getElementById('discovery-text').textContent = used + ' of ' + totalSkillsInIndex;
463
+ document.getElementById('discovery-fill').style.width = pct + '%';
464
+ }
465
+
466
+ // Most active project this week
467
+ if (d.most_active_project) {
468
+ document.getElementById('most-active').innerHTML =
469
+ escapeHtml(d.most_active_project.name) +
470
+ '<div style="font-size:0.78rem;color:#8b949e;margin-top:0.2rem">' +
471
+ pluralFires(d.most_active_project.fires) + '</div>';
472
+ } else {
473
+ document.getElementById('most-active').textContent = 'No activity';
474
+ }
475
+
476
+ renderBars(document.getElementById('top-skills'), d.top_skills, 'name', 'count');
477
+ renderCategories(document.getElementById('categories'), d.category_breakdown);
478
+ renderStatsProjects(document.getElementById('projects'), d.project_breakdown);
479
+
480
+
481
+ const now = new Date().toLocaleTimeString();
482
+ document.getElementById('status').textContent = 'Last updated: ' + now;
483
+ } catch (e) {
484
+ document.getElementById('status').textContent = 'Failed to load data. Is the server running?';
485
+ }
486
+ }
487
+
488
+ refreshStats();
489
+ setInterval(refreshStats, 30000);
490
+ document.getElementById('refresh-btn').addEventListener('click', async (e) => {
491
+ e.preventDefault();
492
+ const btn = e.target;
493
+ btn.textContent = 'Refreshing...';
494
+ btn.style.opacity = '0.6';
495
+ btn.style.pointerEvents = 'none';
496
+ await refreshStats();
497
+ btn.textContent = 'Refresh Now';
498
+ btn.style.opacity = '1';
499
+ btn.style.pointerEvents = 'auto';
500
+ });
501
+
502
+ /* ════════════════════════════════════════════
503
+ Utilities
504
+ ════════════════════════════════════════════ */
505
+ function escapeHtml(str) {
506
+ const div = document.createElement('div');
507
+ div.textContent = str;
508
+ return div.innerHTML;
509
+ }
510
+
511
+ /* ════════════════════════════════════════════
512
+ Skills tab
513
+ ════════════════════════════════════════════ */
514
+ let allSkills = [];
515
+ let skillsLoaded = false;
516
+ let currentPage = 1;
517
+ const PAGE_SIZE = 20;
518
+ let dispFree = 83, dispPro = 29, dispTotal = 112;
519
+
520
+ async function loadSkills() {
521
+ try {
522
+ const res = await fetch('/api/skills');
523
+ const data = await res.json();
524
+
525
+ if (data.error) {
526
+ document.getElementById('skill-list').innerHTML =
527
+ `<span class="empty-state">${data.error}</span>`;
528
+ return;
529
+ }
530
+
531
+ allSkills = data.skills || [];
532
+ skillsLoaded = true;
533
+
534
+ document.getElementById('skills-project').textContent =
535
+ data.project ? `Project: ${data.project}` : '';
536
+ dispFree = data.display_free_count || data.free_count;
537
+ dispPro = data.display_pro_count || data.pro_count;
538
+ dispTotal = data.display_total || data.total;
539
+ document.getElementById('skills-summary').textContent =
540
+ `${dispTotal} skills (${dispFree} free, ${dispPro} Pro)` +
541
+ (data.disabled_count > 0 ? ` \u00b7 ${data.disabled_count} disabled` : '');
542
+
543
+ // Populate category dropdown
544
+ const categories = [...new Set(allSkills.map(s => s.category))].sort();
545
+ const catSelect = document.getElementById('filter-category');
546
+ catSelect.innerHTML = '<option value="">All Categories</option>' +
547
+ categories.map(c => `<option value="${c}">${c}</option>`).join('');
548
+
549
+ renderSkills();
550
+ } catch (e) {
551
+ document.getElementById('skill-list').innerHTML =
552
+ '<span class="empty-state">Failed to load skills. Is the server running?</span>';
553
+ }
554
+ }
555
+
556
+ function getFilteredSkills() {
557
+ const query = document.getElementById('skill-search').value.toLowerCase();
558
+ const category = document.getElementById('filter-category').value;
559
+ const type = document.getElementById('filter-type').value;
560
+ const status = document.getElementById('filter-status').value;
561
+ const sortBy = document.getElementById('sort-by').value;
562
+
563
+ let filtered = allSkills.filter(s => {
564
+ if (query && !s.name.toLowerCase().includes(query) && !s.description.toLowerCase().includes(query)) return false;
565
+ if (category && s.category !== category) return false;
566
+ if (type === 'free' && s.is_pro) return false;
567
+ if (type === 'pro' && !s.is_pro) return false;
568
+ if (status === 'enabled' && !s.enabled) return false;
569
+ if (status === 'disabled' && s.enabled) return false;
570
+ if (status === 'never-used' && s.fire_count > 0) return false;
571
+ return true;
572
+ });
573
+
574
+ filtered.sort((a, b) => {
575
+ switch (sortBy) {
576
+ case 'za': return b.name.localeCompare(a.name);
577
+ case 'most': return b.fire_count - a.fire_count;
578
+ case 'least': return a.fire_count - b.fire_count;
579
+ default: return a.name.localeCompare(b.name);
580
+ }
581
+ });
582
+
583
+ return filtered;
584
+ }
585
+
586
+ function renderSkills() {
587
+ const filtered = getFilteredSkills();
588
+ const container = document.getElementById('skill-list');
589
+ const totalFiltered = filtered.length;
590
+
591
+ if (totalFiltered === 0) {
592
+ container.innerHTML = '<span class="empty-state">No skills match your filters</span>';
593
+ document.getElementById('skills-count').textContent = '';
594
+ document.getElementById('pagination').style.display = 'none';
595
+ return;
596
+ }
597
+
598
+ const totalPages = Math.ceil(totalFiltered / PAGE_SIZE);
599
+ if (currentPage > totalPages) currentPage = totalPages;
600
+ const start = (currentPage - 1) * PAGE_SIZE;
601
+ const end = Math.min(start + PAGE_SIZE, totalFiltered);
602
+ const page = filtered.slice(start, end);
603
+
604
+ container.innerHTML = page.map(s => {
605
+ const name = escapeHtml(s.name);
606
+ const slug = escapeHtml(s.slug);
607
+ const fullDesc = escapeHtml(s.description);
608
+ const truncDesc = escapeHtml(s.description.length > 100 ? s.description.slice(0, 100) + '...' : s.description);
609
+ const needsExpand = s.description.length > 100;
610
+ const source = escapeHtml(s.source_label);
611
+ const proIcon = s.is_pro ? '\uD83D\uDD12 ' : '';
612
+ const typeBadge = s.is_pro
613
+ ? '<span class="badge badge-pro">Pro</span>'
614
+ : '<span class="badge badge-free">Free</span>';
615
+ const fireBadge = s.fire_count > 0
616
+ ? `<span class="badge badge-fires" style="color:#2dd4bf">${s.fire_count} fire${s.fire_count !== 1 ? 's' : ''}</span>`
617
+ : '<span class="badge badge-fires" style="color:#6e7681">0 fires</span>';
618
+ const expandLink = needsExpand
619
+ ? `<a href="#" class="expand-desc" data-full="${fullDesc.replace(/"/g, '&quot;')}" data-trunc="${truncDesc.replace(/"/g, '&quot;')}" style="color:#58a6ff;font-size:0.78rem;text-decoration:none;margin-left:4px">Show more</a>`
620
+ : '';
621
+
622
+ return `<div class="skill-row${s.enabled ? '' : ' disabled'}" data-slug="${slug}">
623
+ <div class="skill-info">
624
+ <div class="skill-name">${proIcon}${name}</div>
625
+ <div class="skill-desc"><span class="desc-text">${truncDesc}</span>${expandLink}</div>
626
+ <div class="skill-meta">
627
+ <span class="badge badge-source">${source}</span>
628
+ ${typeBadge}
629
+ ${fireBadge}
630
+ </div>
631
+ </div>
632
+ <label class="toggle" title="${s.enabled ? 'Disable' : 'Enable'} ${name}">
633
+ <input type="checkbox" ${s.enabled ? 'checked' : ''} data-skill="${name}" data-slug="${slug}">
634
+ <span class="slider"></span>
635
+ </label>
636
+ </div>`;
637
+ }).join('');
638
+
639
+ document.getElementById('skills-count').textContent =
640
+ `Showing ${start + 1}-${end} of ${totalFiltered} skills` +
641
+ (totalFiltered < allSkills.length ? ` (filtered from ${allSkills.length})` : '');
642
+
643
+ // Pagination controls
644
+ const pag = document.getElementById('pagination');
645
+ if (totalPages > 1) {
646
+ pag.style.display = 'flex';
647
+ document.getElementById('page-info').textContent = `Page ${currentPage} of ${totalPages}`;
648
+ document.getElementById('prev-page').disabled = (currentPage <= 1);
649
+ document.getElementById('next-page').disabled = (currentPage >= totalPages);
650
+ } else {
651
+ pag.style.display = 'none';
652
+ }
653
+
654
+ // Wire toggle handlers
655
+ container.querySelectorAll('.toggle input').forEach(cb => {
656
+ cb.addEventListener('change', (e) => toggleSkill(e.target));
657
+ });
658
+
659
+ // Wire expand/collapse handlers for skill descriptions
660
+ container.querySelectorAll('.expand-desc').forEach(link => {
661
+ link.addEventListener('click', (e) => {
662
+ e.preventDefault();
663
+ const descSpan = link.previousElementSibling;
664
+ const isExpanded = link.dataset.expanded === 'true';
665
+ if (isExpanded) {
666
+ descSpan.textContent = link.dataset.trunc;
667
+ link.textContent = 'Show more';
668
+ link.dataset.expanded = 'false';
669
+ } else {
670
+ descSpan.textContent = link.dataset.full;
671
+ link.textContent = 'Show less';
672
+ link.dataset.expanded = 'true';
673
+ }
674
+ });
675
+ });
676
+ }
677
+
678
+ async function toggleSkill(checkbox) {
679
+ const skillName = checkbox.dataset.skill;
680
+ const action = checkbox.checked ? 'enable' : 'disable';
681
+ const row = checkbox.closest('.skill-row');
682
+
683
+ // Optimistic UI
684
+ if (action === 'disable') row.classList.add('disabled');
685
+ else row.classList.remove('disabled');
686
+
687
+ try {
688
+ const res = await fetch('/api/skills/toggle', {
689
+ method: 'POST',
690
+ headers: { 'Content-Type': 'application/json' },
691
+ body: JSON.stringify({ skill: skillName, action }),
692
+ });
693
+ const data = await res.json();
694
+
695
+ if (!data.success) {
696
+ // Rollback
697
+ checkbox.checked = !checkbox.checked;
698
+ if (action === 'disable') row.classList.remove('disabled');
699
+ else row.classList.add('disabled');
700
+ alert('Toggle failed: ' + (data.error || 'Unknown error'));
701
+ return;
702
+ }
703
+
704
+ // Update local state
705
+ const s = allSkills.find(s => s.name === skillName);
706
+ if (s) s.enabled = (action === 'enable');
707
+
708
+ // Update summary
709
+ const disabledCount = allSkills.filter(s => !s.enabled).length;
710
+ document.getElementById('skills-summary').textContent =
711
+ `${dispTotal} skills (${dispFree} free, ${dispPro} Pro)` +
712
+ (disabledCount > 0 ? ` \u00b7 ${disabledCount} disabled` : '');
713
+ } catch (e) {
714
+ // Rollback on network error
715
+ checkbox.checked = !checkbox.checked;
716
+ if (action === 'disable') row.classList.remove('disabled');
717
+ else row.classList.add('disabled');
718
+ alert('Network error toggling skill');
719
+ }
720
+ }
721
+
722
+ // Wire filter controls
723
+ ['skill-search', 'filter-category', 'filter-type', 'filter-status', 'sort-by'].forEach(id => {
724
+ const el = document.getElementById(id);
725
+ el.addEventListener(id === 'skill-search' ? 'input' : 'change', () => { currentPage = 1; renderSkills(); });
726
+ });
727
+
728
+ document.getElementById('prev-page').addEventListener('click', () => {
729
+ if (currentPage > 1) { currentPage--; renderSkills(); }
730
+ });
731
+ document.getElementById('next-page').addEventListener('click', () => {
732
+ currentPage++; renderSkills();
733
+ });
734
+
735
+ /* ════════════════════════════════════════════
736
+ Diary tab
737
+ ════════════════════════════════════════════ */
738
+ let diaryLoaded = false;
739
+
740
+ async function loadDiary() {
741
+ const container = document.getElementById('diary-list');
742
+ try {
743
+ const res = await fetch('/api/diary');
744
+ const entries = await res.json();
745
+ diaryLoaded = true;
746
+
747
+ if (!entries || entries.length === 0) {
748
+ container.innerHTML = '<span class="empty-state">No diary entries yet. Say \'save diary\' in Claude Code to start logging sessions.</span>';
749
+ return;
750
+ }
751
+
752
+ container.innerHTML = entries.map((e, idx) => {
753
+ const title = escapeHtml(e.title || e.filename);
754
+ const date = escapeHtml(e.date || '');
755
+ const preview = escapeHtml(e.content.slice(0, 500));
756
+ const hasMore = e.content.length > 500;
757
+ const fullContent = hasMore ? escapeHtml(e.content) : '';
758
+ return `<div class="diary-card">
759
+ <div class="diary-header">
760
+ <span class="diary-date">${date}</span>
761
+ </div>
762
+ <div class="diary-title">${title}</div>
763
+ <div class="diary-body" id="diary-body-${idx}">${preview}${hasMore ? '...' : ''}</div>
764
+ ${hasMore ? `<button class="show-more" data-idx="${idx}" data-full="${btoa(unescape(encodeURIComponent(e.content)))}" data-expanded="false">Show more</button>` : ''}
765
+ </div>`;
766
+ }).join('');
767
+
768
+ // Wire show more buttons
769
+ container.querySelectorAll('.show-more').forEach(btn => {
770
+ btn.addEventListener('click', () => {
771
+ const idx = btn.dataset.idx;
772
+ const body = document.getElementById('diary-body-' + idx);
773
+ if (btn.dataset.expanded === 'false') {
774
+ body.textContent = decodeURIComponent(escape(atob(btn.dataset.full)));
775
+ btn.textContent = 'Show less';
776
+ btn.dataset.expanded = 'true';
777
+ } else {
778
+ const full = decodeURIComponent(escape(atob(btn.dataset.full)));
779
+ body.textContent = full.slice(0, 500) + '...';
780
+ btn.textContent = 'Show more';
781
+ btn.dataset.expanded = 'false';
782
+ }
783
+ });
784
+ });
785
+ } catch (e) {
786
+ container.innerHTML = '<span class="empty-state">Failed to load diary entries.</span>';
787
+ }
788
+ }
789
+
790
+ /* ════════════════════════════════════════════
791
+ Projects tab
792
+ ════════════════════════════════════════════ */
793
+ let projectsLoaded = false;
794
+
795
+ async function loadProjects() {
796
+ const container = document.getElementById('projects-grid');
797
+ try {
798
+ const res = await fetch('/api/projects');
799
+ const projects = await res.json();
800
+ projectsLoaded = true;
801
+
802
+ if (!projects || projects.length === 0) {
803
+ container.innerHTML = '<span class="empty-state">No project data yet. Use MemStack™ skills in your projects to start tracking.</span>';
804
+ return;
805
+ }
806
+
807
+ container.innerHTML = projects.map(p => {
808
+ const name = escapeHtml(p.name);
809
+ const lastActive = p.last_active ? escapeHtml(p.last_active.split(' ')[0] || p.last_active) : 'Unknown';
810
+ const skillsList = (p.top_skills || []).map(s =>
811
+ `<li><span class="skill-count">${s.count}</span> ${escapeHtml(s.name)}</li>`
812
+ ).join('');
813
+
814
+ return `<div class="project-card">
815
+ <h3>${name}</h3>
816
+ <div class="project-meta">
817
+ <span>${pluralFires(p.total_fires)}</span>
818
+ <span>Last active: ${lastActive}</span>
819
+ </div>
820
+ ${skillsList ? `<div class="project-skills"><strong style="font-size:0.78rem;color:#e6edf3">Top Skills</strong><ul>${skillsList}</ul></div>` : ''}
821
+ </div>`;
822
+ }).join('');
823
+ } catch (e) {
824
+ container.innerHTML = '<span class="empty-state">Failed to load projects.</span>';
825
+ }
826
+ }
827
+ </script>
828
+ </body>
829
+ </html>