- Replace raw TOML textarea with Alpine.js interactive form editor (10 collapsible sections with change-tier badges, dynamic array management for connections/shares/ warmup rules, proper input controls per field type) - Add SSE-based live dashboard updates replacing htmx polling - Add log viewer tab with ring buffer backend and incremental polling - Fix SMB not seeing new shares after config reload: kill entire smbd process group (not just parent PID) so forked workers release port 445 - Add SIGHUP-based smbd config reload for share changes instead of full restart, preserving existing client connections - Generate human-readable commented TOML from config editor instead of bare toml::to_string_pretty() output - Fix Alpine.js 2.x __x.$data calls in dashboard/share templates (now Alpine 3.x) - Fix toggle switch CSS overlap with field labels - Fix dashboard going blank on tab switch (remove hx-swap-oob from tab content) - Add htmx:afterSettle → Alpine.initTree() bridge for robust tab switching Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
85 lines
2.4 KiB
HTML
85 lines
2.4 KiB
HTML
<script>
|
|
function logViewerFn() {
|
|
return {
|
|
entries: [],
|
|
nextId: 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?since=' + this.nextId);
|
|
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.nextId = data.next_id;
|
|
if (this.autoScroll) {
|
|
this.$nextTick(() => {
|
|
const el = this.$refs.logBox;
|
|
if (el) el.scrollTop = el.scrollHeight;
|
|
});
|
|
}
|
|
}
|
|
} catch(e) { /* ignore fetch errors */ }
|
|
},
|
|
|
|
formatTime(ts) {
|
|
const d = new Date(ts * 1000);
|
|
return d.toLocaleTimeString('en-GB', { hour12: false });
|
|
},
|
|
|
|
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="entry in entries" :key="entry.id">
|
|
<div class="log-line">
|
|
<span class="log-ts" x-text="formatTime(entry.ts)"></span>
|
|
<span class="log-msg" x-text="entry.msg"></span>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|