appview/pages: show live updating counters for steps #731

merged
opened by oppi.li targeting master from push-lstutzylzylk

spindles can now give us detailed logs for start and end of steps. the appview can ingest these logs to indicate live durations for steps. it is implemented like so:

  • the logs handler keeps track of start and end times for each step
  • whenever we recieve a start or end time, we update the html to add a data-start or data-end attribute
  • using some javascript, we print a live updating timer for each step:
    • if only data-start is present: then use Now - Start and update each second
    • if both data-start and data-end are present, then use End - Start

Signed-off-by: oppiliappan me@oppi.li

Changed files
+90 -19
appview
pages
templates
pipelines
+12
appview/pages/pages.go
··· 15 15 "path/filepath" 16 16 "strings" 17 17 "sync" 18 + "time" 18 19 19 20 "tangled.org/core/api/tangled" 20 21 "tangled.org/core/appview/commitverify" ··· 1361 1362 Name string 1362 1363 Command string 1363 1364 Collapsed bool 1365 + StartTime time.Time 1364 1366 } 1365 1367 1366 1368 func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error { 1367 1369 return p.executePlain("repo/pipelines/fragments/logBlock", w, params) 1368 1370 } 1369 1371 1372 + type LogBlockEndParams struct { 1373 + Id int 1374 + StartTime time.Time 1375 + EndTime time.Time 1376 + } 1377 + 1378 + func (p *Pages) LogBlockEnd(w io.Writer, params LogBlockEndParams) error { 1379 + return p.executePlain("repo/pipelines/fragments/logBlockEnd", w, params) 1380 + } 1381 + 1370 1382 type LogLineParams struct { 1371 1383 Id int 1372 1384 Content string
+36
appview/pages/templates/fragments/workflow-timers.html
··· 1 + {{ define "fragments/workflow-timers" }} 2 + <script> 3 + function formatElapsed(seconds) { 4 + if (seconds < 1) return '0s'; 5 + if (seconds < 60) return `${seconds}s`; 6 + const minutes = Math.floor(seconds / 60); 7 + const secs = seconds % 60; 8 + if (seconds < 3600) return `${minutes}m ${secs}s`; 9 + const hours = Math.floor(seconds / 3600); 10 + const mins = Math.floor((seconds % 3600) / 60); 11 + return `${hours}h ${mins}m`; 12 + } 13 + 14 + function updateTimers() { 15 + const now = Math.floor(Date.now() / 1000); 16 + 17 + document.querySelectorAll('[data-timer]').forEach(el => { 18 + const startTime = parseInt(el.dataset.start); 19 + const endTime = el.dataset.end ? parseInt(el.dataset.end) : null; 20 + 21 + if (endTime) { 22 + // Step is complete, show final time 23 + const elapsed = endTime - startTime; 24 + el.textContent = formatElapsed(elapsed); 25 + } else { 26 + // Step is running, update live 27 + const elapsed = now - startTime; 28 + el.textContent = formatElapsed(elapsed); 29 + } 30 + }); 31 + } 32 + 33 + setInterval(updateTimers, 1000); 34 + updateTimers(); 35 + </script> 36 + {{ end }}
+7 -6
appview/pages/templates/repo/pipelines/fragments/logBlock.html
··· 2 2 <div id="lines" hx-swap-oob="beforeend"> 3 3 <details id="step-{{ .Id }}" {{if not .Collapsed}}open{{end}} class="group pb-2 rounded-sm border border-gray-200 dark:border-gray-700"> 4 4 <summary class="sticky top-0 pt-2 px-2 group-open:pb-2 group-open:mb-2 list-none cursor-pointer group-open:border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:text-gray-500 hover:dark:text-gray-400"> 5 - <div class="group-open:hidden flex items-center gap-1"> 6 - {{ i "chevron-right" "w-4 h-4" }} {{ .Name }} 7 - </div> 8 - <div class="hidden group-open:flex items-center gap-1"> 9 - {{ i "chevron-down" "w-4 h-4" }} {{ .Name }} 10 - </div> 5 + <div class="group-open:hidden flex items-center gap-1">{{ template "stepHeader" . }}</div> 6 + <div class="hidden group-open:flex items-center gap-1">{{ template "stepHeader" . }}</div> 11 7 </summary> 12 8 <div class="font-mono whitespace-pre overflow-x-auto px-2"><div class="text-blue-600 dark:text-blue-300">{{ .Command }}</div><div id="step-body-{{ .Id }}"></div></div> 13 9 </details> 14 10 </div> 15 11 {{ end }} 12 + 13 + {{ define "stepHeader" }} 14 + {{ i "chevron-right" "w-4 h-4" }} {{ .Name }} 15 + <span class="ml-auto text-sm text-gray-500 tabular-nums" data-timer="{{ .Id }}" data-start="{{ .StartTime.Unix }}"></span> 16 + {{ end }}
+9
appview/pages/templates/repo/pipelines/fragments/logBlockEnd.html
··· 1 + {{ define "repo/pipelines/fragments/logBlockEnd" }} 2 + <span 3 + class="ml-auto text-sm text-gray-500 tabular-nums" 4 + data-timer="{{ .Id }}" 5 + data-start="{{ .StartTime.Unix }}" 6 + data-end="{{ .EndTime.Unix }}" 7 + hx-swap-oob="outerHTML:[data-timer='{{ .Id }}']"></span> 8 + {{ end }} 9 +
+1
appview/pages/templates/repo/pipelines/workflow.html
··· 15 15 {{ block "logs" . }} {{ end }} 16 16 </div> 17 17 </section> 18 + {{ template "fragments/workflow-timers" }} 18 19 {{ end }} 19 20 20 21 {{ define "sidebar" }}
+25 -13
appview/pipelines/pipelines.go
··· 236 236 // start a goroutine to read from spindle 237 237 go readLogs(spindleConn, evChan) 238 238 239 - stepIdx := 0 239 + stepStartTimes := make(map[int]time.Time) 240 240 var fragment bytes.Buffer 241 241 for { 242 242 select { ··· 268 268 269 269 switch logLine.Kind { 270 270 case spindlemodel.LogKindControl: 271 - // control messages create a new step block 272 - stepIdx++ 273 - collapsed := false 274 - if logLine.StepKind == spindlemodel.StepKindSystem { 275 - collapsed = true 271 + switch logLine.StepStatus { 272 + case spindlemodel.StepStatusStart: 273 + stepStartTimes[logLine.StepId] = logLine.Time 274 + collapsed := false 275 + if logLine.StepKind == spindlemodel.StepKindSystem { 276 + collapsed = true 277 + } 278 + err = p.pages.LogBlock(&fragment, pages.LogBlockParams{ 279 + Id: logLine.StepId, 280 + Name: logLine.Content, 281 + Command: logLine.StepCommand, 282 + Collapsed: collapsed, 283 + StartTime: logLine.Time, 284 + }) 285 + case spindlemodel.StepStatusEnd: 286 + startTime := stepStartTimes[logLine.StepId] 287 + endTime := logLine.Time 288 + err = p.pages.LogBlockEnd(&fragment, pages.LogBlockEndParams{ 289 + Id: logLine.StepId, 290 + StartTime: startTime, 291 + EndTime: endTime, 292 + }) 276 293 } 277 - err = p.pages.LogBlock(&fragment, pages.LogBlockParams{ 278 - Id: stepIdx, 279 - Name: logLine.Content, 280 - Command: logLine.StepCommand, 281 - Collapsed: collapsed, 282 - }) 294 + 283 295 case spindlemodel.LogKindData: 284 296 // data messages simply insert new log lines into current step 285 297 err = p.pages.LogLine(&fragment, pages.LogLineParams{ 286 - Id: stepIdx, 298 + Id: logLine.StepId, 287 299 Content: logLine.Content, 288 300 }) 289 301 }