633505e7dc
- Add quick modal, table, service status, and confirmation dialog components - Refactor all pages (certs, dhcp, proxy, etc.) to use new component patterns - Introduce refactor load utility and render guard for consistent UX - Add hoover documentation and update AGENTS.md, architecture, overview
110 lines
4.5 KiB
JavaScript
110 lines
4.5 KiB
JavaScript
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')
|
|
)
|
|
),
|
|
];
|
|
},
|
|
});
|