332d14e37d
- daemon: send full snapshot on connect; versions/tick now carry the full state of one subsystem (subsystem + data); no legacy updated/subsystems payloads; refresh_state and POST /status/refresh broadcast per-subsystem versions with data - client: modelSet() patches models in place; onMessage/topic refresh retired; 3s initial-load fallback via new POST /api/status/refresh - schema: lib/schema.py TypedDicts + hoover/schema.js defaults + docs/state-model.md as single source of truth for state shapes - system: poll at 1s, volatile metrics registered, dashboard uses a dedicated system model (status model removed) - firewall: refuse to strip both https and ssh from the default zone (409, force override via UI confirm); set_zone_services persists services to the declarative config; collector exposes default_zone - UI: pages migrate to flat state shapes; post-mutation modelFetch refreshes removed (WS delta covers it) - tests: ws snapshot/delta/broadcast, refresh-state, schema types, model-set/js ws handler and reconnect fallback
75 lines
3.1 KiB
JavaScript
75 lines
3.1 KiB
JavaScript
import { html, PageHeader, Empty, Card, ConfirmDelete, Table, renderGuard, esc, enc, $val, apiFetch, toast, definePage, getModel, MonoText, QuickModal } from '/static/hoover/index.js';
|
|
|
|
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: () => ({ zone: $val('rule-zone'), rule: ($val('rule-text') || '').trim() }),
|
|
validate: (b) => !b.zone || !b.rule ? 'Zone and rule are required' : null,
|
|
successMsg: 'Rule added',
|
|
},
|
|
});
|
|
|
|
export default definePage({
|
|
init() {
|
|
return {
|
|
firewall: getModel('firewall'),
|
|
};
|
|
},
|
|
render(state) {
|
|
const guard = renderGuard(state.firewall, 'Rules', 'Firewall rich rules', state.firewall.data?.config);
|
|
if (guard) return guard;
|
|
|
|
const cfg = state.firewall.data?.config || {};
|
|
const zones = Object.keys(state.firewall.data?.zones || {});
|
|
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]) => {
|
|
const ruleRows = (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 html`<tr key=${i}>
|
|
<td class="text-muted">${i + 1}</td>
|
|
<td class="mono-text td-fullwidth"><${MonoText} text=${ruleText} /></td>
|
|
<td>
|
|
<${ConfirmDelete}
|
|
url=${'/api/firewall/rich-rules/' + enc(zone) + '/' + enc(ruleId || '')}
|
|
deleteKey=${zone + '-' + (ruleId || i)}
|
|
message=${'Remove rule: ' + ruleText.substring(0, 40) + '...?'}
|
|
success="Rule removed" />
|
|
</td>
|
|
</tr>`;
|
|
});
|
|
return Card({
|
|
header: 'Zone: ' + esc(zone),
|
|
key: zone,
|
|
children: [Table({
|
|
columns: ['#', 'Rule', 'Action'],
|
|
rows: ruleRows,
|
|
emptyText: 'No rules',
|
|
wrapCard: false,
|
|
})],
|
|
});
|
|
});
|
|
|
|
return [
|
|
PageHeader({
|
|
title: 'Rules',
|
|
subtitle: 'Firewall rich rules',
|
|
actions: html`<button class="btn btn-primary"
|
|
onClick=${() => addRule({ zones })}>Add Rule</button>`,
|
|
}),
|
|
...(cards.length ? cards : [Empty({ text: 'No rich rules configured.' })]),
|
|
];
|
|
},
|
|
});
|