@sienklogic/plan-build-run 2.31.0 → 2.32.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.
Files changed (82) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dashboard/public/css/settings.css +314 -0
  3. package/dashboard/src/components/Layout.tsx +1 -0
  4. package/dashboard/src/components/settings/LogEntryList.tsx +108 -0
  5. package/dashboard/src/components/settings/LogFileList.tsx +36 -0
  6. package/dashboard/src/components/settings/LogViewer.tsx +129 -0
  7. package/dashboard/src/components/settings/SettingsPage.tsx +1 -1
  8. package/dashboard/src/index.tsx +10 -2
  9. package/dashboard/src/routes/settings.routes.tsx +231 -0
  10. package/dashboard/src/services/config.service.d.ts +9 -0
  11. package/dashboard/src/services/log.service.d.ts +13 -0
  12. package/package.json +2 -2
  13. package/plugins/copilot-pbr/plugin.json +1 -1
  14. package/plugins/cursor-pbr/.cursor-plugin/plugin.json +1 -1
  15. package/plugins/pbr/.claude-plugin/plugin.json +1 -1
  16. package/dashboard/src/app.js +0 -91
  17. package/dashboard/src/middleware/current-phase.js +0 -25
  18. package/dashboard/src/middleware/errorHandler.js +0 -62
  19. package/dashboard/src/middleware/notFoundHandler.js +0 -9
  20. package/dashboard/src/routes/events.routes.js +0 -94
  21. package/dashboard/src/routes/index.routes.js +0 -35
  22. package/dashboard/src/routes/pages.routes.js +0 -853
  23. package/dashboard/src/views/analytics.ejs +0 -5
  24. package/dashboard/src/views/audit-detail.ejs +0 -5
  25. package/dashboard/src/views/audits.ejs +0 -5
  26. package/dashboard/src/views/config.ejs +0 -5
  27. package/dashboard/src/views/dependencies.ejs +0 -5
  28. package/dashboard/src/views/error.ejs +0 -20
  29. package/dashboard/src/views/index.ejs +0 -5
  30. package/dashboard/src/views/logs.ejs +0 -3
  31. package/dashboard/src/views/milestone-detail.ejs +0 -5
  32. package/dashboard/src/views/milestones.ejs +0 -5
  33. package/dashboard/src/views/note-detail.ejs +0 -3
  34. package/dashboard/src/views/notes.ejs +0 -5
  35. package/dashboard/src/views/partials/activity-feed.ejs +0 -27
  36. package/dashboard/src/views/partials/analytics-content.ejs +0 -241
  37. package/dashboard/src/views/partials/audit-detail-content.ejs +0 -14
  38. package/dashboard/src/views/partials/audits-content.ejs +0 -36
  39. package/dashboard/src/views/partials/breadcrumbs.ejs +0 -18
  40. package/dashboard/src/views/partials/config-content.ejs +0 -219
  41. package/dashboard/src/views/partials/dashboard-content.ejs +0 -124
  42. package/dashboard/src/views/partials/dependencies-content.ejs +0 -50
  43. package/dashboard/src/views/partials/empty-state.ejs +0 -12
  44. package/dashboard/src/views/partials/footer.ejs +0 -9
  45. package/dashboard/src/views/partials/head.ejs +0 -31
  46. package/dashboard/src/views/partials/header.ejs +0 -18
  47. package/dashboard/src/views/partials/layout-bottom.ejs +0 -8
  48. package/dashboard/src/views/partials/layout-top.ejs +0 -17
  49. package/dashboard/src/views/partials/log-entries-content.ejs +0 -17
  50. package/dashboard/src/views/partials/logs-content.ejs +0 -131
  51. package/dashboard/src/views/partials/milestone-detail-content.ejs +0 -20
  52. package/dashboard/src/views/partials/milestones-content.ejs +0 -127
  53. package/dashboard/src/views/partials/note-detail-content.ejs +0 -24
  54. package/dashboard/src/views/partials/notes-content.ejs +0 -28
  55. package/dashboard/src/views/partials/phase-content.ejs +0 -226
  56. package/dashboard/src/views/partials/phase-doc-content.ejs +0 -36
  57. package/dashboard/src/views/partials/phase-timeline.ejs +0 -27
  58. package/dashboard/src/views/partials/phases-content.ejs +0 -137
  59. package/dashboard/src/views/partials/quick-content.ejs +0 -42
  60. package/dashboard/src/views/partials/quick-detail-content.ejs +0 -30
  61. package/dashboard/src/views/partials/requirements-content.ejs +0 -44
  62. package/dashboard/src/views/partials/research-content.ejs +0 -56
  63. package/dashboard/src/views/partials/research-detail-content.ejs +0 -25
  64. package/dashboard/src/views/partials/roadmap-content.ejs +0 -197
  65. package/dashboard/src/views/partials/sidebar.ejs +0 -98
  66. package/dashboard/src/views/partials/todo-create-content.ejs +0 -59
  67. package/dashboard/src/views/partials/todo-detail-content.ejs +0 -43
  68. package/dashboard/src/views/partials/todos-content.ejs +0 -110
  69. package/dashboard/src/views/partials/todos-done-content.ejs +0 -46
  70. package/dashboard/src/views/phase-detail.ejs +0 -5
  71. package/dashboard/src/views/phase-doc.ejs +0 -5
  72. package/dashboard/src/views/phases.ejs +0 -5
  73. package/dashboard/src/views/quick-detail.ejs +0 -5
  74. package/dashboard/src/views/quick.ejs +0 -5
  75. package/dashboard/src/views/requirements.ejs +0 -3
  76. package/dashboard/src/views/research-detail.ejs +0 -3
  77. package/dashboard/src/views/research.ejs +0 -3
  78. package/dashboard/src/views/roadmap.ejs +0 -5
  79. package/dashboard/src/views/todo-create.ejs +0 -5
  80. package/dashboard/src/views/todo-detail.ejs +0 -5
  81. package/dashboard/src/views/todos-done.ejs +0 -3
  82. package/dashboard/src/views/todos.ejs +0 -5
