import { h, PageHeader, Tabs, esc, definePage, refactorLoad } from '/static/hoover/index.js?v=6'; const logTabs = [ { key: 'journal', label: 'Journal', url: '/api/logs/journal' }, { key: 'nginx-access', label: 'Nginx Access', url: '/api/logs/nginx/access' }, { key: 'nginx-error', label: 'Nginx Error', url: '/api/logs/nginx/error' }, { key: 'dnsmasq', label: 'Dnsmasq', url: '/api/logs/dnsmasq' }, { key: 'app', label: 'App', url: '/api/logs/app' }, ]; async function fetchLog(state, url, signal) { if (signal?.aborted) return; const res = await fetch(url, { signal }); if (signal?.aborted) return; if (!res.ok) throw new Error(`HTTP ${res.status}`); const text = await res.text(); if (signal?.aborted) return; state.lines = text.split('\n').filter(l => l.length > 0); } export default definePage({ init() { return { activeTab: 'journal', lines: [], loading: false, _abortCtrl: null }; }, subscribe: [], async load(state, abortController, entry) { await refactorLoad(state, s => s.lines?.length, async (s, sig, isAborted) => { const tab = logTabs.find(t => t.key === s.activeTab) || logTabs[0]; await fetchLog(s, tab.url, sig); }, { entry, abortController }, ); }, onUnmount(state) { state._abortCtrl?.abort(); state.lines = []; }, render(state) { const tab = logTabs.find(t => t.key === state.activeTab) || logTabs[0]; const lineVnodes = state.lines.map((line, i) => h('div', { class: 'log-line', key: i }, esc(line)) ); return [ PageHeader({ title: 'Logs', subtitle: 'System & service logs' }), Tabs({ state, tabs: logTabs.map(t => t.key), formatLabel: (k) => { const tab = logTabs.find(t => t.key === k); return tab ? tab.label : k.charAt(0).toUpperCase() + k.slice(1); }, onTabClick: async (key) => { const tab = logTabs.find(t => t.key === key); if (!tab) return; state._abortCtrl?.abort(); const ctrl = new AbortController(); state._abortCtrl = ctrl; const tabs = state.lines?.length ? state : null; state.refreshing = !!tabs; if (!tabs) state.loading = true; state.error = null; try { await fetchLog(state, tab.url, ctrl.signal); } catch (e) { if (!ctrl.signal.aborted) state.error = String(e); } state.loading = false; state.refreshing = false; }, }), h('div', { class: 'card', key: 'log-card' }, h('div', { class: 'card-header' }, h('span', null, tab.label), h('button', { class: 'btn btn-sm btn-outline', style: 'float:right;', 'on:click': async () => { state._abortCtrl?.abort(); const ctrl = new AbortController(); state._abortCtrl = ctrl; state.refreshing = true; state.error = null; try { await fetchLog(state, tab.url, ctrl.signal); } catch (e) { if (!ctrl.signal.aborted) state.error = String(e); } state.loading = false; state.refreshing = false; }, }, '\u21BB') ), h('div', { class: 'card-body log-body' }, state.loading && !state.refreshing ? h('div', { class: 'loading' }, state.refreshing ? 'Refreshing...' : 'Loading...') : state.error ? h('div', { class: 'error-msg' }, state.error) : lineVnodes.length > 0 ? h('pre', null, lineVnodes) : h('div', { class: 'text-muted text-sm' }, 'No log lines available') ) ), ]; }, });