Commit150528ebRecorded29 Mar 2026Repositoryvigil

Replace inline JS with sigil-web-ui fragment streaming

Message

Migrate Vigil from JSON SSE + client-side JS rendering to sigil-web-ui's HTML fragment streaming pattern. The server now sends pre-rendered HTML fragments via sse-morph events, and the client uses sigil-web-ui's thin JS library for DOM updates.

Key changes: - views.sgl: Remove ~110 lines of inline JavaScript, extract reusable component renderers (service card, status badge, log line, etc.), add CSS-based log filtering, scrollable check history, data-sg-* declarative attributes for SSE auto-connect and auto-scroll - web.sgl: Switch SSE events from JSON to sse-morph with pre-rendered HTML, add sigil-web-ui JS route, add log filter endpoint returning SSE batch responses, extract shared entry-morph-events helper - logs.sgl: Broadcast raw log entries instead of pre-formatted SSE; the SSE handler transforms them into sse-morph events - main.sgl: Pass state to notify-status-change for entry lookup

Changed
 src/vigil/logs.sgl  |  10 +++---
 src/vigil/main.sgl  |   2 +-
 src/vigil/views.sgl | 319 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------------------------------------------------------------------------------
 src/vigil/web.sgl   | 119 ++++++++++++++++++++++++++++++++++++++++++++++------------------
 4 files changed, 224 insertions(+), 226 deletions(-)
