pre-refactor
This commit is contained in:
@@ -15,6 +15,7 @@ __pycache__/
|
|||||||
# Local AI tool config (contains internal hostnames)
|
# Local AI tool config (contains internal hostnames)
|
||||||
opencode.json
|
opencode.json
|
||||||
opencode.json.pwenv
|
opencode.json.pwenv
|
||||||
|
PLAN.md
|
||||||
|
|
||||||
# Playwright MCP artifacts
|
# Playwright MCP artifacts
|
||||||
.playwright-mcp/
|
.playwright-mcp/
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ systemctl restart nginx
|
|||||||
systemctl restart vacuum-wall
|
systemctl restart vacuum-wall
|
||||||
systemctl restart vacuum-walld
|
systemctl restart vacuum-walld
|
||||||
|
|
||||||
|
systemctl status nginx
|
||||||
systemctl status vacuum-wall
|
systemctl status vacuum-wall
|
||||||
systemctl status vacuum-walld
|
systemctl status vacuum-walld
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -197,7 +197,7 @@ def spa_page(path=""):
|
|||||||
scheme = "wss" if request.is_secure else "ws"
|
scheme = "wss" if request.is_secure else "ws"
|
||||||
ws_url = f"{scheme}://{request.host}/ws"
|
ws_url = f"{scheme}://{request.host}/ws"
|
||||||
html = (SPA_DIR / "index.html").read_text()
|
html = (SPA_DIR / "index.html").read_text()
|
||||||
return html.replace("__WS_URL__", ws_url)
|
return html.replace("__WS_URL_PLACEHOLDER__", ws_url)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
+9
-17
@@ -55,7 +55,7 @@ window.addEventListener('hashchange', () => {
|
|||||||
router.state.path = location.hash.slice(1) || '/dashboard';
|
router.state.path = location.hash.slice(1) || '/dashboard';
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ── Sidebar component ─────────────────────────────────────── */
|
/* ── Sidebar render root ───────────────────────────────────── */
|
||||||
function Sidebar() {
|
function Sidebar() {
|
||||||
const current = router.state.path;
|
const current = router.state.path;
|
||||||
return h('div', { class: 'sidebar' },
|
return h('div', { class: 'sidebar' },
|
||||||
@@ -72,29 +72,21 @@ function Sidebar() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Route component wrapper ───────────────────────────────── */
|
/* ── Main content render root ──────────────────────────────── */
|
||||||
function RouteComponent() {
|
function MainContent() {
|
||||||
return router.component();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── App layout ────────────────────────────────────────────── */
|
|
||||||
function AppLayout() {
|
|
||||||
return [
|
return [
|
||||||
h('div', { class: 'layout' },
|
router.component(),
|
||||||
Sidebar(),
|
|
||||||
h('div', { class: 'main' },
|
|
||||||
RouteComponent(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ToastContainer(),
|
ToastContainer(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Init ──────────────────────────────────────────────────── */
|
/* ── Init ──────────────────────────────────────────────────── */
|
||||||
export function initApp() {
|
export function initApp() {
|
||||||
const appEl = document.getElementById('app');
|
const sidebarEl = document.getElementById('sidebar');
|
||||||
if (appEl) {
|
const mainEl = document.getElementById('main');
|
||||||
render(appEl, AppLayout);
|
if (sidebarEl && mainEl) {
|
||||||
|
render(sidebarEl, Sidebar);
|
||||||
|
render(mainEl, MainContent);
|
||||||
}
|
}
|
||||||
// Defer connect() after the first render microtask settles to prevent
|
// Defer connect() after the first render microtask settles to prevent
|
||||||
// the initial requestUpdate() from triggering a second commit while
|
// the initial requestUpdate() from triggering a second commit while
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ export async function apiFetch(url, options = {}) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url, { method, headers, body: options.body, credentials: 'same-origin', ...opts });
|
const res = await fetch(url, { method, headers, body: options.body, credentials: 'same-origin', ...opts });
|
||||||
|
if (opts.signal?.aborted) {
|
||||||
|
return { ok: false, data: null, error: 'Aborted', status: 0 };
|
||||||
|
}
|
||||||
if (res.status === 401) {
|
if (res.status === 401) {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
return { ok: false, data: null, error: 'Session expired', status: 401 };
|
return { ok: false, data: null, error: 'Session expired', status: 401 };
|
||||||
|
|||||||
@@ -20,13 +20,19 @@ import { reactive } from './reactivity.js';
|
|||||||
import { h } from './vdom.js';
|
import { h } from './vdom.js';
|
||||||
import { _compExpandedCache } from './render.js';
|
import { _compExpandedCache } from './render.js';
|
||||||
|
|
||||||
/**
|
/** Registry of mounted components: key → { state, subscriptions, loadAbort, entry, isLoading } */
|
||||||
* Registry of mounted components: key → { state, subscriptions, loadAbort, entry }
|
|
||||||
*/
|
|
||||||
const _mounted = new Map();
|
const _mounted = new Map();
|
||||||
|
|
||||||
/**
|
/** Check whether a state object belongs to a currently mounted component.
|
||||||
* External subscribe function from websocket.js.
|
* Used by websocket.js to skip auto-refresh for unmounted pages. */
|
||||||
|
export function isComponentStateMounted(state) {
|
||||||
|
for (const entry of _mounted.values()) {
|
||||||
|
if (entry.state === state) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** External subscribe function from websocket.js.
|
||||||
* Set via setSubscribeFn() when the websocket module initializes.
|
* Set via setSubscribeFn() when the websocket module initializes.
|
||||||
*/
|
*/
|
||||||
let _subscribeFn = null;
|
let _subscribeFn = null;
|
||||||
@@ -67,29 +73,45 @@ export function definePage(def) {
|
|||||||
* enters the tree for the first time.
|
* enters the tree for the first time.
|
||||||
*/
|
*/
|
||||||
export function mountComponent(key, renderer) {
|
export function mountComponent(key, renderer) {
|
||||||
// Prevent duplicate mounts when normalization loses #comp tracking
|
|
||||||
if (_mounted.has(key)) return;
|
|
||||||
|
|
||||||
const pd = renderer._pageDef;
|
const pd = renderer._pageDef;
|
||||||
if (!pd) return;
|
if (!pd) return;
|
||||||
|
|
||||||
const entry = {
|
let entry = _mounted.get(key);
|
||||||
state: pd.state,
|
|
||||||
subscriptions: [],
|
|
||||||
loadAbort: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
_mounted.set(key, entry);
|
if (entry) {
|
||||||
|
// Re-mount of an already-mounted page: restart load with fresh AbortController
|
||||||
|
if (entry.loadAbort) {
|
||||||
|
entry.loadAbort.abort();
|
||||||
|
}
|
||||||
|
entry.requestId++;
|
||||||
|
entry.loadAbort = null;
|
||||||
|
} else {
|
||||||
|
// Fresh mount
|
||||||
|
entry = {
|
||||||
|
state: pd.state,
|
||||||
|
subscriptions: [],
|
||||||
|
loadAbort: null,
|
||||||
|
requestId: 0,
|
||||||
|
};
|
||||||
|
_mounted.set(key, entry);
|
||||||
|
}
|
||||||
|
|
||||||
// Fire load
|
// Clear error on re-mount; load() decides loading vs refreshing
|
||||||
|
pd.state.error = null;
|
||||||
|
|
||||||
|
// Fire load with fresh AbortController
|
||||||
if (pd.load) {
|
if (pd.load) {
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
entry.loadAbort = abortController;
|
entry.loadAbort = abortController;
|
||||||
pd.load(pd.state, abortController);
|
entry.requestId++;
|
||||||
|
entry.isLoading = true;
|
||||||
|
Promise.resolve()
|
||||||
|
.then(() => pd.load(pd.state, abortController, entry))
|
||||||
|
.finally(() => { entry.isLoading = false; });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register WS subscriptions
|
// Register WS subscriptions (only on fresh mount)
|
||||||
if (_subscribeFn && pd.subscribe.length) {
|
if (!entry.subscriptions.length && _subscribeFn && pd.subscribe.length) {
|
||||||
for (const topic of pd.subscribe) {
|
for (const topic of pd.subscribe) {
|
||||||
const unsub = _subscribeFn(renderer, topic, pd.load, pd.state);
|
const unsub = _subscribeFn(renderer, topic, pd.load, pd.state);
|
||||||
if (unsub) entry.subscriptions.push(unsub);
|
if (unsub) entry.subscriptions.push(unsub);
|
||||||
@@ -111,6 +133,8 @@ export function unmountComponent(key, renderer) {
|
|||||||
if (entry.loadAbort) {
|
if (entry.loadAbort) {
|
||||||
entry.loadAbort.abort();
|
entry.loadAbort.abort();
|
||||||
}
|
}
|
||||||
|
// Invalidate any in-flight callbacks
|
||||||
|
entry.requestId++;
|
||||||
|
|
||||||
// Unsubscribe from WS
|
// Unsubscribe from WS
|
||||||
for (const unsub of entry.subscriptions) {
|
for (const unsub of entry.subscriptions) {
|
||||||
|
|||||||
@@ -218,7 +218,11 @@ function diffContainer(container, prev, vnodes) {
|
|||||||
if (oldDom?.nodeType === Node.ELEMENT_NODE) sweepDom(oldDom);
|
if (oldDom?.nodeType === Node.ELEMENT_NODE) sweepDom(oldDom);
|
||||||
const nd = createDom(newV);
|
const nd = createDom(newV);
|
||||||
_vnodeDom.set(newV, nd);
|
_vnodeDom.set(newV, nd);
|
||||||
if (oldDom?.parentNode) oldDom.parentNode.replaceChild(nd, oldDom);
|
if (oldDom?.parentNode) {
|
||||||
|
oldDom.parentNode.replaceChild(nd, oldDom);
|
||||||
|
} else if (nd.parentNode !== container) {
|
||||||
|
container.insertBefore(nd, lastDom ? lastDom.nextSibling : null);
|
||||||
|
}
|
||||||
lastDom = nd;
|
lastDom = nd;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
* auto-refresh messages from the backend can trigger page reloads.
|
* auto-refresh messages from the backend can trigger page reloads.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { setSubscribeFn } from './component.js';
|
import { setSubscribeFn, isComponentStateMounted } from './component.js';
|
||||||
|
|
||||||
const _wsSubs = new Map();
|
const _wsSubs = new Map();
|
||||||
let _wsConn = null;
|
let _wsConn = null;
|
||||||
@@ -74,7 +74,7 @@ function handleMessage(msg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const s of _wsSubs.values()) {
|
for (const s of _wsSubs.values()) {
|
||||||
if (s.unsubscribed) continue;
|
if (s.unsubscribed || !isComponentStateMounted(s.state)) continue;
|
||||||
if (s.topic === '*') {
|
if (s.topic === '*') {
|
||||||
s.loadFn(s.state);
|
s.loadFn(s.state);
|
||||||
} else if (topics.some(t => t === s.topic || t === '*')) {
|
} else if (topics.some(t => t === s.topic || t === '*')) {
|
||||||
|
|||||||
@@ -7,8 +7,13 @@
|
|||||||
<link rel="stylesheet" href="/static/style.css">
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app">
|
||||||
<script>window.__WS_URL__ = "__WS_URL__"</script>
|
<div class="layout">
|
||||||
|
<div id="sidebar"></div>
|
||||||
|
<div class="main" id="main"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>window.__WS_URL__ = "__WS_URL_PLACEHOLDER__"</script>
|
||||||
<script type="module" src="/static/app.js?v=4"></script>
|
<script type="module" src="/static/app.js?v=4"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -57,29 +57,36 @@ async function pollCertIssue(rid, state) {
|
|||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function load(state) {
|
async function load(state, abortController, entry) {
|
||||||
|
if (state.certs?.length) state.refreshing = true;
|
||||||
|
else state.loading = true;
|
||||||
try {
|
try {
|
||||||
const r = await apiFetch('/api/certs/list');
|
const myId = entry ? entry.requestId : 0;
|
||||||
|
const sig = abortController?.signal;
|
||||||
|
const r = await apiFetch('/api/certs/list', { signal: sig });
|
||||||
|
if (abortController?.signal.aborted || (entry && entry.requestId !== myId)) return;
|
||||||
if (r.ok) state.certs = r.data || [];
|
if (r.ok) state.certs = r.data || [];
|
||||||
else state.error = r.error;
|
else state.error = r.error;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (abortController?.signal.aborted) return;
|
||||||
state.error = String(e);
|
state.error = String(e);
|
||||||
}
|
}
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
|
state.refreshing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default definePage({
|
export default definePage({
|
||||||
init() {
|
init() {
|
||||||
return { certs: [], loading: true, error: null };
|
return { certs: [], loading: true, refreshing: false, error: null };
|
||||||
},
|
},
|
||||||
subscribe: ['acme'],
|
subscribe: ['acme'],
|
||||||
load,
|
load,
|
||||||
render(state) {
|
render(state) {
|
||||||
if (state.loading) {
|
if (state.loading && !state.refreshing) {
|
||||||
return [
|
return [
|
||||||
PageHeader({ title: 'Certificates' }),
|
PageHeader({ title: 'Certificates' }),
|
||||||
h('div', { class: 'card', key: 'loading' },
|
h('div', { class: 'card', key: 'loading' },
|
||||||
h('div', { class: 'card-body loading' }, 'Loading...'),
|
h('div', { class: 'card-body loading' }, state.refreshing ? 'Refreshing...' : 'Loading...'),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,25 +2,31 @@ import { h, PageHeader, Badge, StatusDot, Empty, Card, esc, enc, att_esc, $val,
|
|||||||
|
|
||||||
export default definePage({
|
export default definePage({
|
||||||
init() {
|
init() {
|
||||||
return { data: null, loading: true, error: null };
|
return { data: null, loading: true, refreshing: false, error: null };
|
||||||
},
|
},
|
||||||
subscribe: ['*'],
|
subscribe: ['*'],
|
||||||
async load(state) {
|
async load(state, abortController, entry) {
|
||||||
|
const myId = entry ? entry.requestId : 0;
|
||||||
|
if (state.data) state.refreshing = true;
|
||||||
|
else state.loading = true;
|
||||||
try {
|
try {
|
||||||
const res = await apiFetch('/api/status/all');
|
const res = await apiFetch('/api/status/all', { signal: abortController?.signal });
|
||||||
|
if (abortController?.signal.aborted || entry.requestId !== myId) return;
|
||||||
if (res.ok) state.data = res.data;
|
if (res.ok) state.data = res.data;
|
||||||
else state.error = res.error;
|
else state.error = res.error;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (abortController?.signal.aborted) return;
|
||||||
state.error = String(e);
|
state.error = String(e);
|
||||||
}
|
}
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
|
state.refreshing = false;
|
||||||
},
|
},
|
||||||
render(state) {
|
render(state) {
|
||||||
if (state.loading) {
|
if (state.loading && !state.refreshing) {
|
||||||
return [
|
return [
|
||||||
PageHeader({ title: 'Dashboard', subtitle: 'System overview' }),
|
PageHeader({ title: 'Dashboard', subtitle: 'System overview' }),
|
||||||
h('div', { class: 'card', key: 'loading' },
|
h('div', { class: 'card', key: 'loading' },
|
||||||
h('div', { class: 'card-body loading' }, 'Loading...'),
|
h('div', { class: 'card-body loading' }, state.refreshing ? 'Refreshing...' : 'Loading...'),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,32 +120,41 @@ function addDnsModal(state) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function load(state) {
|
async function load(state, abortController, entry) {
|
||||||
|
if (Object.keys(state.config || {}).length) state.refreshing = true;
|
||||||
|
else state.loading = true;
|
||||||
try {
|
try {
|
||||||
const cfgR = await apiFetch('/api/dhcp/config');
|
const myId = entry ? entry.requestId : 0;
|
||||||
|
const sig = abortController?.signal;
|
||||||
|
const cfgR = await apiFetch('/api/dhcp/config', { signal: sig });
|
||||||
|
if (abortController?.signal.aborted || (entry && entry.requestId !== myId)) return;
|
||||||
if (cfgR.ok) state.config = cfgR.data || {};
|
if (cfgR.ok) state.config = cfgR.data || {};
|
||||||
const stR = await apiFetch('/api/dhcp/status');
|
const stR = await apiFetch('/api/dhcp/status', { signal: sig });
|
||||||
|
if (abortController?.signal.aborted || (entry && entry.requestId !== myId)) return;
|
||||||
if (stR.ok) state.status = stR.data || {};
|
if (stR.ok) state.status = stR.data || {};
|
||||||
const lsR = await apiFetch('/api/dhcp/leases');
|
const lsR = await apiFetch('/api/dhcp/leases', { signal: sig });
|
||||||
|
if (abortController?.signal.aborted || (entry && entry.requestId !== myId)) return;
|
||||||
if (lsR.ok) state.leases = lsR.data || [];
|
if (lsR.ok) state.leases = lsR.data || [];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (abortController?.signal.aborted) return;
|
||||||
state.error = String(e);
|
state.error = String(e);
|
||||||
}
|
}
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
|
state.refreshing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default definePage({
|
export default definePage({
|
||||||
init() {
|
init() {
|
||||||
return { config: {}, status: {}, leases: [], loading: true, error: null, activeTab: 'ranges' };
|
return { config: {}, status: {}, leases: [], loading: true, refreshing: false, error: null, activeTab: 'ranges' };
|
||||||
},
|
},
|
||||||
subscribe: ['dnsmasq'],
|
subscribe: ['dnsmasq'],
|
||||||
load,
|
load,
|
||||||
render(state) {
|
render(state) {
|
||||||
if (state.loading) {
|
if (state.loading && !state.refreshing) {
|
||||||
return [
|
return [
|
||||||
PageHeader({ title: 'DHCP & DNS' }),
|
PageHeader({ title: 'DHCP & DNS' }),
|
||||||
h('div', { class: 'card', key: 'loading' },
|
h('div', { class: 'card', key: 'loading' },
|
||||||
h('div', { class: 'card-body loading' }, 'Loading...'),
|
h('div', { class: 'card-body loading' }, state.refreshing ? 'Refreshing...' : 'Loading...'),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,12 +50,17 @@ function cfgModal(name, state) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function load(state) {
|
async function load(state, abortController, entry) {
|
||||||
|
if (state.ifaces?.length) state.refreshing = true;
|
||||||
|
else state.loading = true;
|
||||||
try {
|
try {
|
||||||
|
const myId = entry ? entry.requestId : 0;
|
||||||
|
const sig = abortController?.signal;
|
||||||
const [fw, net] = await Promise.all([
|
const [fw, net] = await Promise.all([
|
||||||
apiFetch('/api/firewall/zones'),
|
apiFetch('/api/firewall/zones', { signal: sig }),
|
||||||
apiFetch('/api/network/interfaces'),
|
apiFetch('/api/network/interfaces', { signal: sig }),
|
||||||
]);
|
]);
|
||||||
|
if (abortController?.signal.aborted || (entry && entry.requestId !== myId)) return;
|
||||||
// Extract zone names from available zones (for the dropdown)
|
// Extract zone names from available zones (for the dropdown)
|
||||||
state.zones = fw.ok ? (fw.data?.available || []) : [];
|
state.zones = fw.ok ? (fw.data?.available || []) : [];
|
||||||
if (net.ok) {
|
if (net.ok) {
|
||||||
@@ -78,23 +83,25 @@ async function load(state) {
|
|||||||
state.error = net.error;
|
state.error = net.error;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (abortController?.signal.aborted) return;
|
||||||
state.error = String(e);
|
state.error = String(e);
|
||||||
}
|
}
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
|
state.refreshing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default definePage({
|
export default definePage({
|
||||||
init() {
|
init() {
|
||||||
return { ifaces: [], zones: [], loading: true, error: null };
|
return { ifaces: [], zones: [], loading: true, refreshing: false, error: null };
|
||||||
},
|
},
|
||||||
subscribe: ['firewall', 'networkd'],
|
subscribe: ['firewall', 'networkd'],
|
||||||
load,
|
load,
|
||||||
render(state) {
|
render(state) {
|
||||||
if (state.loading) {
|
if (state.loading && !state.refreshing) {
|
||||||
return [
|
return [
|
||||||
PageHeader({ title: 'Interfaces' }),
|
PageHeader({ title: 'Interfaces' }),
|
||||||
h('div', { class: 'card', key: 'loading' },
|
h('div', { class: 'card', key: 'loading' },
|
||||||
h('div', { class: 'card-body loading' }, 'Loading...'),
|
h('div', { class: 'card-body loading' }, state.refreshing ? 'Refreshing...' : 'Loading...'),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,27 +9,35 @@ const logTabs = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
async function fetchLog(state, url) {
|
async function fetchLog(state, url, signal) {
|
||||||
state.loading = true;
|
state.loading = true;
|
||||||
state.error = null;
|
state.error = null;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url);
|
const res = await fetch(url, { signal });
|
||||||
|
if (signal?.aborted) return;
|
||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
|
if (signal?.aborted) return;
|
||||||
state.lines = text.split('\n').filter(l => l.length > 0);
|
state.lines = text.split('\n').filter(l => l.length > 0);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (signal?.aborted) return;
|
||||||
state.error = String(e);
|
state.error = String(e);
|
||||||
}
|
}
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
|
state.refreshing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default definePage({
|
export default definePage({
|
||||||
init() {
|
init() {
|
||||||
return { activeTab: 'journal', lines: [], loading: false, error: null };
|
return { activeTab: 'journal', lines: [], loading: false, refreshing: false, error: null };
|
||||||
},
|
},
|
||||||
subscribe: [],
|
subscribe: [],
|
||||||
async load(state) {
|
async load(state, abortController, entry) {
|
||||||
|
if (state.lines?.length) state.refreshing = true;
|
||||||
|
else state.loading = true;
|
||||||
|
const myId = entry ? entry.requestId : 0;
|
||||||
const tab = logTabs.find(t => t.key === state.activeTab) || logTabs[0];
|
const tab = logTabs.find(t => t.key === state.activeTab) || logTabs[0];
|
||||||
await fetchLog(state, tab.url);
|
await fetchLog(state, tab.url, abortController?.signal);
|
||||||
|
if (abortController?.signal.aborted || (entry && entry.requestId !== myId)) return;
|
||||||
},
|
},
|
||||||
onUnmount(state) {
|
onUnmount(state) {
|
||||||
state.lines = [];
|
state.lines = [];
|
||||||
@@ -65,8 +73,8 @@ export default definePage({
|
|||||||
}, '\u21BB')
|
}, '\u21BB')
|
||||||
),
|
),
|
||||||
h('div', { class: 'card-body log-body' },
|
h('div', { class: 'card-body log-body' },
|
||||||
state.loading
|
state.loading && !state.refreshing
|
||||||
? h('div', { class: 'loading' }, 'Loading...')
|
? h('div', { class: 'loading' }, state.refreshing ? 'Refreshing...' : 'Loading...')
|
||||||
: state.error
|
: state.error
|
||||||
? h('div', { class: 'error-msg' }, state.error)
|
? h('div', { class: 'error-msg' }, state.error)
|
||||||
: lineVnodes.length > 0
|
: lineVnodes.length > 0
|
||||||
|
|||||||
@@ -44,30 +44,38 @@ function addFwdModal(zones, state) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function load(state) {
|
async function load(state, abortController, entry) {
|
||||||
|
if (Object.keys(state.config || {}).length) state.refreshing = true;
|
||||||
|
else state.loading = true;
|
||||||
try {
|
try {
|
||||||
const r = await apiFetch('/api/firewall/config');
|
const myId = entry ? entry.requestId : 0;
|
||||||
|
const sig = abortController?.signal;
|
||||||
|
const r = await apiFetch('/api/firewall/config', { signal: sig });
|
||||||
|
if (abortController?.signal.aborted || (entry && entry.requestId !== myId)) return;
|
||||||
if (r.ok) state.config = r.data || {};
|
if (r.ok) state.config = r.data || {};
|
||||||
const zr = await apiFetch('/api/firewall/zones');
|
const zr = await apiFetch('/api/firewall/zones', { signal: sig });
|
||||||
|
if (abortController?.signal.aborted || (entry && entry.requestId !== myId)) return;
|
||||||
if (zr.ok) state.activeZones = Object.keys(zr.data?.active || {});
|
if (zr.ok) state.activeZones = Object.keys(zr.data?.active || {});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (abortController?.signal.aborted) return;
|
||||||
state.error = String(e);
|
state.error = String(e);
|
||||||
}
|
}
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
|
state.refreshing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default definePage({
|
export default definePage({
|
||||||
init() {
|
init() {
|
||||||
return { config: {}, activeZones: [], loading: true, error: null };
|
return { config: {}, activeZones: [], loading: true, refreshing: false, error: null };
|
||||||
},
|
},
|
||||||
subscribe: ['firewall'],
|
subscribe: ['firewall'],
|
||||||
load,
|
load,
|
||||||
render(state) {
|
render(state) {
|
||||||
if (state.loading) {
|
if (state.loading && !state.refreshing) {
|
||||||
return [
|
return [
|
||||||
PageHeader({ title: 'NAT', subtitle: 'Masquerade & port forwarding' }),
|
PageHeader({ title: 'NAT', subtitle: 'Masquerade & port forwarding' }),
|
||||||
h('div', { class: 'card', key: 'loading' },
|
h('div', { class: 'card', key: 'loading' },
|
||||||
h('div', { class: 'card-body loading' }, 'Loading...'),
|
h('div', { class: 'card-body loading' }, state.refreshing ? 'Refreshing...' : 'Loading...'),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,30 +82,38 @@ function editDomainModal(domain, state) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function load(state) {
|
async function load(state, abortController, entry) {
|
||||||
|
if (state.domains?.length) state.refreshing = true;
|
||||||
|
else state.loading = true;
|
||||||
try {
|
try {
|
||||||
const domainsR = await apiFetch('/api/proxy/domains');
|
const myId = entry ? entry.requestId : 0;
|
||||||
|
const sig = abortController?.signal;
|
||||||
|
const domainsR = await apiFetch('/api/proxy/domains', { signal: sig });
|
||||||
|
if (abortController?.signal.aborted || (entry && entry.requestId !== myId)) return;
|
||||||
if (domainsR.ok) state.domains = domainsR.data || [];
|
if (domainsR.ok) state.domains = domainsR.data || [];
|
||||||
const certsR = await apiFetch('/api/certs/list');
|
const certsR = await apiFetch('/api/certs/list', { signal: sig });
|
||||||
|
if (abortController?.signal.aborted || (entry && entry.requestId !== myId)) return;
|
||||||
if (certsR.ok) state.certs = certsR.data || [];
|
if (certsR.ok) state.certs = certsR.data || [];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (abortController?.signal.aborted) return;
|
||||||
state.error = String(e);
|
state.error = String(e);
|
||||||
}
|
}
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
|
state.refreshing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default definePage({
|
export default definePage({
|
||||||
init() {
|
init() {
|
||||||
return { domains: [], certs: [], loading: true, error: null };
|
return { domains: [], certs: [], loading: true, refreshing: false, error: null };
|
||||||
},
|
},
|
||||||
subscribe: ['nginx', 'acme'],
|
subscribe: ['nginx', 'acme'],
|
||||||
load,
|
load,
|
||||||
render(state) {
|
render(state) {
|
||||||
if (state.loading) {
|
if (state.loading && !state.refreshing) {
|
||||||
return [
|
return [
|
||||||
PageHeader({ title: 'Proxy' }),
|
PageHeader({ title: 'Proxy' }),
|
||||||
h('div', { class: 'card', key: 'loading' },
|
h('div', { class: 'card', key: 'loading' },
|
||||||
h('div', { class: 'card-body loading' }, 'Loading...'),
|
h('div', { class: 'card-body loading' }, state.refreshing ? 'Refreshing...' : 'Loading...'),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,31 +33,39 @@ function addRuleModal(zones, state) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function load(state) {
|
async function load(state, abortController, entry) {
|
||||||
|
if (Object.keys(state.config || {}).length) state.refreshing = true;
|
||||||
|
else state.loading = true;
|
||||||
try {
|
try {
|
||||||
const r = await apiFetch('/api/firewall/config');
|
const myId = entry ? entry.requestId : 0;
|
||||||
|
const sig = abortController?.signal;
|
||||||
|
const r = await apiFetch('/api/firewall/config', { signal: sig });
|
||||||
|
if (abortController?.signal.aborted || (entry && entry.requestId !== myId)) return;
|
||||||
if (r.ok) state.config = r.data || {};
|
if (r.ok) state.config = r.data || {};
|
||||||
else state.error = r.error;
|
else state.error = r.error;
|
||||||
const zr = await apiFetch('/api/firewall/zones');
|
const zr = await apiFetch('/api/firewall/zones', { signal: sig });
|
||||||
|
if (abortController?.signal.aborted || (entry && entry.requestId !== myId)) return;
|
||||||
if (zr.ok) state.zones = Object.keys(zr.data?.active || {});
|
if (zr.ok) state.zones = Object.keys(zr.data?.active || {});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (abortController?.signal.aborted) return;
|
||||||
state.error = String(e);
|
state.error = String(e);
|
||||||
}
|
}
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
|
state.refreshing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default definePage({
|
export default definePage({
|
||||||
init() {
|
init() {
|
||||||
return { config: {}, loading: true, error: null, zones: [] };
|
return { config: {}, loading: true, refreshing: false, error: null, zones: [] };
|
||||||
},
|
},
|
||||||
subscribe: ['firewall'],
|
subscribe: ['firewall'],
|
||||||
load,
|
load,
|
||||||
render(state) {
|
render(state) {
|
||||||
if (state.loading) {
|
if (state.loading && !state.refreshing) {
|
||||||
return [
|
return [
|
||||||
PageHeader({ title: 'Rules', subtitle: 'Firewall rich rules' }),
|
PageHeader({ title: 'Rules', subtitle: 'Firewall rich rules' }),
|
||||||
h('div', { class: 'card', key: 'loading' },
|
h('div', { class: 'card', key: 'loading' },
|
||||||
h('div', { class: 'card-body loading' }, 'Loading...'),
|
h('div', { class: 'card-body loading' }, state.refreshing ? 'Refreshing...' : 'Loading...'),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,32 +76,41 @@ function downloadConfigModal(peerName, config, state) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function load(state) {
|
async function load(state, abortController, entry) {
|
||||||
|
if (state.peers?.length) state.refreshing = true;
|
||||||
|
else state.loading = true;
|
||||||
try {
|
try {
|
||||||
const stR = await apiFetch('/api/wireguard/status');
|
const myId = entry ? entry.requestId : 0;
|
||||||
|
const sig = abortController?.signal;
|
||||||
|
const stR = await apiFetch('/api/wireguard/status', { signal: sig });
|
||||||
|
if (abortController?.signal.aborted || (entry && entry.requestId !== myId)) return;
|
||||||
if (stR.ok) state.status = stR.data || {};
|
if (stR.ok) state.status = stR.data || {};
|
||||||
const pR = await apiFetch('/api/wireguard/peers');
|
const pR = await apiFetch('/api/wireguard/peers', { signal: sig });
|
||||||
|
if (abortController?.signal.aborted || (entry && entry.requestId !== myId)) return;
|
||||||
if (pR.ok) state.peers = pR.data || [];
|
if (pR.ok) state.peers = pR.data || [];
|
||||||
const cfgR = await apiFetch('/api/wireguard/config');
|
const cfgR = await apiFetch('/api/wireguard/config', { signal: sig });
|
||||||
|
if (abortController?.signal.aborted || (entry && entry.requestId !== myId)) return;
|
||||||
if (cfgR.ok) state.config = cfgR.data || {};
|
if (cfgR.ok) state.config = cfgR.data || {};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (abortController?.signal.aborted) return;
|
||||||
state.error = String(e);
|
state.error = String(e);
|
||||||
}
|
}
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
|
state.refreshing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default definePage({
|
export default definePage({
|
||||||
init() {
|
init() {
|
||||||
return { status: {}, peers: [], config: {}, loading: true, error: null };
|
return { status: {}, peers: [], config: {}, loading: true, refreshing: false, error: null };
|
||||||
},
|
},
|
||||||
subscribe: ['wireguard'],
|
subscribe: ['wireguard'],
|
||||||
load,
|
load,
|
||||||
render(state) {
|
render(state) {
|
||||||
if (state.loading) {
|
if (state.loading && !state.refreshing) {
|
||||||
return [
|
return [
|
||||||
PageHeader({ title: 'WireGuard' }),
|
PageHeader({ title: 'WireGuard' }),
|
||||||
h('div', { class: 'card', key: 'loading' },
|
h('div', { class: 'card', key: 'loading' },
|
||||||
h('div', { class: 'card-body loading' }, 'Loading...'),
|
h('div', { class: 'card-body loading' }, state.refreshing ? 'Refreshing...' : 'Loading...'),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,24 +107,32 @@ function zoneSvcModal(zoneName, state) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function load(state) {
|
async function load(state, abortController, entry) {
|
||||||
|
if (Object.keys(state.zones || {}).length) state.refreshing = true;
|
||||||
|
else state.loading = true;
|
||||||
try {
|
try {
|
||||||
|
const myId = entry ? entry.requestId : 0;
|
||||||
|
const sig = abortController?.signal;
|
||||||
const [zRes, svcRes, ifRes] = await Promise.all([
|
const [zRes, svcRes, ifRes] = await Promise.all([
|
||||||
apiFetch('/api/firewall/zones'),
|
apiFetch('/api/firewall/zones', { signal: sig }),
|
||||||
apiFetch('/api/firewall/services'),
|
apiFetch('/api/firewall/services', { signal: sig }),
|
||||||
apiFetch('/api/firewall/interfaces'),
|
apiFetch('/api/firewall/interfaces', { signal: sig }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (abortController?.signal.aborted || (entry && entry.requestId !== myId)) return;
|
||||||
|
|
||||||
if (zRes.ok) {
|
if (zRes.ok) {
|
||||||
const data = zRes.data || {};
|
const data = zRes.data || {};
|
||||||
const activeZones = data.active || {};
|
const activeZones = data.active || {};
|
||||||
const availableZones = data.available || [];
|
const availableZones = data.available || [];
|
||||||
|
|
||||||
const detailPromises = availableZones.map(name =>
|
const detailPromises = availableZones.map(name =>
|
||||||
apiFetch('/api/firewall/zones/' + enc(name)).catch(() => null)
|
apiFetch('/api/firewall/zones/' + enc(name), { signal: sig }).catch(() => null)
|
||||||
);
|
);
|
||||||
const detailResults = await Promise.all(detailPromises);
|
const detailResults = await Promise.all(detailPromises);
|
||||||
|
|
||||||
|
if (abortController?.signal.aborted || (entry && entry.requestId !== myId)) return;
|
||||||
|
|
||||||
const zones = {};
|
const zones = {};
|
||||||
for (let i = 0; i < availableZones.length; i++) {
|
for (let i = 0; i < availableZones.length; i++) {
|
||||||
const name = availableZones[i];
|
const name = availableZones[i];
|
||||||
@@ -143,23 +151,25 @@ async function load(state) {
|
|||||||
if (svcRes.ok) state.services = svcRes.data || [];
|
if (svcRes.ok) state.services = svcRes.data || [];
|
||||||
if (ifRes.ok) state.interfaces = ifRes.data || [];
|
if (ifRes.ok) state.interfaces = ifRes.data || [];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (abortController?.signal.aborted) return;
|
||||||
state.error = String(e);
|
state.error = String(e);
|
||||||
}
|
}
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
|
state.refreshing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default definePage({
|
export default definePage({
|
||||||
init() {
|
init() {
|
||||||
return { zones: {}, services: [], interfaces: [], loading: true, error: null };
|
return { zones: {}, services: [], interfaces: [], loading: true, refreshing: false, error: null };
|
||||||
},
|
},
|
||||||
subscribe: ['firewall'],
|
subscribe: ['firewall'],
|
||||||
load,
|
load,
|
||||||
render(state) {
|
render(state) {
|
||||||
if (state.loading) {
|
if (state.loading && !state.refreshing) {
|
||||||
return [
|
return [
|
||||||
PageHeader({ title: 'Zones', subtitle: 'Firewall zone management' }),
|
PageHeader({ title: 'Zones', subtitle: 'Firewall zone management' }),
|
||||||
h('div', { class: 'card', key: 'loading' },
|
h('div', { class: 'card', key: 'loading' },
|
||||||
h('div', { class: 'card-body loading' }, 'Loading...'),
|
h('div', { class: 'card-body loading' }, state.refreshing ? 'Refreshing...' : 'Loading...'),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user