project-mgr 0.2.1 → 0.3.1

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/src/web/ui.ts DELETED
@@ -1,977 +0,0 @@
1
- export function getWebUIHTML(): string {
2
- return `<!DOCTYPE html>
3
- <html lang="en">
4
- <head>
5
- <meta charset="UTF-8" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>Project Manager - Projects & Activity Feed</title>
8
- <style>
9
- :root {
10
- --bg: #0d1117;
11
- --card-bg: #161b22;
12
- --border: #30363d;
13
- --text: #c9d1d9;
14
- --text-muted: #8b949e;
15
- --heading: #f0f6fc;
16
- --accent: #58a6ff;
17
- --accent-hover: #79c0ff;
18
- --success: #3fb950;
19
- --warning: #d29922;
20
- --danger: #f85149;
21
- --purple: #bc8cff;
22
- --cyan: #39c5cf;
23
- --radius: 8px;
24
- }
25
-
26
- * {
27
- box-sizing: border-box;
28
- margin: 0;
29
- padding: 0;
30
- }
31
-
32
- body {
33
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
34
- background-color: var(--bg);
35
- color: var(--text);
36
- line-height: 1.5;
37
- padding: 0;
38
- margin: 0;
39
- min-height: 100vh;
40
- }
41
-
42
- header {
43
- background-color: var(--card-bg);
44
- border-bottom: 1px solid var(--border);
45
- position: sticky;
46
- top: 0;
47
- z-index: 100;
48
- padding: 1rem 1.5rem;
49
- }
50
-
51
- .header-container {
52
- max-width: 1200px;
53
- margin: 0 auto;
54
- display: flex;
55
- flex-wrap: wrap;
56
- align-items: center;
57
- justify-content: space-between;
58
- gap: 1rem;
59
- }
60
-
61
- .brand {
62
- display: flex;
63
- align-items: center;
64
- gap: 0.75rem;
65
- }
66
-
67
- .logo-icon {
68
- width: 32px;
69
- height: 32px;
70
- background: linear-gradient(135deg, var(--accent), var(--purple));
71
- border-radius: 8px;
72
- display: flex;
73
- align-items: center;
74
- justify-content: center;
75
- color: #fff;
76
- font-weight: bold;
77
- font-size: 1.1rem;
78
- }
79
-
80
- h1 {
81
- font-size: 1.25rem;
82
- font-weight: 600;
83
- color: var(--heading);
84
- }
85
-
86
- .stats-bar {
87
- display: flex;
88
- gap: 0.75rem;
89
- flex-wrap: wrap;
90
- }
91
-
92
- .stat-pill {
93
- background: rgba(255, 255, 255, 0.05);
94
- border: 1px solid var(--border);
95
- border-radius: 20px;
96
- padding: 0.25rem 0.75rem;
97
- font-size: 0.85rem;
98
- display: flex;
99
- align-items: center;
100
- gap: 0.35rem;
101
- }
102
-
103
- .stat-val {
104
- font-weight: 600;
105
- color: var(--heading);
106
- }
107
-
108
- .controls {
109
- display: flex;
110
- align-items: center;
111
- gap: 0.75rem;
112
- flex-wrap: wrap;
113
- width: 100%;
114
- max-width: 1200px;
115
- margin: 1rem auto 0 auto;
116
- padding: 0 1.5rem;
117
- }
118
-
119
- .search-box {
120
- flex: 1;
121
- min-width: 240px;
122
- position: relative;
123
- }
124
-
125
- .search-input {
126
- width: 100%;
127
- background: var(--card-bg);
128
- border: 1px solid var(--border);
129
- color: var(--text);
130
- padding: 0.5rem 0.75rem 0.5rem 2.25rem;
131
- border-radius: var(--radius);
132
- font-size: 0.9rem;
133
- outline: none;
134
- transition: border-color 0.2s;
135
- }
136
-
137
- .search-input:focus {
138
- border-color: var(--accent);
139
- }
140
-
141
- .search-icon {
142
- position: absolute;
143
- left: 0.75rem;
144
- top: 50%;
145
- transform: translateY(-50%);
146
- color: var(--text-muted);
147
- pointer-events: none;
148
- font-size: 0.85rem;
149
- }
150
-
151
- .nav-tabs {
152
- display: flex;
153
- gap: 0.5rem;
154
- background: var(--card-bg);
155
- padding: 0.25rem;
156
- border-radius: var(--radius);
157
- border: 1px solid var(--border);
158
- }
159
-
160
- .tab-btn {
161
- background: transparent;
162
- border: none;
163
- color: var(--text-muted);
164
- padding: 0.4rem 1rem;
165
- font-size: 0.875rem;
166
- font-weight: 500;
167
- border-radius: 6px;
168
- cursor: pointer;
169
- transition: all 0.2s;
170
- }
171
-
172
- .tab-btn:hover {
173
- color: var(--heading);
174
- }
175
-
176
- .tab-btn.active {
177
- background: var(--accent);
178
- color: #ffffff;
179
- }
180
-
181
- .btn {
182
- background: var(--card-bg);
183
- border: 1px solid var(--border);
184
- color: var(--text);
185
- padding: 0.45rem 0.85rem;
186
- border-radius: var(--radius);
187
- font-size: 0.875rem;
188
- cursor: pointer;
189
- display: inline-flex;
190
- align-items: center;
191
- gap: 0.35rem;
192
- transition: all 0.2s;
193
- }
194
-
195
- .btn:hover {
196
- border-color: var(--text-muted);
197
- color: var(--heading);
198
- }
199
-
200
- .btn-sm {
201
- padding: 0.3rem 0.6rem;
202
- font-size: 0.8rem;
203
- }
204
-
205
- .btn-primary {
206
- background: var(--accent);
207
- color: #ffffff;
208
- border-color: var(--accent);
209
- margin-top: 0.75rem;
210
- }
211
-
212
- .btn-primary:hover {
213
- background: var(--accent-hover);
214
- color: #ffffff;
215
- }
216
-
217
- main {
218
- max-width: 1200px;
219
- margin: 1.5rem auto;
220
- padding: 0 1.5rem;
221
- }
222
-
223
- .view-panel {
224
- display: none;
225
- }
226
-
227
- .view-panel.active {
228
- display: block;
229
- }
230
-
231
- /* Activity Feed Toolbar & Grouping Toggle */
232
- .activity-toolbar {
233
- display: flex;
234
- align-items: center;
235
- justify-content: space-between;
236
- gap: 1rem;
237
- background: var(--card-bg);
238
- border: 1px solid var(--border);
239
- border-radius: var(--radius);
240
- padding: 0.75rem 1rem;
241
- margin-bottom: 1.25rem;
242
- flex-wrap: wrap;
243
- }
244
-
245
- .date-picker-wrapper {
246
- display: flex;
247
- align-items: center;
248
- gap: 0.6rem;
249
- font-size: 0.9rem;
250
- color: var(--heading);
251
- font-weight: 500;
252
- flex-wrap: wrap;
253
- }
254
-
255
- .date-input {
256
- background: var(--bg);
257
- border: 1px solid var(--border);
258
- color: var(--heading);
259
- padding: 0.35rem 0.65rem;
260
- border-radius: 6px;
261
- font-size: 0.875rem;
262
- outline: none;
263
- font-family: inherit;
264
- }
265
-
266
- .date-input:focus {
267
- border-color: var(--accent);
268
- }
269
-
270
- .view-mode-toggle {
271
- display: flex;
272
- gap: 0.25rem;
273
- background: var(--bg);
274
- padding: 0.2rem;
275
- border-radius: 6px;
276
- border: 1px solid var(--border);
277
- }
278
-
279
- .mode-btn {
280
- background: transparent;
281
- border: none;
282
- color: var(--text-muted);
283
- padding: 0.3rem 0.75rem;
284
- font-size: 0.8rem;
285
- font-weight: 500;
286
- border-radius: 4px;
287
- cursor: pointer;
288
- transition: all 0.2s;
289
- }
290
-
291
- .mode-btn:hover {
292
- color: var(--heading);
293
- }
294
-
295
- .mode-btn.active {
296
- background: var(--accent);
297
- color: #ffffff;
298
- font-weight: 600;
299
- }
300
-
301
- /* Project Grouping Container */
302
- .project-group-card {
303
- background: var(--card-bg);
304
- border: 1px solid var(--border);
305
- border-radius: var(--radius);
306
- padding: 1.25rem;
307
- margin-bottom: 1.25rem;
308
- }
309
-
310
- .project-group-header {
311
- display: flex;
312
- align-items: center;
313
- justify-content: space-between;
314
- border-bottom: 1px solid var(--border);
315
- padding-bottom: 0.75rem;
316
- margin-bottom: 1rem;
317
- }
318
-
319
- .project-group-title {
320
- display: flex;
321
- align-items: center;
322
- gap: 0.6rem;
323
- font-size: 1.1rem;
324
- font-weight: 600;
325
- color: var(--heading);
326
- }
327
-
328
- .project-group-count {
329
- font-size: 0.8rem;
330
- color: var(--text-muted);
331
- background: rgba(255, 255, 255, 0.06);
332
- padding: 0.15rem 0.55rem;
333
- border-radius: 12px;
334
- }
335
-
336
- /* Projects Grid */
337
- .projects-grid {
338
- display: grid;
339
- grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
340
- gap: 1.25rem;
341
- }
342
-
343
- .project-card {
344
- background: var(--card-bg);
345
- border: 1px solid var(--border);
346
- border-radius: var(--radius);
347
- padding: 1.25rem;
348
- display: flex;
349
- flex-direction: column;
350
- justify-content: space-between;
351
- transition: transform 0.15s, border-color 0.15s;
352
- }
353
-
354
- .project-card:hover {
355
- border-color: var(--text-muted);
356
- transform: translateY(-2px);
357
- }
358
-
359
- .card-header {
360
- display: flex;
361
- align-items: flex-start;
362
- justify-content: space-between;
363
- gap: 0.75rem;
364
- margin-bottom: 0.75rem;
365
- }
366
-
367
- .project-title {
368
- font-size: 1.1rem;
369
- font-weight: 600;
370
- color: var(--heading);
371
- }
372
-
373
- .project-slug {
374
- font-size: 0.8rem;
375
- color: var(--text-muted);
376
- font-family: monospace;
377
- }
378
-
379
- .status-badge {
380
- font-size: 0.75rem;
381
- font-weight: 600;
382
- text-transform: uppercase;
383
- padding: 0.2rem 0.55rem;
384
- border-radius: 12px;
385
- display: inline-block;
386
- white-space: nowrap;
387
- }
388
-
389
- .status-active { background: rgba(63, 185, 80, 0.15); color: var(--success); border: 1px solid rgba(63, 185, 80, 0.3); }
390
- .status-planned { background: rgba(88, 166, 255, 0.15); color: var(--accent); border: 1px solid rgba(88, 166, 255, 0.3); }
391
- .status-idea { background: rgba(188, 140, 255, 0.15); color: var(--purple); border: 1px solid rgba(188, 140, 255, 0.3); }
392
- .status-blocked { background: rgba(248, 81, 73, 0.15); color: var(--danger); border: 1px solid rgba(248, 81, 73, 0.3); }
393
- .status-paused { background: rgba(210, 153, 34, 0.15); color: var(--warning); border: 1px solid rgba(210, 153, 34, 0.3); }
394
- .status-completed { background: rgba(57, 197, 207, 0.15); color: var(--cyan); border: 1px solid rgba(57, 197, 207, 0.3); }
395
- .status-archived { background: rgba(139, 148, 158, 0.15); color: var(--text-muted); border: 1px solid rgba(139, 148, 158, 0.3); }
396
-
397
- .card-body {
398
- margin-bottom: 1rem;
399
- font-size: 0.9rem;
400
- }
401
-
402
- .card-description {
403
- color: var(--text);
404
- margin-bottom: 0.5rem;
405
- }
406
-
407
- .card-notes {
408
- color: var(--text-muted);
409
- font-size: 0.85rem;
410
- font-style: italic;
411
- border-left: 2px solid var(--border);
412
- padding-left: 0.5rem;
413
- margin-top: 0.5rem;
414
- }
415
-
416
- .latest-summary {
417
- background: rgba(0, 0, 0, 0.2);
418
- padding: 0.6rem;
419
- border-radius: 6px;
420
- margin-top: 0.75rem;
421
- font-size: 0.85rem;
422
- }
423
-
424
- .latest-summary-title {
425
- font-size: 0.75rem;
426
- color: var(--text-muted);
427
- text-transform: uppercase;
428
- margin-bottom: 0.25rem;
429
- }
430
-
431
- .card-footer {
432
- display: flex;
433
- align-items: center;
434
- justify-content: space-between;
435
- font-size: 0.85rem;
436
- color: var(--text-muted);
437
- border-top: 1px solid var(--border);
438
- padding-top: 0.75rem;
439
- margin-top: auto;
440
- }
441
-
442
- .link-btn {
443
- color: var(--accent);
444
- text-decoration: none;
445
- cursor: pointer;
446
- font-weight: 500;
447
- }
448
-
449
- .link-btn:hover {
450
- text-decoration: underline;
451
- }
452
-
453
- /* Timeline View */
454
- .timeline {
455
- display: flex;
456
- flex-direction: column;
457
- gap: 1rem;
458
- position: relative;
459
- }
460
-
461
- .timeline-item {
462
- background: var(--card-bg);
463
- border: 1px solid var(--border);
464
- border-radius: var(--radius);
465
- padding: 1.25rem;
466
- position: relative;
467
- }
468
-
469
- .timeline-header {
470
- display: flex;
471
- flex-wrap: wrap;
472
- align-items: center;
473
- justify-content: space-between;
474
- gap: 0.5rem;
475
- margin-bottom: 0.75rem;
476
- }
477
-
478
- .timeline-meta {
479
- display: flex;
480
- align-items: center;
481
- gap: 0.5rem;
482
- flex-wrap: wrap;
483
- }
484
-
485
- .project-tag {
486
- background: rgba(88, 166, 255, 0.1);
487
- color: var(--accent);
488
- border: 1px solid rgba(88, 166, 255, 0.2);
489
- padding: 0.15rem 0.5rem;
490
- border-radius: 4px;
491
- font-family: monospace;
492
- font-size: 0.85rem;
493
- cursor: pointer;
494
- }
495
-
496
- .time-stamp {
497
- font-size: 0.8rem;
498
- color: var(--text-muted);
499
- }
500
-
501
- .source-badge {
502
- font-size: 0.75rem;
503
- color: var(--text-muted);
504
- background: rgba(255, 255, 255, 0.05);
505
- padding: 0.1rem 0.4rem;
506
- border-radius: 4px;
507
- }
508
-
509
- .entry-summary {
510
- font-size: 1rem;
511
- font-weight: 500;
512
- color: var(--heading);
513
- margin-bottom: 0.5rem;
514
- }
515
-
516
- .entry-progress {
517
- font-size: 0.9rem;
518
- color: var(--text);
519
- margin-bottom: 0.75rem;
520
- }
521
-
522
- .section-box {
523
- margin-top: 0.5rem;
524
- font-size: 0.875rem;
525
- }
526
-
527
- .section-title {
528
- font-weight: 600;
529
- font-size: 0.8rem;
530
- text-transform: uppercase;
531
- margin-bottom: 0.25rem;
532
- display: flex;
533
- align-items: center;
534
- gap: 0.25rem;
535
- }
536
-
537
- .section-title.blockers { color: var(--danger); }
538
- .section-title.next-steps { color: var(--accent); }
539
- .section-title.decisions { color: var(--purple); }
540
-
541
- .bullet-list {
542
- list-style: disc inside;
543
- color: var(--text);
544
- padding-left: 0.25rem;
545
- }
546
-
547
- .bullet-list li {
548
- margin-bottom: 0.2rem;
549
- }
550
-
551
- .tags-container {
552
- display: flex;
553
- flex-wrap: wrap;
554
- gap: 0.35rem;
555
- margin-top: 0.75rem;
556
- }
557
-
558
- .pill-tag {
559
- background: rgba(255, 255, 255, 0.06);
560
- border: 1px solid var(--border);
561
- color: var(--text-muted);
562
- font-size: 0.75rem;
563
- padding: 0.1rem 0.45rem;
564
- border-radius: 12px;
565
- }
566
-
567
- .empty-state {
568
- text-align: center;
569
- padding: 3rem 1rem;
570
- color: var(--text-muted);
571
- }
572
-
573
- .empty-state h3 {
574
- color: var(--heading);
575
- margin-bottom: 0.5rem;
576
- }
577
-
578
- .filter-bar {
579
- display: flex;
580
- align-items: center;
581
- gap: 0.5rem;
582
- margin-bottom: 1rem;
583
- font-size: 0.875rem;
584
- color: var(--text-muted);
585
- }
586
-
587
- .filter-chip {
588
- background: rgba(88, 166, 255, 0.15);
589
- color: var(--accent);
590
- padding: 0.2rem 0.6rem;
591
- border-radius: 12px;
592
- display: inline-flex;
593
- align-items: center;
594
- gap: 0.3rem;
595
- }
596
-
597
- .filter-chip button {
598
- background: none;
599
- border: none;
600
- color: var(--accent);
601
- cursor: pointer;
602
- font-weight: bold;
603
- }
604
- </style>
605
- </head>
606
- <body>
607
- <header>
608
- <div class="header-container">
609
- <div class="brand">
610
- <div class="logo-icon">PM</div>
611
- <div>
612
- <h1>Project Manager</h1>
613
- </div>
614
- </div>
615
- <div class="stats-bar" id="statsBar">
616
- <div class="stat-pill">Projects: <span class="stat-val" id="statProjects">-</span></div>
617
- <div class="stat-pill">Active: <span class="stat-val" id="statActive">-</span></div>
618
- <div class="stat-pill">Updates: <span class="stat-val" id="statUpdates">-</span></div>
619
- </div>
620
- </div>
621
- </header>
622
-
623
- <div class="controls">
624
- <div class="search-box">
625
- <span class="search-icon">🔍</span>
626
- <input type="text" id="searchInput" class="search-input" placeholder="Search projects, status, or activity..." />
627
- </div>
628
- <div class="nav-tabs">
629
- <button class="tab-btn active" id="tabProjects" onclick="switchTab('projects')">Projects</button>
630
- <button class="tab-btn" id="tabActivity" onclick="switchTab('activity')">Activity Feed</button>
631
- </div>
632
- <button class="btn" onclick="fetchData()">🔄 Refresh</button>
633
- </div>
634
-
635
- <main>
636
- <div id="filterBarContainer" class="filter-bar" style="display: none;">
637
- Filtering by project: <span id="activeFilterChip" class="filter-chip"></span>
638
- </div>
639
-
640
- <!-- Projects View -->
641
- <div id="viewProjects" class="view-panel active">
642
- <div id="projectsContainer" class="projects-grid"></div>
643
- </div>
644
-
645
- <!-- Activity Feed View -->
646
- <div id="viewActivity" class="view-panel">
647
- <div class="activity-toolbar">
648
- <div class="date-picker-wrapper">
649
- <label for="activityDateInput">📅 Date:</label>
650
- <input type="date" id="activityDateInput" class="date-input" onchange="render()" />
651
- <button class="btn btn-sm" onclick="setActivityDateToday()">Today</button>
652
- <button class="btn btn-sm" onclick="clearActivityDate()">Show All Dates</button>
653
- </div>
654
- <div class="view-mode-toggle">
655
- <button class="mode-btn active" id="modeGrouped" onclick="setActivityGroupMode('grouped')">📁 Group by Project</button>
656
- <button class="mode-btn" id="modeFlat" onclick="setActivityGroupMode('flat')">📜 Flat List</button>
657
- </div>
658
- </div>
659
- <div id="timelineContainer" class="timeline"></div>
660
- </div>
661
- </main>
662
-
663
- <script>
664
- // Reserved dictionary object for future i18n support
665
- const i18n = {
666
- en: {
667
- projects: "Projects",
668
- activityFeed: "Activity Feed",
669
- refresh: "Refresh",
670
- dateLabel: "Date:",
671
- today: "Today",
672
- showAllDates: "Show All Dates",
673
- groupByProject: "Group by Project",
674
- flatList: "Flat List",
675
- noProjectsTitle: "No projects found",
676
- noProjectsDesc: "No projects match your search or filter criteria.",
677
- noActivityTitle: "No activity entries",
678
- noActivityDesc: "No status updates match the selected date or search filter.",
679
- }
680
- };
681
- const currentLang = "en";
682
- function t(key) {
683
- return (i18n[currentLang] && i18n[currentLang][key]) || key;
684
- }
685
-
686
- let projectsData = [];
687
- let historyData = [];
688
- let activeProjectFilter = null;
689
- let activeGroupMode = 'grouped';
690
-
691
- function getTodayDateString() {
692
- const now = new Date();
693
- const year = now.getFullYear();
694
- const month = String(now.getMonth() + 1).padStart(2, '0');
695
- const day = String(now.getDate()).padStart(2, '0');
696
- return \`\${year}-\${month}-\${day}\`;
697
- }
698
-
699
- function getLocalDateString(isoStr) {
700
- if (!isoStr) return '';
701
- const d = new Date(isoStr);
702
- if (isNaN(d.getTime())) return '';
703
- const year = d.getFullYear();
704
- const month = String(d.getMonth() + 1).padStart(2, '0');
705
- const day = String(d.getDate()).padStart(2, '0');
706
- return \`\${year}-\${month}-\${day}\`;
707
- }
708
-
709
- // Set default date input value to Today on page load
710
- document.getElementById('activityDateInput').value = getTodayDateString();
711
-
712
- async function fetchData() {
713
- try {
714
- const [projRes, histRes, statsRes] = await Promise.all([
715
- fetch('/api/projects'),
716
- fetch('/api/history?limit=1000'),
717
- fetch('/api/stats')
718
- ]);
719
-
720
- if (projRes.ok) projectsData = await projRes.json();
721
- if (histRes.ok) historyData = await histRes.json();
722
- if (statsRes.ok) {
723
- const stats = await statsRes.json();
724
- document.getElementById('statProjects').textContent = stats.totalProjects;
725
- document.getElementById('statActive').textContent = stats.statusCounts.active || 0;
726
- document.getElementById('statUpdates').textContent = stats.totalEntries;
727
- }
728
-
729
- render();
730
- } catch (err) {
731
- console.error('Failed to load data', err);
732
- }
733
- }
734
-
735
- function switchTab(tab) {
736
- document.getElementById('tabProjects').classList.toggle('active', tab === 'projects');
737
- document.getElementById('tabActivity').classList.toggle('active', tab === 'activity');
738
- document.getElementById('viewProjects').classList.toggle('active', tab === 'projects');
739
- document.getElementById('viewActivity').classList.toggle('active', tab === 'activity');
740
- }
741
-
742
- function setActivityDateToday() {
743
- document.getElementById('activityDateInput').value = getTodayDateString();
744
- render();
745
- }
746
-
747
- function clearActivityDate() {
748
- document.getElementById('activityDateInput').value = '';
749
- render();
750
- }
751
-
752
- function setActivityGroupMode(mode) {
753
- activeGroupMode = mode;
754
- document.getElementById('modeGrouped').classList.toggle('active', mode === 'grouped');
755
- document.getElementById('modeFlat').classList.toggle('active', mode === 'flat');
756
- render();
757
- }
758
-
759
- function filterByProject(slug) {
760
- activeProjectFilter = slug;
761
- switchTab('activity');
762
- render();
763
- }
764
-
765
- function clearProjectFilter() {
766
- activeProjectFilter = null;
767
- render();
768
- }
769
-
770
- document.getElementById('searchInput').addEventListener('input', render);
771
-
772
- function formatTime(isoStr) {
773
- if (!isoStr) return '';
774
- try {
775
- const d = new Date(isoStr);
776
- return d.toLocaleString();
777
- } catch {
778
- return isoStr;
779
- }
780
- }
781
-
782
- function renderTimelineItem(item) {
783
- return \`
784
- <div class="timeline-item">
785
- <div class="timeline-header">
786
- <div class="timeline-meta">
787
- <span class="project-tag" data-action="filter-project" data-project="\${escapeHtml(item.project)}">\${escapeHtml(item.project)}</span>
788
- <span class="status-badge status-\${item.status}">\${item.status}</span>
789
- <span class="source-badge">source: \${item.source}</span>
790
- </div>
791
- <div class="time-stamp">\${formatTime(item.occurredAt)}</div>
792
- </div>
793
- <div class="entry-summary">\${escapeHtml(item.summary)}</div>
794
- \${item.progress ? \`<div class="entry-progress"><strong>Progress:</strong> \${escapeHtml(item.progress)}</div>\` : ''}
795
-
796
- \${item.blockers && item.blockers.length ? \`
797
- <div class="section-box">
798
- <div class="section-title blockers">⛔ Blockers</div>
799
- <ul class="bullet-list">
800
- \${item.blockers.map(b => \`<li>\${escapeHtml(b)}</li>\`).join('')}
801
- </ul>
802
- </div>
803
- \` : ''}
804
-
805
- \${item.nextSteps && item.nextSteps.length ? \`
806
- <div class="section-box">
807
- <div class="section-title next-steps">🎯 Next Steps</div>
808
- <ul class="bullet-list">
809
- \${item.nextSteps.map(n => \`<li>\${escapeHtml(n)}</li>\`).join('')}
810
- </ul>
811
- </div>
812
- \` : ''}
813
-
814
- \${item.decisions && item.decisions.length ? \`
815
- <div class="section-box">
816
- <div class="section-title decisions">💡 Decisions</div>
817
- <ul class="bullet-list">
818
- \${item.decisions.map(d => \`<li>\${escapeHtml(d)}</li>\`).join('')}
819
- </ul>
820
- </div>
821
- \` : ''}
822
-
823
- \${item.tags && item.tags.length ? \`
824
- <div class="tags-container">
825
- \${item.tags.map(t => \`<span class="pill-tag">#\${escapeHtml(t)}</span>\`).join('')}
826
- </div>
827
- \` : ''}
828
- </div>
829
- \`;
830
- }
831
-
832
- document.addEventListener('click', function(e) {
833
- const filterTarget = e.target.closest('[data-action="filter-project"]');
834
- if (filterTarget && filterTarget.dataset.project) {
835
- filterByProject(filterTarget.dataset.project);
836
- }
837
- if (e.target && e.target.id === 'btnClearFilter') {
838
- clearProjectFilter();
839
- }
840
- });
841
-
842
- function render() {
843
- const query = document.getElementById('searchInput').value.trim().toLowerCase();
844
- const selectedDate = document.getElementById('activityDateInput').value;
845
-
846
- // Render Active Filter Chip
847
- const filterBar = document.getElementById('filterBarContainer');
848
- const chip = document.getElementById('activeFilterChip');
849
- if (activeProjectFilter) {
850
- filterBar.style.display = 'flex';
851
- chip.innerHTML = \`\${escapeHtml(activeProjectFilter)} <button id="btnClearFilter">✕</button>\`;
852
- } else {
853
- filterBar.style.display = 'none';
854
- }
855
-
856
- // Render Projects
857
- const projContainer = document.getElementById('projectsContainer');
858
- const filteredProjects = projectsData.filter(p => {
859
- if (activeProjectFilter && p.project !== activeProjectFilter) return false;
860
- if (!query) return true;
861
- return p.project.toLowerCase().includes(query) ||
862
- p.displayName.toLowerCase().includes(query) ||
863
- (p.description && p.description.toLowerCase().includes(query)) ||
864
- (p.notes && p.notes.toLowerCase().includes(query)) ||
865
- (p.latestStatus && p.latestStatus.summary.toLowerCase().includes(query));
866
- });
867
-
868
- if (filteredProjects.length === 0) {
869
- projContainer.innerHTML = \`
870
- <div class="empty-state" style="grid-column: 1 / -1;">
871
- <h3>\${t('noProjectsTitle')}</h3>
872
- <p>\${t('noProjectsDesc')}</p>
873
- </div>\`;
874
- } else {
875
- projContainer.innerHTML = filteredProjects.map(p => {
876
- const status = p.latestStatus ? p.latestStatus.status : 'none';
877
- return \`
878
- <div class="project-card">
879
- <div>
880
- <div class="card-header">
881
- <div>
882
- <div class="project-title">\${escapeHtml(p.displayName)}</div>
883
- <div class="project-slug">\${escapeHtml(p.project)}</div>
884
- </div>
885
- <span class="status-badge status-\${status}">\${status}</span>
886
- </div>
887
- <div class="card-body">
888
- \${p.description ? \`<div class="card-description">\${escapeHtml(p.description)}</div>\` : ''}
889
- \${p.notes ? \`<div class="card-notes">\${escapeHtml(p.notes)}</div>\` : ''}
890
- \${p.latestStatus ? \`
891
- <div class="latest-summary">
892
- <div class="latest-summary-title">Latest Update (\${formatTime(p.latestStatus.occurredAt)})</div>
893
- <div>\${escapeHtml(p.latestStatus.summary)}</div>
894
- </div>
895
- \` : ''}
896
- </div>
897
- </div>
898
- <div class="card-footer">
899
- <span>Created: \${formatTime(p.createdAt)}</span>
900
- <a class="link-btn" data-action="filter-project" data-project="\${escapeHtml(p.project)}">View Activity →</a>
901
- </div>
902
- </div>
903
- \`;
904
- }).join('');
905
- }
906
-
907
- // Render Timeline Feed
908
- const timelineContainer = document.getElementById('timelineContainer');
909
- const filteredHistory = historyData.filter(h => {
910
- if (activeProjectFilter && h.project !== activeProjectFilter) return false;
911
- if (selectedDate && getLocalDateString(h.occurredAt) !== selectedDate) return false;
912
- if (!query) return true;
913
- return h.project.toLowerCase().includes(query) ||
914
- h.status.toLowerCase().includes(query) ||
915
- h.summary.toLowerCase().includes(query) ||
916
- (h.progress && h.progress.toLowerCase().includes(query)) ||
917
- h.tags.some(t => t.toLowerCase().includes(query));
918
- });
919
-
920
- if (filteredHistory.length === 0) {
921
- timelineContainer.innerHTML = \`
922
- <div class="empty-state">
923
- <h3>\${t('noActivityTitle')} \${selectedDate ? 'on ' + selectedDate : ''}</h3>
924
- <p>\${t('noActivityDesc')}</p>
925
- \${selectedDate ? \`<button class="btn btn-primary" onclick="clearActivityDate()">\${t('showAllDates')}</button>\` : ''}
926
- </div>\`;
927
- } else {
928
- if (activeGroupMode === 'grouped') {
929
- // Group by Project
930
- const groups = {};
931
- filteredHistory.forEach(item => {
932
- if (!groups[item.project]) groups[item.project] = [];
933
- groups[item.project].push(item);
934
- });
935
-
936
- timelineContainer.innerHTML = Object.keys(groups).map(projSlug => {
937
- const projMeta = projectsData.find(p => p.project === projSlug);
938
- const titleName = projMeta ? projMeta.displayName : projSlug;
939
- const entries = groups[projSlug];
940
-
941
- return \`
942
- <div class="project-group-card">
943
- <div class="project-group-header">
944
- <div class="project-group-title">
945
- 📁 <span>\${escapeHtml(titleName)}</span>
946
- <span class="project-slug">(\${escapeHtml(projSlug)})</span>
947
- </div>
948
- <span class="project-group-count">\${entries.length} update\${entries.length > 1 ? 's' : ''}</span>
949
- </div>
950
- <div class="timeline">
951
- \${entries.map(renderTimelineItem).join('')}
952
- </div>
953
- </div>
954
- \`;
955
- }).join('');
956
- } else {
957
- // Flat Timeline
958
- timelineContainer.innerHTML = filteredHistory.map(renderTimelineItem).join('');
959
- }
960
- }
961
- }
962
-
963
- function escapeHtml(str) {
964
- if (!str) return '';
965
- return String(str)
966
- .replace(/&/g, '&amp;')
967
- .replace(/</g, '&lt;')
968
- .replace(/>/g, '&gt;')
969
- .replace(/"/g, '&quot;')
970
- .replace(/'/g, '&#039;');
971
- }
972
-
973
- fetchData();
974
- </script>
975
- </body>
976
- </html>`;
977
- }