project-mgr 0.2.1 → 0.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/dist/web/ui.js CHANGED
@@ -1,606 +1,17 @@
1
- export function getWebUIHTML() {
1
+ import { DEFAULT_LOCALE, getMessages, resolveLocale, } from "./i18n.js";
2
+ import { getWebUIScript } from "./script.js";
3
+ import { WEB_UI_STYLES } from "./styles.js";
4
+ export function getWebUIHTML(defaultLang = DEFAULT_LOCALE) {
5
+ const initialLang = resolveLocale(defaultLang);
6
+ const dict = getMessages(initialLang);
2
7
  return `<!DOCTYPE html>
3
- <html lang="en">
8
+ <html lang="${dict.htmlLang}">
4
9
  <head>
5
10
  <meta charset="UTF-8" />
6
11
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>Project Manager - Projects & Activity Feed</title>
12
+ <title>${dict.pageTitle}</title>
8
13
  <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
- }
14
+ ${WEB_UI_STYLES}
604
15
  </style>
605
16
  </head>
606
17
  <body>
@@ -609,13 +20,19 @@ export function getWebUIHTML() {
609
20
  <div class="brand">
610
21
  <div class="logo-icon">PM</div>
611
22
  <div>
612
- <h1>Project Manager</h1>
23
+ <h1 id="appTitle" data-i18n="appTitle">${dict.appTitle}</h1>
613
24
  </div>
614
25
  </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>
26
+ <div class="header-right">
27
+ <div class="stats-bar" id="statsBar">
28
+ <div class="stat-pill"><span id="labelStatProjects" data-i18n="statProjectsLabel">${dict.statProjectsLabel}</span> <span class="stat-val" id="statProjects">-</span></div>
29
+ <div class="stat-pill"><span id="labelStatActive" data-i18n="statActiveLabel">${dict.statActiveLabel}</span> <span class="stat-val" id="statActive">-</span></div>
30
+ <div class="stat-pill"><span id="labelStatUpdates" data-i18n="statUpdatesLabel">${dict.statUpdatesLabel}</span> <span class="stat-val" id="statUpdates">-</span></div>
31
+ </div>
32
+ <div class="lang-switch" id="langSwitch" role="group" aria-label="${dict.langSwitchAriaLabel}">
33
+ <button type="button" class="lang-btn ${initialLang === "zh" ? "active" : ""}" id="btnLangZh" onclick="setLang('zh')">中文</button>
34
+ <button type="button" class="lang-btn ${initialLang === "en" ? "active" : ""}" id="btnLangEn" onclick="setLang('en')">English</button>
35
+ </div>
619
36
  </div>
620
37
  </div>
621
38
  </header>
@@ -623,18 +40,18 @@ export function getWebUIHTML() {
623
40
  <div class="controls">
624
41
  <div class="search-box">
625
42
  <span class="search-icon">🔍</span>
626
- <input type="text" id="searchInput" class="search-input" placeholder="Search projects, status, or activity..." />
43
+ <input type="text" id="searchInput" class="search-input" data-i18n-placeholder="searchPlaceholder" placeholder="${dict.searchPlaceholder}" />
627
44
  </div>
628
45
  <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>
46
+ <button class="tab-btn active" id="tabProjects" data-i18n="projects" onclick="switchTab('projects')">${dict.projects}</button>
47
+ <button class="tab-btn" id="tabActivity" data-i18n="activityFeed" onclick="switchTab('activity')">${dict.activityFeed}</button>
631
48
  </div>
632
- <button class="btn" onclick="fetchData()">🔄 Refresh</button>
49
+ <button class="btn" id="btnRefresh" data-i18n="refresh" onclick="refreshData()">${dict.refresh}</button>
633
50
  </div>
634
51
 
635
52
  <main>
636
53
  <div id="filterBarContainer" class="filter-bar" style="display: none;">
637
- Filtering by project: <span id="activeFilterChip" class="filter-chip"></span>
54
+ <span id="labelFilterByProject" data-i18n="filteringByProject">${dict.filteringByProject}</span> <span id="activeFilterChip" class="filter-chip"></span>
638
55
  </div>
639
56
 
640
57
  <!-- Projects View -->
@@ -646,331 +63,23 @@ export function getWebUIHTML() {
646
63
  <div id="viewActivity" class="view-panel">
647
64
  <div class="activity-toolbar">
648
65
  <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>
66
+ <label for="activityDateInput" id="labelActivityDate" data-i18n="dateLabel">${dict.dateLabel}</label>
67
+ <input type="date" id="activityDateInput" class="date-input" onchange="changeActivityDate()" />
68
+ <button class="btn btn-sm" id="btnDateToday" data-i18n="today" onclick="setActivityDateToday()">${dict.today}</button>
69
+ <button class="btn btn-sm" id="btnShowAllDates" data-i18n="showAllDates" onclick="clearActivityDate()">${dict.showAllDates}</button>
653
70
  </div>
654
71
  <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>
72
+ <button class="mode-btn active" id="modeGrouped" data-i18n="groupByProject" onclick="setActivityGroupMode('grouped')">${dict.groupByProject}</button>
73
+ <button class="mode-btn" id="modeFlat" data-i18n="flatList" onclick="setActivityGroupMode('flat')">${dict.flatList}</button>
657
74
  </div>
658
75
  </div>
76
+ <button id="btnBackToActivities" class="btn" type="button" data-i18n="backToActivities" onclick="returnToActivityList()" hidden>${dict.backToActivities}</button>
659
77
  <div id="timelineContainer" class="timeline"></div>
660
78
  </div>
661
79
  </main>
662
80
 
663
81
  <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();
82
+ ${getWebUIScript(initialLang)}
974
83
  </script>
975
84
  </body>
976
85
  </html>`;