import { h, PageHeader, Empty, Card, ConfirmDelete, Table, renderGuard, esc, enc, $val, apiFetch, toast, definePage, refactorLoad, MonoText, QuickModal } from '/static/hoover/index.js?v=6'; const addRule = QuickModal({ title: 'Add Rich Rule', fields: (d) => [ { label: 'Zone', id: 'rule-zone', tag: 'select', options: d.zones }, { label: 'Rule (XML)', id: 'rule-text', tag: 'textarea', placeholder: 'rule family="ipv4" source address="192.168.1.0/24" accept' }, ], submit: { url: '/api/firewall/rich-rules', body: (s) => ({ zone: $val('rule-zone'), rule: ($val('rule-text') || '').trim() }), validate: (b) => !b.zone || !b.rule ? 'Zone and rule are required' : null, successMsg: 'Rule added', }, reload: (s) => load(s._s), }); async function load(state, abortController, entry) { await refactorLoad(state, s => Object.keys(s.config || {}).length, async (s, sig, isAborted) => { const r = await apiFetch('/api/firewall/config', { signal: sig }); if (isAborted()) return; if (r.ok) s.config = r.data || {}; else s.error = r.error; const zr = await apiFetch('/api/firewall/zones', { signal: sig }); if (isAborted()) return; if (zr.ok) s.zones = Object.keys(zr.data?.active || {}); else if (!s.error) s.error = zr.error; }, { entry, abortController }, ); } export default definePage({ init() { return { config: {}, zones: [] }; }, subscribe: ['firewall'], load, render(state) { const guard = renderGuard(state, 'Rules', 'Firewall rich rules', state.config); if (guard) return guard; 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 Card({ header: 'Zone: ' + esc(zone), key: zone, children: [Table({ columns: ['#', 'Rule', 'Action'], rows: (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', { class: 'mono-text td-fullwidth' }, MonoText({ text: ruleText })), h('td', null, ConfirmDelete({ url: '/api/firewall/rich-rules/' + enc(zone) + '/' + enc(ruleId || ''), message: 'Remove rule: ' + ruleText.substring(0, 40) + '...?', success: 'Rule removed', reload: () => load(state), }), ), ); }), emptyText: 'No rules', wrapCard: false, })], }); }); return [ PageHeader({ title: 'Rules', subtitle: 'Firewall rich rules', actions: h('button', { class: 'btn btn-primary', 'on:click': () => addRule({ zones: state.zones, _s: state }), }, 'Add Rule'), }), ...(cards.length ? cards : [Empty({ text: 'No rich rules configured.' })]), ]; }, });