grabbit 64d6171ec9 Unify logging to tracing: file appender + unified log viewer
Replace scattered println!/eprintln! with structured tracing macros throughout
supervisor, scheduler, and web modules. Add LogConfig (file + level) to Config
and a new logging module that initialises a stderr + optional non-blocking file
appender on `warpgate run`. Remove the in-memory LogBuffer/LogEntry from
AppState; the web /api/logs endpoint now reads the log file directly with
from_line/lines pagination. `warpgate log` replaces journalctl with `tail`,
and the Logs tab Alpine.js is updated to match the new API response shape.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 11:24:06 +08:00

79 lines
2.2 KiB
HTML

<script>
function logViewerFn() {
return {
entries: [],
fromLine: 0,
polling: null,
autoScroll: true,
init() {
this.fetchLogs();
this.polling = setInterval(() => this.fetchLogs(), 2000);
},
destroy() {
if (this.polling) clearInterval(this.polling);
},
async fetchLogs() {
try {
const resp = await fetch('/api/logs?lines=200&from_line=' + this.fromLine);
const data = await resp.json();
if (data.entries.length > 0) {
this.entries = this.entries.concat(data.entries);
// Keep client-side buffer reasonable
if (this.entries.length > 1000) {
this.entries = this.entries.slice(-500);
}
this.fromLine = data.total_lines;
if (this.autoScroll) {
this.$nextTick(() => {
const el = this.$refs.logBox;
if (el) el.scrollTop = el.scrollHeight;
});
}
}
} catch(e) { /* ignore fetch errors */ }
},
clear() {
this.entries = [];
}
}
}
if (window.Alpine) {
Alpine.data('logViewer', logViewerFn);
} else {
document.addEventListener('alpine:init', function() {
Alpine.data('logViewer', logViewerFn);
});
}
</script>
<div x-data="logViewer" x-init="init()" @htmx:before-swap.window="destroy()">
<div class="log-toolbar">
<div>
<span class="log-count" x-text="entries.length + ' entries'"></span>
</div>
<div style="display:flex;gap:12px;align-items:center">
<label class="toggle" style="font-size:0.85em">
<input type="checkbox" x-model="autoScroll">
<span class="slider"></span>
Auto-scroll
</label>
<button type="button" class="btn btn-secondary" style="padding:4px 12px;font-size:0.8em" @click="clear()">Clear</button>
</div>
</div>
<div class="log-viewer" x-ref="logBox">
<template x-if="entries.length === 0">
<div style="color:var(--text-muted);padding:24px;text-align:center">No log entries yet. Events will appear here as they occur.</div>
</template>
<template x-for="(line, idx) in entries" :key="idx">
<div class="log-line">
<span class="log-msg" x-text="line"></span>
</div>
</template>
</div>
</div>