Diff
src/vigil/logs.sglmodified
@@ -2,7 +2,9 @@
2
;;;
3
;;; Reads structured log output (JSON lines or plain text) from
4
;;; configured file sources using tail -f. Stores entries in a
5
;;; per-service ring buffer and broadcasts new entries via SSE.
+5
;;; per-service ring buffer and broadcasts new entries.
+6
;;; The SSE handler in (vigil web) transforms entries into
+7
;;; sse-morph events with pre-rendered HTML fragments.
8
9
(define-library (vigil logs)
10
(import (sigil core)
@@ -14,7 +16,6 @@
16
(sigil async)
17
(sigil time)
18
(sigil channels)
17
(sigil http response)
19
(vigil state))
20
21
(export ;; Log entry
@@ -108,9 +109,8 @@
109
(bc (svc-broadcast svc)))
110
(set-car! box new-entries)
111
(set-car! cnt-box new-count)
111
(broadcast-send bc
112
(sse-event "log"
113
(json-encode (log-entry->dict entry))))))))
+112
;; Broadcast the raw entry; the SSE handler transforms it
+113
(broadcast-send bc entry)))))
114
115
;; ============================================================
116
;; File Tailer
src/vigil/main.sglmodified
@@ -91,7 +91,7 @@
91
(current-error-port))
92
(go (check-loop svc st interval
93
(lambda (name result old-status)
94
(notify-status-change bc name result)
+94
(notify-status-change bc st name result)
95
(alerter-send! alerter name result old-status)))))
96
services)
97
;; Start log tailers
src/vigil/views.sglmodified
@@ -2,6 +2,11 @@
2
;;;
3
;;; Server-rendered SXML page templates for Vigil's web interface.
4
;;; Uses sigil-web-styles for all styling via theme-stylesheet.
+5
;;; Uses sigil-web-ui for live updates via SSE fragment streaming.
+6
;;;
+7
;;; Component renderers (render-service-card, render-log-line, etc.)
+8
;;; are reused for both initial page render AND SSE live updates.
+9
;;;
10
;;; Note: Uses explicit list construction instead of unquote-splicing
11
;;; due to compiled module limitation (see sigil-compiled-module-gotchas).
12
@@ -12,13 +17,21 @@
17
(sigil sxml)
18
(sigil time)
19
(sigil web styles)
+20
(sigil web ui)
21
(vigil state)
22
(vigil logs))
23
24
(export render-dashboard-page
25
render-service-page
26
render-not-found-page
21
vigil-css)
+27
vigil-css
+28
;; Component renderers for SSE reuse
+29
render-service-card
+30
render-log-line
+31
render-history-row
+32
render-status-badge
+33
render-status-details
+34
render-filter-toolbar)
35
36
(begin
37
@@ -39,10 +52,12 @@
52
.log-filter-warn.active { background: var(--color-warning); border-color: var(--color-warning); color: #000; }
53
.log-filter-info.active { background: var(--color-success); border-color: var(--color-success); color: #000; }
54
.log-filter-debug.active { background: var(--color-text-muted); border-color: var(--color-text-muted); color: #000; }
42
.log-scroll-toggle { margin-left: auto; }
55
")
56
57
(define vigil-css-3 "
+58
.check-history-scroll { max-height: 20rem; overflow-y: auto; border: 1px solid var(--color-border); border-radius: 6px; }
+59
.check-history-scroll .table { margin: 0; }
+60
.check-history-scroll thead { position: sticky; top: 0; background: var(--color-bg, #0d0d1a); z-index: 1; }
61
#log-viewer { overflow: hidden; }
62
.log-container { background: var(--color-bg-inset, #1a1a2e); border: 1px solid var(--color-border); border-radius: 6px; height: 500px; overflow-y: auto; font-family: monospace; font-size: 0.8125rem; line-height: 1.5; }
63
.log-line { padding: 0.125rem 0.75rem; border-bottom: 1px solid var(--color-border); white-space: pre-wrap; word-break: break-all; }
@@ -55,10 +70,20 @@
70
.log-time { color: var(--color-text-muted); margin-right: 0.5rem; font-size: 0.75rem; }
71
.log-msg { color: var(--color-text); }
72
.log-empty { padding: 2rem; text-align: center; color: var(--color-text-muted); }
+73
")
+74
+75
;; CSS-based log filtering: when a filter class is on the container,
+76
;; hide non-matching log lines via CSS (no JavaScript needed)
+77
(define vigil-css-4 "
+78
.log-container.filter-error .log-line:not(.log-line-error) { display: none; }
+79
.log-container.filter-warn .log-line:not(.log-line-warn) { display: none; }
+80
.log-container.filter-info .log-line:not(.log-line-info) { display: none; }
+81
.log-container.filter-debug .log-line:not(.log-line-debug) { display: none; }
+82
.sg-sse-connector { display: none; }
83
")
84
85
(define vigil-supplemental-css
61
(string-append vigil-css-1 vigil-css-2 vigil-css-3))
+86
(string-append vigil-css-1 vigil-css-2 vigil-css-3 vigil-css-4))
87
88
;; Generate the full stylesheet from the design system + app-specific styles
89
(define vigil-css
@@ -68,29 +93,33 @@
93
;; Layout
94
;; ============================================================
95
71
(define (render-page title body-sxml)
72
(sxml->html
73
(list 'html
74
(list 'head
75
'(meta (@ (charset "utf-8")))
76
'(meta (@ (name "viewport") (content "width=device-width, initial-scale=1")))
77
(list 'title (string-append title " - Vigil"))
78
'(link (@ (rel "stylesheet") (href "/style.css"))))
79
(append
80
(list 'body
81
'(header (@ (class "header"))
82
(a (@ (href "/") (class "header-logo")) "vigil")
83
(nav (@ (class "header-nav"))
84
(a (@ (href "/")) "Dashboard"))))
85
(list
86
(cons 'main (cons '(@ (class "main")) body-sxml))
87
'(footer (@ (class "footer"))
88
"Crafted with "
89
(a (@ (href "https://usesigil.org")) "Sigil"))
90
(sse-script))))))
91
92
(define (sse-script)
93
(list 'script sse-js))
+96
;; sse-urls: list of SSE endpoint URLs to auto-connect
+97
(define (render-page title body-sxml sse-urls)
+98
(let ((head-scripts (sigil-web-ui-head)))
+99
(sxml->html
+100
(list 'html
+101
(append
+102
(list 'head
+103
'(meta (@ (charset "utf-8")))
+104
'(meta (@ (name "viewport") (content "width=device-width, initial-scale=1")))
+105
(list 'title (string-append title " - Vigil"))
+106
'(link (@ (rel "stylesheet") (href "/style.css"))))
+107
head-scripts)
+108
(append
+109
(list 'body
+110
'(header (@ (class "header"))
+111
(a (@ (href "/") (class "header-logo")) "vigil")
+112
(nav (@ (class "header-nav"))
+113
(a (@ (href "/")) "Dashboard"))))
+114
(map (lambda (url)
+115
(list 'div (list '@ (list 'class "sg-sse-connector")
+116
(list 'data-sg-sse url))))
+117
sse-urls)
+118
(list
+119
(cons 'main (cons '(@ (class "main")) body-sxml))
+120
'(footer (@ (class "footer"))
+121
"Crafted with "
+122
(a (@ (href "https://usesigil.org")) "Sigil"))))))))
123
124
;; ============================================================
125
;; Dashboard Page
@@ -105,7 +134,8 @@
134
(list 'h1 '(@ (class "page-title")) "Services")
135
(cons 'div
136
(cons '(@ (class "service-grid"))
108
(map render-service-card entries)))))))
+137
(map render-service-card entries)))))
+138
(list "/sse/status")))
139
140
(define (render-service-card entry)
141
(let* ((name (service-entry-name entry))
@@ -144,41 +174,58 @@
174
175
(define (render-service-page entry log-entries)
176
(let* ((name (service-entry-name entry))
147
(status (service-entry-status entry))
148
(status-str (symbol->string status))
149
(latency (service-entry-last-latency entry))
150
(uptime (service-entry-uptime entry))
151
(history (service-entry-history entry))
152
(last-change (service-entry-last-change entry)))
+177
(history (service-entry-history entry)))
178
(render-page name
179
(list
180
(list 'h1 '(@ (class "page-title")) name)
181
(list 'div '(@ (class "status-panel"))
157
(list 'div
158
(list '@ (list 'class
159
(string-append "status-badge status-" status-str)))
160
status-str)
161
(list 'div '(@ (class "status-details"))
162
(list 'div (string-append "Uptime: " (format-uptime uptime)))
163
(list 'div (string-append "Latency: " (format-latency latency)))
164
(list 'div (string-append "Last change: "
165
(if (> last-change 0)
166
(format-datetime last-change)
167
"never")))))
+182
(render-status-badge entry)
+183
(render-status-details entry))
184
(list 'div '(@ (class "check-history"))
185
(list 'h2 "Recent Checks")
186
(if (null? history)
187
(list 'p "No checks recorded yet.")
172
(cons 'table
173
(cons '(@ (class "table"))
174
(cons
175
'(thead
176
(tr (th "Status") (th "Latency") (th "Error") (th "Time")))
177
(list
178
(cons 'tbody
179
(map render-history-row
180
(take-up-to history 20)))))))))
181
(render-log-viewer name log-entries)))))
+188
(list 'div '(@ (class "check-history-scroll"))
+189
(cons 'table
+190
(cons '(@ (class "table"))
+191
(cons
+192
'(thead
+193
(tr (th "Status") (th "Latency") (th "Error") (th "Time")))
+194
(list
+195
(cons 'tbody
+196
(cons (list '@ (list 'id
+197
(string-append "check-history-body-" name)))
+198
(map render-history-row
+199
(take-up-to history 20)))))))))))
+200
(render-log-viewer name log-entries))
+201
(list "/sse/status"
+202
(string-append "/sse/service/" name "/logs")))))
+203
+204
;; ============================================================
+205
;; Status Components (reused for SSE updates)
+206
;; ============================================================
+207
+208
(define (render-status-badge entry)
+209
(let* ((name (service-entry-name entry))
+210
(status-str (symbol->string (service-entry-status entry))))
+211
(list 'div
+212
(list '@ (list 'id (string-append "status-badge-" name))
+213
(list 'class (string-append "status-badge status-" status-str)))
+214
status-str)))
+215
+216
(define (render-status-details entry)
+217
(let* ((name (service-entry-name entry))
+218
(latency (service-entry-last-latency entry))
+219
(uptime (service-entry-uptime entry))
+220
(last-change (service-entry-last-change entry)))
+221
(list 'div (list '@ (list 'id (string-append "status-details-" name))
+222
(list 'class "status-details"))
+223
(list 'div (string-append "Uptime: " (format-uptime uptime)))
+224
(list 'div (string-append "Latency: " (format-latency latency)))
+225
(list 'div (string-append "Last change: "
+226
(if (> last-change 0)
+227
(format-datetime last-change)
+228
"never"))))))
229
230
;; ============================================================
231
;; Log Viewer
@@ -186,24 +233,12 @@
233
234
(define (render-log-viewer name log-entries)
235
(list 'div (list '@ (list 'class "log-viewer")
189
(list 'id "log-viewer")
190
(list 'data-service name))
+236
(list 'id "log-viewer"))
237
(list 'h2 "Logs")
192
(list 'div '(@ (class "log-toolbar"))
193
(list 'button '(@ (class "log-filter active") (data-level "all")
194
(onclick "filterLogs('all')")) "All")
195
(list 'button '(@ (class "log-filter log-filter-error") (data-level "error")
196
(onclick "filterLogs('error')")) "Error")
197
(list 'button '(@ (class "log-filter log-filter-warn") (data-level "warn")
198
(onclick "filterLogs('warn')")) "Warn")
199
(list 'button '(@ (class "log-filter log-filter-info") (data-level "info")
200
(onclick "filterLogs('info')")) "Info")
201
(list 'button '(@ (class "log-filter log-filter-debug") (data-level "debug")
202
(onclick "filterLogs('debug')")) "Debug")
203
(list 'button '(@ (class "log-filter log-scroll-toggle active")
204
(id "scroll-toggle")
205
(onclick "toggleAutoScroll()")) "Auto-scroll"))
206
(list 'div '(@ (class "log-container") (id "log-container"))
+238
(render-filter-toolbar name "all")
+239
(list 'div (list '@ (list 'class "log-container")
+240
(list 'id "log-container")
+241
(list 'data-sg-auto-scroll "bottom"))
242
(if (null? log-entries)
243
(list 'div '(@ (class "log-empty") (id "log-empty"))
244
"Waiting for log entries...")
@@ -211,6 +246,30 @@
246
(cons '(@ (id "log-lines"))
247
(map render-log-line log-entries)))))))
248
+249
;; Render the filter toolbar with the given active level
+250
(define (render-filter-toolbar name active-level)
+251
(list 'div (list '@ (list 'class "log-toolbar")
+252
(list 'id "log-toolbar"))
+253
(render-filter-button name "all" active-level "" "All")
+254
(render-filter-button name "error" active-level "log-filter-error" "Error")
+255
(render-filter-button name "warn" active-level "log-filter-warn" "Warn")
+256
(render-filter-button name "info" active-level "log-filter-info" "Info")
+257
(render-filter-button name "debug" active-level "log-filter-debug" "Debug")))
+258
+259
(define (render-filter-button name level active-level extra-class label)
+260
(let ((is-active (string=? level active-level))
+261
(base-class (if (string=? extra-class "")
+262
"log-filter"
+263
(string-append "log-filter " extra-class))))
+264
(list 'button
+265
(list '@ (list 'class (if is-active
+266
(string-append base-class " active")
+267
base-class))
+268
(list 'data-sg-action
+269
(string-append "/service/" name "/filter/" level))
+270
(list 'data-sg-method "get"))
+271
label)))
+272
273
(define (render-log-line entry)
274
(let ((level (log-entry-level entry))
275
(msg (log-entry-message entry))
@@ -253,7 +312,8 @@
312
(list
313
(list 'h1 '(@ (class "page-title")) "Service Not Found")
314
(list 'p (string-append "No service named \"" name "\" found."))
256
(list 'p (list 'a '(@ (href "/")) "Back to dashboard")))))
+315
(list 'p (list 'a '(@ (href "/")) "Back to dashboard")))
+316
'()))
317
318
;; ============================================================
319
;; Helpers
@@ -274,119 +334,4 @@
334
(string-append (number->string ms) "ms")
335
"---"))
336
277
;; ============================================================
278
;; SSE JavaScript
279
;; ============================================================
280
281
(define sse-js-1 "
282
var es = new EventSource('/sse/status');
283
es.addEventListener('status', function(e) {
284
var d = JSON.parse(e.data);
285
var card = document.getElementById('svc-' + d.name);
286
if (card) {
287
var a = {up:'success',down:'error',degraded:'warning'};
288
card.className = 'card card-accent accent-' + (a[d.status]||'primary');
289
var sts = card.querySelector('.service-status');
290
if (sts) sts.lastChild.textContent = ' ' + d.status;
291
var lat = card.querySelector('.service-latency');
292
if (lat) lat.textContent = d.latency > 0 ? d.latency + 'ms' : '---';
293
}
294
var badge = document.querySelector('.status-badge');
295
var panel = document.querySelector('.status-details');
296
if (badge && panel) {
297
var svcName = document.querySelector('.page-title');
298
if (svcName && svcName.textContent === d.name) {
299
badge.className = 'status-badge status-' + d.status;
300
badge.textContent = d.status;
301
var details = panel.querySelectorAll('div');
302
if (details[1]) details[1].textContent = 'Latency: ' + (d.latency > 0 ? d.latency + 'ms' : '---');
303
var tbody = document.querySelector('.check-history tbody');
304
if (tbody) {
305
var tr = document.createElement('tr');
306
var ts = d.timestamp;
307
var timeStr = '---';
308
if (ts > 0) {
309
var date = new Date(ts * 1000);
310
timeStr = date.getFullYear() + '-' + ('0'+(date.getMonth()+1)).slice(-2) + '-' + ('0'+date.getDate()).slice(-2) + ' ' + ('0'+date.getHours()).slice(-2) + ':' + ('0'+date.getMinutes()).slice(-2) + ':' + ('0'+date.getSeconds()).slice(-2);
311
}
312
tr.innerHTML = '<td><span class=\"status-dot dot-' + d.status + '\"></span> ' + d.status + '</td>' +
313
'<td>' + (d.latency > 0 ? d.latency + 'ms' : '---') + '</td>' +
314
'<td>' + (d.error || '') + '</td>' +
315
'<td>' + timeStr + '</td>';
316
tbody.insertBefore(tr, tbody.firstChild);
317
if (tbody.children.length > 20) tbody.removeChild(tbody.lastChild);
318
}
319
}
320
}
321
});
322
es.onerror = function(e) { console.error('SSE status error:', e); };
323
")
324
325
(define sse-js-2 "
326
var logViewer = document.getElementById('log-viewer');
327
if (logViewer) {
328
var svcName = logViewer.getAttribute('data-service');
329
var logEs = new EventSource('/sse/service/' + svcName + '/logs');
330
var autoScroll = true;
331
var activeFilter = 'all';
332
var container = document.getElementById('log-container');
333
")
334
335
(define sse-js-3 "
336
logEs.addEventListener('log', function(e) {
337
var d = JSON.parse(e.data);
338
var empty = document.getElementById('log-empty');
339
if (empty) empty.remove();
340
var lines = document.getElementById('log-lines');
341
if (!lines) {
342
lines = document.createElement('div');
343
lines.id = 'log-lines';
344
container.appendChild(lines);
345
}
346
var el = document.createElement('div');
347
el.className = 'log-line log-line-' + d.level;
348
el.setAttribute('data-level', d.level);
349
")
350
351
(define sse-js-4 "
352
var ts = d.timestamp;
353
var timeStr = '';
354
if (typeof ts === 'string' && ts.length >= 19) {
355
timeStr = ts.substring(11, 19);
356
} else if (typeof ts === 'number' && ts > 0) {
357
var date = new Date(ts * 1000);
358
timeStr = ('0'+date.getHours()).slice(-2)+':'+('0'+date.getMinutes()).slice(-2)+':'+('0'+date.getSeconds()).slice(-2);
359
}
360
el.innerHTML = '<span class=\"log-time\">' + timeStr + '</span>' +
361
'<span class=\"log-level log-level-' + d.level + '\">' + d.level + '</span>' +
362
'<span class=\"log-msg\">' + escapeHtml(d.message) + '</span>';
363
if (activeFilter !== 'all' && d.level !== activeFilter) el.style.display = 'none';
364
lines.appendChild(el);
365
if (autoScroll) container.scrollTop = container.scrollHeight;
366
});
367
")
368
369
(define sse-js-5 "
370
logEs.onerror = function(e) { console.error('SSE log error:', e); };
371
}
372
function escapeHtml(s) {
373
var d = document.createElement('div'); d.textContent = s; return d.innerHTML;
374
}
375
function filterLogs(level) {
376
activeFilter = level;
377
var btns = document.querySelectorAll('.log-filter:not(.log-scroll-toggle)');
378
btns.forEach(function(b) { b.classList.toggle('active', b.getAttribute('data-level') === level); });
379
var lines = document.querySelectorAll('.log-line');
380
lines.forEach(function(l) { l.style.display = (level === 'all' || l.getAttribute('data-level') === level) ? '' : 'none'; });
381
}
382
function toggleAutoScroll() {
383
autoScroll = !autoScroll;
384
document.getElementById('scroll-toggle').classList.toggle('active', autoScroll);
385
if (autoScroll) { var c = document.getElementById('log-container'); c.scrollTop = c.scrollHeight; }
386
}
387
")
388
389
(define sse-js
390
(string-append sse-js-1 sse-js-2 sse-js-3 sse-js-4 sse-js-5))
391
337
))
src/vigil/web.sglmodified
@@ -2,12 +2,18 @@
2
;;;
3
;;; Provides the HTTP interface for the monitoring dashboard,
4
;;; including HTML pages, JSON API, and SSE live updates.
+5
;;;
+6
;;; SSE endpoints send sigil-web-ui fragment events (sse-morph)
+7
;;; with pre-rendered HTML instead of JSON. The same template
+8
;;; functions from (vigil views) are used for initial page render
+9
;;; and live SSE updates.
10
11
(define-library (vigil web)
12
(import (sigil core)
13
(sigil string)
14
(sigil math)
15
(sigil json)
+16
(sigil sxml)
17
(sigil channels)
18
(sigil time)
19
(sigil io)
@@ -16,6 +22,7 @@
22
(sigil http server)
23
(sigil web routes)
24
(sigil web middleware)
+25
(sigil web ui)
26
(vigil config)
27
(vigil state)
28
(vigil logs)
@@ -34,16 +41,31 @@
41
(define (make-status-broadcast)
42
(make-broadcast))
43
37
(define (notify-status-change bc name result)
38
;; Send an SSE event to all connected dashboard clients
39
(broadcast-send bc
40
(sse-event "status"
41
(json-encode
42
(dict name: name
43
status: (symbol->string (check-record-status result))
44
latency: (check-record-latency result)
45
error: (or (check-record-error result) "")
46
timestamp: (check-record-timestamp result))))))
+44
;; Build sse-morph events for a service entry's live-updating components.
+45
;; Targets both dashboard cards and detail page elements;
+46
;; the client silently ignores targets that don't exist on the page.
+47
(define (entry-morph-events entry)
+48
(let ((name (service-entry-name entry)))
+49
(string-append
+50
(sse-morph target: (string-append "#svc-" name)
+51
mode: "replace"
+52
content: (render-service-card entry))
+53
(sse-morph target: (string-append "#status-badge-" name)
+54
mode: "replace"
+55
content: (render-status-badge entry))
+56
(sse-morph target: (string-append "#status-details-" name)
+57
mode: "replace"
+58
content: (render-status-details entry)))))
+59
+60
(define (notify-status-change bc st name result)
+61
(let ((entry (state-get-entry st name)))
+62
(when entry
+63
(broadcast-send bc
+64
(string-append
+65
(entry-morph-events entry)
+66
(sse-morph target: (string-append "#check-history-body-" name)
+67
mode: "prepend"
+68
content: (render-history-row result)))))))
69
70
;; ============================================================
71
;; Application Setup
@@ -60,6 +82,14 @@
82
(route method: GET pattern: "/style.css"
83
handler: css-handler)
84
+85
;; Sigil Web UI client library
+86
(route method: GET pattern: "/js/sigil-web-ui.js"
+87
handler: (sigil-web-ui-handler))
+88
+89
;; Log filter (returns SSE batch to update filter state)
+90
(route method: GET pattern: "/service/:name/filter/:level"
+91
handler: (filter-handler))
+92
93
;; SSE endpoints
94
(route method: GET pattern: "/sse/status"
95
handler: (sse-status-handler st bc))
@@ -111,25 +141,44 @@
141
body: vigil-css)))
142
143
;; ============================================================
114
;; SSE Handler
+144
;; Log Filter Handler
+145
;; ============================================================
+146
+147
;; Returns an SSE batch response that:
+148
;; 1. Re-renders the filter toolbar with correct active state
+149
;; 2. Adds/removes CSS filter class on the log container
+150
(define (filter-handler)
+151
(lambda (req)
+152
(let* ((name (path-param req "name"))
+153
(level (path-param req "level"))
+154
(remove-classes "filter-error filter-warn filter-info filter-debug")
+155
(add-class (if (string=? level "all")
+156
#f
+157
(string-append "filter-" level))))
+158
(sse-response-batch
+159
(sse-morph target: "#log-toolbar"
+160
mode: "replace"
+161
content: (render-filter-toolbar name level))
+162
(sse-class target: "#log-container"
+163
remove: remove-classes
+164
add: add-class)))))
+165
+166
;; ============================================================
+167
;; SSE Status Handler
168
;; ============================================================
169
170
(define (sse-status-handler st bc)
171
(lambda (req)
172
(http-response/sse-broadcast bc
120
(lambda (msg) msg) ;; messages are pre-formatted SSE strings
+173
(lambda (msg) msg)
174
on-connect: (lambda ()
122
;; Send current state of all services on connect
175
(let ((entries (state-all-entries st)))
176
(if (null? entries)
177
#f
178
(let ((port (open-output-string)))
179
(for-each
180
(lambda (entry)
129
(display
130
(sse-event "status"
131
(json-encode (service-entry->dict entry)))
132
port))
+181
(display (entry-morph-events entry) port))
182
entries)
183
(get-output-string port))))))))
184
@@ -142,22 +191,26 @@
191
(let* ((name (path-param req "name"))
192
(bc (and log-st (log-state-get-broadcast log-st name))))
193
(if bc
145
(http-response/sse-broadcast bc
146
(lambda (msg) msg)
147
on-connect: (lambda ()
148
;; Send buffered log entries on connect
149
(let ((entries (log-state-get-entries log-st name)))
150
(if (null? entries)
151
#f
152
(let ((port (open-output-string)))
153
(for-each
154
(lambda (entry)
155
(display
156
(sse-event "log"
157
(json-encode (log-entry->dict entry)))
158
port))
159
entries)
160
(get-output-string port))))))
+194
(let ((first-entry? (list #t)))
+195
(http-response/sse-broadcast bc
+196
(lambda (entry)
+197
(let ((morph (sse-morph target: "#log-lines"
+198
mode: "append"
+199
content: (render-log-line entry))))
+200
(if (car first-entry?)
+201
(begin
+202
(set-car! first-entry? #f)
+203
(string-append (sse-remove target: "#log-empty") morph))
+204
morph)))
+205
on-connect: (lambda ()
+206
(let ((entries (log-state-get-entries log-st name)))
+207
(if (null? entries)
+208
#f
+209
(begin
+210
(set-car! first-entry? #f)
+211
(sse-morph target: "#log-lines"
+212
mode: "inner"
+213
content: (map render-log-line entries))))))))
214
(http-response/json 404
215
(json-encode
216
(dict error: "no log source configured")))))))