import { h, PageHeader, Badge, StatusDot, Empty, Card, esc, enc, att_esc, $val, apiFetch, toast, dismissToast, openModal, closeModal, formModal, definePage, hComp, connect, reactive, requestUpdate, Link, createRouter, ToastContainer, render, parseZones } from '/static/hoover/index.js'; function addRuleModal(zones, state) { openModal((inner, idx) => { formModal(inner, 'Add Rich Rule', [ { label: 'Zone', id: 'rule-zone', tag: 'select', options: zones.map(z => [z, z === zones[0]]) }, { label: 'Rule (XML)', id: 'rule-text', tag: 'textarea', placeholder: 'rule family="ipv4" source address="192.168.1.0/24" accept' }, ], [ { label: 'Cancel', cls: 'btn-outline', action: 'c', handler: () => closeModal(idx) }, { label: 'Add', cls: 'btn-primary', action: 's', handler: async () => { const zone = $val('rule-zone'); const rule = ($val('rule-text') || '').trim(); if (!zone || !rule) { toast('Zone and rule are required', 'error'); return; } const r = await apiFetch('/api/firewall/rich-rules', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ zone, rule }), }); if (r.ok) { toast('Rule added', 'success'); closeModal(idx); await load(state); } else { toast(r.error || 'Failed', 'error'); } }, }, ], ); }); } async function load(state, abortController, entry) { if (Object.keys(state.config || {}).length) state.refreshing = true; else state.loading = true; try { 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 || {}; else state.error = r.error; 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 || {}); } catch (e) { if (abortController?.signal.aborted) return; state.error = String(e); } state.loading = false; state.refreshing = false; } export default definePage({ init() { return { config: {}, loading: true, refreshing: false, error: null, zones: [] }; }, subscribe: ['firewall'], load, render(state) { if (state.loading && !state.refreshing) { return [ PageHeader({ title: 'Rules', subtitle: 'Firewall rich rules' }), h('div', { class: 'card', key: 'loading' }, h('div', { class: 'card-body loading' }, state.refreshing ? 'Refreshing...' : 'Loading...'), ), ]; } if (state.error) { return [ PageHeader({ title: 'Rules' }), h('div', { class: 'card', key: 'error' }, h('div', { class: 'card-body error-msg' }, state.error), ), ]; } const cfg = state.config || {}; const zoneData = cfg.zones || {}; const zoneRules = {}; Object.entries(zoneData).forEach(([zname, zcfg]) => { const rr = zcfg.rich_rules || []; if (rr.length) zoneRules[zname] = rr; }); const cards = Object.entries(zoneRules).map(([zone, rules]) => { return h('div', { class: 'card', key: zone }, h('div', { class: 'card-header' }, 'Zone: ' + esc(zone)), h('div', { class: 'card-body' }, h('table', { class: 'table' }, h('thead', null, h('tr', null, h('th', null, '#'), h('th', null, 'Rule'), h('th', { style: 'width:80px;' }, 'Action'), ), ), h('tbody', null, (Array.isArray(rules) ? rules : []).map((entry, i) => { const ruleId = typeof entry === 'object' ? entry.id : null; const ruleText = typeof entry === 'object' ? (entry.rule || '') : String(entry); return h('tr', { key: i }, h('td', { class: 'text-muted' }, i + 1), h('td', { style: 'font-family:monospace;font-size:12px;word-break:break-all;' }, esc(ruleText)), h('td', null, h('button', { class: 'btn btn-sm btn-danger', 'on:click': async () => { if (!confirm('Remove rule: ' + ruleText.substring(0, 40) + '...?')) return; const r = await apiFetch('/api/firewall/rich-rules/' + enc(zone) + '/' + enc(ruleId || ''), { method: 'DELETE' }); if (r.ok) { toast('Rule removed', 'success'); await load(state); } else { toast(r.error || 'Failed', 'error'); } }}, 'Remove'), ), ); }), ), ), ), ); }); return [ PageHeader({ title: 'Rules', subtitle: 'Firewall rich rules', actions: h('button', { class: 'btn btn-primary', 'on:click': () => addRuleModal(state.zones, state) }, 'Add Rule'), }), ...(cards.length ? cards : [Empty({ text: 'No rich rules configured.' })]), ]; }, });