package/CHANGELOG.md CHANGED
@@ -5,6 +5,27 @@ All notable changes to Plan-Build-Run will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.32.1](https://github.com/SienkLogic/plan-build-run/compare/plan-build-run-v2.32.0...plan-build-run-v2.32.1) (2026-02-24)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **43-02:** use tsx runtime with absolute static path for cross-cwd dashboard launch ([b19d7d5](https://github.com/SienkLogic/plan-build-run/commit/b19d7d5267632eed82760257df7f50592a71f139))
14
+
15
+ ## [2.32.0](https://github.com/SienkLogic/plan-build-run/compare/plan-build-run-v2.31.0...plan-build-run-v2.32.0) (2026-02-24)
16
+
17
+
18
+ ### Features
19
+
20
+ * **42-01:** create settings routes, CSS, wire into app, and add config.service.d.ts ([3c4f953](https://github.com/SienkLogic/plan-build-run/commit/3c4f9531de93acc4e47b4ab6a94f962972c3f6ab))
21
+ * **42-02:** add log viewer routes (page, entries, SSE tail) and CSS ([cbcb47c](https://github.com/SienkLogic/plan-build-run/commit/cbcb47ce31b2e06481e329bfd01597bcc644a4a8))
22
+ * **42-02:** add LogFileList, LogEntryList, and LogViewer components ([3101c12](https://github.com/SienkLogic/plan-build-run/commit/3101c129dbf5e6535b720188be2a49a583008303))
23
+
24
+
25
+ ### Bug Fixes
26
+
27
+ * **42-02:** move beforeunload cleanup to addEventListener for JSX compatibility ([e582a51](https://github.com/SienkLogic/plan-build-run/commit/e582a51ef628e7596bb42e352dbf8df861d67e07))
28
+
8
29
  ## [2.31.0](https://github.com/SienkLogic/plan-build-run/compare/plan-build-run-v2.30.0...plan-build-run-v2.31.0) (2026-02-24)
9
30
 
10
31
 
@@ -0,0 +1,314 @@
1
+ /* Settings page styles */
2
+
3
+ /* Tab bar */
4
+ .settings-tabs {
5
+ display: flex;
6
+ gap: var(--size-2);
7
+ border-bottom: 2px solid var(--surface-3, #e2e8f0);
8
+ margin-bottom: var(--size-5);
9
+ padding-bottom: 0;
10
+ }
11
+
12
+ /* Mode toggle (Form / Raw JSON) */
13
+ .config-mode-toggle {
14
+ display: flex;
15
+ gap: var(--size-2);
16
+ margin-bottom: var(--size-4);
17
+ }
18
+
19
+ /* Tab buttons — shared by settings-tabs and config-mode-toggle */
20
+ .tab-btn {
21
+ background: none;
22
+ border: none;
23
+ border-bottom: 2px solid transparent;
24
+ padding: var(--size-2) var(--size-4);
25
+ font-size: var(--font-size-1);
26
+ font-weight: 500;
27
+ color: var(--text-2, #64748b);
28
+ cursor: pointer;
29
+ transition: color 0.15s ease, border-color 0.15s ease;
30
+ margin-bottom: -2px;
31
+ }
32
+
33
+ .tab-btn:hover {
34
+ color: var(--text-1, #1e293b);
35
+ }
36
+
37
+ .tab-btn.active {
38
+ color: var(--link, #2563eb);
39
+ border-bottom-color: var(--link, #2563eb);
40
+ }
41
+
42
+ /* Config section card */
43
+ .config-section {
44
+ border: 1px solid var(--surface-3, #e2e8f0);
45
+ border-radius: var(--radius-2);
46
+ padding: var(--size-4);
47
+ margin-bottom: var(--size-4);
48
+ background: var(--surface-1, #f8fafc);
49
+ }
50
+
51
+ .config-section--nested {
52
+ margin-top: var(--size-4);
53
+ background: var(--surface-2, #f1f5f9);
54
+ }
55
+
56
+ /* Section heading */
57
+ .config-section__title {
58
+ font-size: var(--font-size-1);
59
+ font-weight: 600;
60
+ color: var(--text-1, #1e293b);
61
+ margin: 0 0 var(--size-3) 0;
62
+ text-transform: uppercase;
63
+ letter-spacing: 0.05em;
64
+ }
65
+
66
+ /* Individual field row */
67
+ .config-field {
68
+ display: flex;
69
+ align-items: center;
70
+ gap: var(--size-3);
71
+ padding: var(--size-1) 0;
72
+ font-size: var(--font-size-1);
73
+ color: var(--text-1, #1e293b);
74
+ cursor: pointer;
75
+ }
76
+
77
+ .config-field--check {
78
+ flex-direction: row;
79
+ }
80
+
81
+ .config-field__label {
82
+ min-width: 12rem;
83
+ color: var(--text-2, #64748b);
84
+ font-size: var(--font-size-0);
85
+ }
86
+
87
+ .config-field input[type='text'],
88
+ .config-field input[type='number'],
89
+ .config-field select {
90
+ flex: 1;
91
+ padding: var(--size-1) var(--size-2);
92
+ border: 1px solid var(--surface-3, #e2e8f0);
93
+ border-radius: var(--radius-1);
94
+ font-size: var(--font-size-0);
95
+ background: var(--surface-1, #fff);
96
+ color: var(--text-1, #1e293b);
97
+ }
98
+
99
+ .config-field input[readonly] {
100
+ opacity: 0.6;
101
+ cursor: not-allowed;
102
+ }
103
+
104
+ .config-field input[type='checkbox'] {
105
+ width: 1rem;
106
+ height: 1rem;
107
+ accent-color: var(--link, #2563eb);
108
+ cursor: pointer;
109
+ }
110
+
111
+ /* Raw JSON textarea */
112
+ .config-raw-json {
113
+ width: 100%;
114
+ font-family: var(--font-mono, 'JetBrains Mono', monospace);
115
+ font-size: var(--font-size-0);
116
+ padding: var(--size-3);
117
+ border: 1px solid var(--surface-3, #e2e8f0);
118
+ border-radius: var(--radius-2);
119
+ background: var(--surface-1, #f8fafc);
120
+ color: var(--text-1, #1e293b);
121
+ resize: vertical;
122
+ box-sizing: border-box;
123
+ }
124
+
125
+ /* Config actions row */
126
+ .config-actions {
127
+ display: flex;
128
+ justify-content: flex-end;
129
+ margin-bottom: var(--size-3);
130
+ }
131
+
132
+ /* Inline feedback */
133
+ .config-feedback {
134
+ min-height: 1.5rem;
135
+ margin-top: var(--size-2);
136
+ font-size: var(--font-size-0);
137
+ }
138
+
139
+ .feedback--success {
140
+ color: var(--green-7, #15803d);
141
+ font-weight: 500;
142
+ }
143
+
144
+ .feedback--error {
145
+ color: var(--red-7, #b91c1c);
146
+ font-weight: 500;
147
+ }
148
+
149
+ /* Empty state */
150
+ .config-empty {
151
+ font-size: var(--font-size-0);
152
+ color: var(--text-2, #64748b);
153
+ font-style: italic;
154
+ margin: 0;
155
+ }
156
+
157
+ /* Primary button (consistent with other pages) */
158
+ .btn--primary {
159
+ display: inline-flex;
160
+ align-items: center;
161
+ gap: var(--size-2);
162
+ padding: var(--size-2) var(--size-5);
163
+ background: var(--link, #2563eb);
164
+ color: #fff;
165
+ border: none;
166
+ border-radius: var(--radius-2);
167
+ font-size: var(--font-size-1);
168
+ font-weight: 500;
169
+ cursor: pointer;
170
+ transition: background 0.15s ease;
171
+ }
172
+
173
+ .btn--primary:hover {
174
+ background: var(--blue-7, #1d4ed8);
175
+ }
176
+
177
+ /* Dark theme overrides */
178
+ [data-theme='dark'] .config-section {
179
+ background: var(--surface-2, #1e2535);
180
+ border-color: var(--surface-3, #2d3748);
181
+ }
182
+
183
+ [data-theme='dark'] .config-section--nested {
184
+ background: var(--surface-1, #161e2d);
185
+ }
186
+
187
+ [data-theme='dark'] .config-raw-json,
188
+ [data-theme='dark'] .config-field input[type='text'],
189
+ [data-theme='dark'] .config-field input[type='number'],
190
+ [data-theme='dark'] .config-field select {
191
+ background: var(--surface-2, #1e2535);
192
+ border-color: var(--surface-3, #2d3748);
193
+ color: var(--text-1, #e2e8f0);
194
+ }
195
+
196
+ /* ── Log Viewer ─────────────────────────────────────── */
197
+ .log-viewer-layout {
198
+ display: grid;
199
+ grid-template-columns: 220px 1fr;
200
+ gap: var(--size-4);
201
+ height: calc(100vh - var(--size-12));
202
+ }
203
+
204
+ .log-file-sidebar {
205
+ border-right: 1px solid var(--surface-3);
206
+ padding-right: var(--size-3);
207
+ overflow-y: auto;
208
+ }
209
+
210
+ .log-file-list {
211
+ list-style: none;
212
+ padding: 0;
213
+ margin: 0;
214
+ }
215
+
216
+ .log-file-item {
217
+ display: block;
218
+ padding: var(--size-2) var(--size-3);
219
+ border-radius: var(--radius-2);
220
+ text-decoration: none;
221
+ color: var(--text-2);
222
+ font-size: var(--font-size-0);
223
+ margin-block: 2px;
224
+ }
225
+
226
+ .log-file-item:hover { background: var(--surface-2); }
227
+ .log-file-item--active { background: var(--surface-3); color: var(--text-1); font-weight: 500; }
228
+
229
+ .log-file-name { display: block; font-family: var(--font-mono); font-size: 0.75rem; }
230
+ .log-file-size { color: var(--text-2); }
231
+ .log-file-date { color: var(--text-2); }
232
+
233
+ .log-main { overflow-y: auto; padding-right: var(--size-2); }
234
+
235
+ .log-filters {
236
+ display: flex;
237
+ gap: var(--size-3);
238
+ align-items: center;
239
+ margin-bottom: var(--size-3);
240
+ padding: var(--size-2) var(--size-3);
241
+ background: var(--surface-2);
242
+ border-radius: var(--radius-2);
243
+ }
244
+
245
+ .log-table {
246
+ width: 100%;
247
+ border-collapse: collapse;
248
+ font-size: var(--font-size-0);
249
+ }
250
+
251
+ .log-table th,
252
+ .log-table td {
253
+ text-align: left;
254
+ padding: var(--size-1) var(--size-2);
255
+ border-bottom: 1px solid var(--surface-3);
256
+ }
257
+
258
+ .log-table th { color: var(--text-2); font-weight: 500; }
259
+
260
+ .log-badge {
261
+ display: inline-block;
262
+ padding: 1px 6px;
263
+ border-radius: var(--radius-round);
264
+ font-size: 0.7rem;
265
+ font-weight: 600;
266
+ text-transform: uppercase;
267
+ background: var(--surface-3);
268
+ color: var(--text-2);
269
+ }
270
+
271
+ .log-badge--error { background: var(--red-2); color: var(--red-9); }
272
+ .log-badge--warn { background: var(--yellow-2); color: var(--yellow-9); }
273
+ .log-badge--info { background: var(--blue-2); color: var(--blue-9); }
274
+ .log-badge--hook { background: var(--violet-2); color: var(--violet-9); }
275
+ .log-badge--task { background: var(--green-2); color: var(--green-9); }
276
+ .log-badge--agent { background: var(--cyan-2); color: var(--cyan-9); }
277
+
278
+ .log-pagination {
279
+ display: flex;
280
+ gap: var(--size-2);
281
+ align-items: center;
282
+ padding: var(--size-2) 0;
283
+ }
284
+
285
+ .log-tail-indicator {
286
+ display: flex;
287
+ align-items: center;
288
+ gap: var(--size-2);
289
+ font-size: var(--font-size-0);
290
+ color: var(--text-2);
291
+ margin-bottom: var(--size-2);
292
+ }
293
+
294
+ .tail-dot {
295
+ width: 8px;
296
+ height: 8px;
297
+ border-radius: 50%;
298
+ background: var(--surface-3);
299
+ }
300
+
301
+ .tail-dot--active {
302
+ background: var(--green-6);
303
+ animation: pulse 1.5s ease-in-out infinite;
304
+ }
305
+
306
+ @keyframes pulse {
307
+ 0%, 100% { opacity: 1; }
308
+ 50% { opacity: 0.4; }
309
+ }
310
+
311
+ .log-tail-entries { font-family: var(--font-mono); font-size: 0.75rem; color: var(--text-2); }
312
+ .log-tail-row { padding: 2px 0; border-bottom: 1px solid var(--surface-2); }
313
+ .log-empty, .log-prompt { color: var(--text-2); padding: var(--size-4); }
314
+ .log-summary { color: var(--text-2); font-size: var(--font-size-0); margin-bottom: var(--size-2); }
@@ -37,6 +37,7 @@ export function Layout({ title, children, currentView }: LayoutProps) {
37
37
  <link rel="stylesheet" href="/css/command-center.css" />
38
38
  <link rel="stylesheet" href="/css/explorer.css" />
39
39
  <link rel="stylesheet" href="/css/timeline.css" />
40
+ <link rel="stylesheet" href="/css/settings.css" />
40
41
 
41
42
  {/* Prevent flash of wrong theme */}
42
43
  {html`<script>
@@ -0,0 +1,108 @@
1
+ interface LogEntryListProps {
2
+ entries: object[];
3
+ total: number;
4
+ page: number;
5
+ pageSize: number;
6
+ file: string;
7
+ typeFilter: string;
8
+ q: string;
9
+ }
10
+
11
+ function sanitizeType(t: unknown): string {
12
+ if (typeof t !== 'string' || !t) return 'unknown';
13
+ return t.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase();
14
+ }
15
+
16
+ function getTimestamp(entry: Record<string, unknown>): string {
17
+ const raw = entry['timestamp'] ?? entry['ts'] ?? entry['time'];
18
+ if (!raw) return '—';
19
+ try {
20
+ return new Date(raw as string).toLocaleString();
21
+ } catch {
22
+ return String(raw);
23
+ }
24
+ }
25
+
26
+ function getSummary(entry: Record<string, unknown>): string {
27
+ const { type: _type, timestamp: _ts, ts: _ts2, time: _time, ...rest } = entry;
28
+ const str = JSON.stringify(rest);
29
+ return str.length > 120 ? str.slice(0, 117) + '...' : str;
30
+ }
31
+
32
+ export function LogEntryRow({ entry }: { entry: object }) {
33
+ const e = entry as Record<string, unknown>;
34
+ const type = typeof e['type'] === 'string' ? e['type'] : '';
35
+ const badge = sanitizeType(type);
36
+ const timestamp = getTimestamp(e);
37
+ const summary = getSummary(e);
38
+
39
+ return (
40
+ <tr>
41
+ <td>
42
+ <span class={`log-badge log-badge--${badge}`}>{type || '—'}</span>
43
+ </td>
44
+ <td>{timestamp}</td>
45
+ <td>{summary}</td>
46
+ </tr>
47
+ );
48
+ }
49
+
50
+ export function LogEntryList({ entries, total, page, pageSize, file, typeFilter, q }: LogEntryListProps) {
51
+ const start = (page - 1) * pageSize + 1;
52
+ const end = Math.min(page * pageSize, total);
53
+ const encodedFile = encodeURIComponent(file);
54
+ const encodedQ = encodeURIComponent(q);
55
+ const encodedType = encodeURIComponent(typeFilter);
56
+
57
+ const baseQuery = `file=${encodedFile}&typeFilter=${encodedType}&q=${encodedQ}`;
58
+
59
+ return (
60
+ <div id="log-entries">
61
+ <p class="log-summary">
62
+ {total === 0
63
+ ? 'No entries found.'
64
+ : `Showing ${start}–${end} of ${total} entries`}
65
+ </p>
66
+
67
+ {entries.length > 0 && (
68
+ <table class="log-table">
69
+ <thead>
70
+ <tr>
71
+ <th>Type</th>
72
+ <th>Timestamp</th>
73
+ <th>Message / Data</th>
74
+ </tr>
75
+ </thead>
76
+ <tbody>
77
+ {entries.map((entry, i) => (
78
+ <LogEntryRow key={i} entry={entry} />
79
+ ))}
80
+ </tbody>
81
+ </table>
82
+ )}
83
+
84
+ <div class="log-pagination">
85
+ <button
86
+ hx-get={`/api/settings/logs/entries?${baseQuery}&page=${page - 1}`}
87
+ hx-target="#log-entries"
88
+ hx-swap="outerHTML"
89
+ disabled={page <= 1}
90
+ >
91
+ Prev
92
+ </button>
93
+ <span>{page}</span>
94
+ <button
95
+ hx-get={`/api/settings/logs/entries?${baseQuery}&page=${page + 1}`}
96
+ hx-target="#log-entries"
97
+ hx-swap="outerHTML"
98
+ disabled={end >= total}
99
+ >
100
+ Next
101
+ </button>
102
+ </div>
103
+ </div>
104
+ );
105
+ }
106
+
107
+ // Alias used by route for HTMX partial responses
108
+ export const LogEntriesFragment = LogEntryList;
@@ -0,0 +1,36 @@
1
+ interface LogFileListProps {
2
+ files: Array<{ name: string; size: number; modified: string }>;
3
+ selectedFile?: string;
4
+ }
5
+
6
+ function formatBytes(n: number): string {
7
+ if (n >= 1024) return `${(n / 1024).toFixed(1)} KB`;
8
+ return `${n} B`;
9
+ }
10
+
11
+ export function LogFileList({ files, selectedFile }: LogFileListProps) {
12
+ if (files.length === 0) {
13
+ return <p class="log-empty">No log files found in .planning/logs/</p>;
14
+ }
15
+
16
+ return (
17
+ <ul class="log-file-list">
18
+ {files.map((file) => {
19
+ const isSelected = file.name === selectedFile;
20
+ const date = new Date(file.modified).toLocaleDateString();
21
+ return (
22
+ <li key={file.name}>
23
+ <a
24
+ href={`/settings/logs?file=${encodeURIComponent(file.name)}`}
25
+ class={`log-file-item${isSelected ? ' log-file-item--active' : ''}`}
26
+ >
27
+ <span class="log-file-name">{file.name}</span>
28
+ <span class="log-file-size">{formatBytes(file.size)}</span>
29
+ <span class="log-file-date">{date}</span>
30
+ </a>
31
+ </li>
32
+ );
33
+ })}
34
+ </ul>
35
+ );
36
+ }
@@ -0,0 +1,129 @@
1
+ import { html } from 'hono/html';
2
+ import { LogFileList } from './LogFileList';
3
+ import { LogEntryList } from './LogEntryList';
4
+
5
+ interface LogViewerProps {
6
+ files: Array<{ name: string; size: number; modified: string }>;
7
+ selectedFile?: string;
8
+ entries?: object[];
9
+ total?: number;
10
+ page?: number;
11
+ pageSize?: number;
12
+ typeFilter?: string;
13
+ q?: string;
14
+ isLatest?: boolean;
15
+ }
16
+
17
+ export function LogViewer({
18
+ files,
19
+ selectedFile,
20
+ entries = [],
21
+ total = 0,
22
+ page = 1,
23
+ pageSize = 50,
24
+ typeFilter = '',
25
+ q = '',
26
+ isLatest = false,
27
+ }: LogViewerProps) {
28
+ return (
29
+ <div class="log-viewer-layout">
30
+ <aside class="log-file-sidebar">
31
+ <h2 class="config-section__title">Log Files</h2>
32
+ <LogFileList files={files} selectedFile={selectedFile} />
33
+ </aside>
34
+
35
+ <section class="log-main">
36
+ {!selectedFile ? (
37
+ <p class="log-prompt">Select a log file to view entries.</p>
38
+ ) : (
39
+ <>
40
+ <form
41
+ class="log-filters"
42
+ hx-get="/api/settings/logs/entries"
43
+ hx-target="#log-entries"
44
+ hx-swap="outerHTML"
45
+ hx-trigger="change, input delay:300ms from:[name=q]"
46
+ >
47
+ <input type="hidden" name="file" value={selectedFile} />
48
+ <input type="hidden" name="page" value="1" />
49
+
50
+ <select name="typeFilter">
51
+ <option value="" selected={typeFilter === ''}>All types</option>
52
+ <option value="hook" selected={typeFilter === 'hook'}>hook</option>
53
+ <option value="task" selected={typeFilter === 'task'}>task</option>
54
+ <option value="agent" selected={typeFilter === 'agent'}>agent</option>
55
+ <option value="error" selected={typeFilter === 'error'}>error</option>
56
+ <option value="info" selected={typeFilter === 'info'}>info</option>
57
+ <option value="warn" selected={typeFilter === 'warn'}>warn</option>
58
+ <option value="debug" selected={typeFilter === 'debug'}>debug</option>
59
+ </select>
60
+
61
+ <input
62
+ type="text"
63
+ name="q"
64
+ placeholder="Search entries..."
65
+ value={q || ''}
66
+ />
67
+ </form>
68
+
69
+ <div id="log-entries-container">
70
+ <LogEntryList
71
+ entries={entries}
72
+ total={total}
73
+ page={page}
74
+ pageSize={pageSize}
75
+ file={selectedFile}
76
+ typeFilter={typeFilter}
77
+ q={q}
78
+ />
79
+ </div>
80
+
81
+ {isLatest && (
82
+ <>
83
+ <div
84
+ class="log-tail-indicator"
85
+ x-data={`logTail({ file: '${selectedFile}' })`}
86
+ x-init="start()"
87
+ >
88
+ <span class="tail-dot" x-bind:class="{ 'tail-dot--active': connected }"></span>
89
+ <span x-text="connected ? 'Live' : 'Connecting...'"></span>
90
+ <span class="tail-count" x-text="newCount + ' new entries'"></span>
91
+ </div>
92
+ <div id="log-tail-entries" class="log-tail-entries"></div>
93
+ </>
94
+ )}
95
+ </>
96
+ )}
97
+ </section>
98
+
99
+ {html`<script>
100
+ function logTail({ file }) {
101
+ return {
102
+ connected: false,
103
+ newCount: 0,
104
+ es: null,
105
+ start() {
106
+ this.es = new EventSource('/api/settings/logs/tail?file=' + encodeURIComponent(file));
107
+ this.es.addEventListener('log-entry', (e) => {
108
+ this.newCount++;
109
+ const container = document.getElementById('log-tail-entries');
110
+ if (!container) return;
111
+ const row = document.createElement('div');
112
+ row.className = 'log-tail-row';
113
+ try {
114
+ const entry = JSON.parse(e.data);
115
+ row.textContent = JSON.stringify(entry);
116
+ } catch { row.textContent = e.data; }
117
+ container.prepend(row);
118
+ });
119
+ this.es.onopen = () => { this.connected = true; };
120
+ this.es.onerror = () => { this.connected = false; };
121
+ window.addEventListener('beforeunload', () => this.stop());
122
+ },
123
+ stop() { if (this.es) this.es.close(); }
124
+ };
125
+ }
126
+ </script>`}
127
+ </div>
128
+ );
129
+ }
@@ -1,7 +1,7 @@
1
1
  import { ConfigEditor } from './ConfigEditor';
2
2
 
3
3
  interface SettingsPageProps {
4
- config: object;
4
+ config: Record<string, unknown>;
5
5
  activeTab?: 'config' | 'logs';
6
6
  }
7
7
 
@@ -1,6 +1,8 @@
1
1
  import { serve } from '@hono/node-server';
2
2
  import { serveStatic } from '@hono/node-server/serve-static';
3
3
  import { Hono } from 'hono';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { dirname, join } from 'node:path';
4
6
  import { compress } from 'hono/compress';
5
7
  import { logger } from 'hono/logger';
6
8
  import { secureHeaders } from 'hono/secure-headers';
@@ -9,6 +11,7 @@ import { indexRouter } from './routes/index.routes';
9
11
  import { commandCenterRouter } from './routes/command-center.routes';
10
12
  import { explorerRouter } from './routes/explorer.routes';
11
13
  import { timelineRouter } from './routes/timeline.routes';
14
+ import { settingsRouter } from './routes/settings.routes';
12
15
  import { sseHandler } from './sse-handler';
13
16
  import { startWatcher } from './watcher-setup';
14
17
  import { currentPhaseMiddleware } from './middleware/current-phase';
@@ -32,6 +35,10 @@ type Env = {
32
35
  };
33
36
  };
34
37
 
38
+ const __filename = fileURLToPath(import.meta.url);
39
+ const __dirname = dirname(__filename);
40
+ const publicDir = join(__dirname, '..', 'public');
41
+
35
42
  function createApp(config: ServerConfig) {
36
43
  const app = new Hono<Env>();
37
44
 
@@ -61,8 +68,8 @@ function createApp(config: ServerConfig) {
61
68
  c.header('Vary', 'Accept');
62
69
  });
63
70
 
64
- // Static file serving from public/
65
- app.use('*', serveStatic({ root: './public' }));
71
+ // Static file serving from public/ (absolute path for cross-cwd compatibility)
72
+ app.use('*', serveStatic({ root: publicDir }));
66
73
 
67
74
  // Current phase middleware — populates c.var.currentPhase for all routes
68
75
  app.use('*', currentPhaseMiddleware);
@@ -72,6 +79,7 @@ function createApp(config: ServerConfig) {
72
79
  app.route('/api/command-center', commandCenterRouter);
73
80
  app.route('/', explorerRouter);
74
81
  app.route('/', timelineRouter);
82
+ app.route('/', settingsRouter);
75
83
 
76
84
  // SSE endpoint — real streamSSE handler with multi-client broadcast
77
85
  app.get('/api/events/stream', sseHandler);