firewall: interface-coverage apply guard, target drift, non-destructive DHCP sync
Post-DHCP-incident hardening per HARDEN.md.
- apply guard: refuse (ConflictError, `force` overrides) when a
network-managed interface would end up in no zone; absent
`interfaces` key = hands-off, explicit `[]` = unassign-all
- surface `uncovered_interfaces` in firewall state (lo/wg* filtered)
+ advisory in /api/status/pending; zones.js banner + interfaces-picker
last-zone confirm
- target drift (Option A): absent or default-normalizing target is
unmanaged: not diffed, never re-set by apply; create_zone runs
--new-zone first and sets non-default targets only; importer omits
the target key for default zones
- FirewallToDhcpSync keeps stale DHCP ranges and flags them instead of
deleting; `dnsmasq` affected only on a real gateway mutation
- real pre-apply recovery snapshot in data/firewall/rules.json
({timestamp, default_zone, zones, config}); drop the empty post-apply
skeleton
- daemon shutdown: bounded grace for in-flight tasks + suppressed
teardown exception noise on SIGTERM
- also carries the firewall service-descriptions feature
(get_service_descriptions + service_descriptions state field + UI)
- tests + docs across firewall/status/state/sync/schema; ruff clean,
867 passing
This commit is contained in:
@@ -1292,6 +1292,12 @@ h('button', { 'on:click': () => addZone(state) }, 'Add Zone')
|
||||
|
||||
Factory that returns a function to open a multi-select modal. Use as an `on:click` handler in VNode props.
|
||||
|
||||
The picker is a scrollable, **filtered checkbox list** (not a native
|
||||
`<select multiple>`): options are sorted, a live search box filters rows in
|
||||
place (shown when there are more than 8 options; typing does not re-render
|
||||
the modal, so focus is preserved), a counter shows `N of M selected`, and
|
||||
**Select all** / **Clear** act on the currently visible rows.
|
||||
|
||||
```javascript
|
||||
const editIface = MultiSelectModal({
|
||||
title: 'Interfaces: ' + zoneName,
|
||||
@@ -1315,9 +1321,16 @@ h('button', { 'on:click': editIface }, 'Edit')
|
||||
| `options` | All selectable options (`string[]`) |
|
||||
| `selected` | Currently selected values (`string[]`) |
|
||||
| `fieldKey` | JSON key for the submitted field |
|
||||
| `descriptions` | Optional `{option: description}` map; renders a muted one-line description under each row |
|
||||
| `common` | Optional `string[]`. When set, an advanced toggle appears: cleared (default) the list shows common options plus anything currently selected; checked it shows every option |
|
||||
| `successMsg` | Success toast message (default: `'Updated'`) |
|
||||
| `confirm` | `(body) => string \| null` confirm gate — see `apiSubmit` |
|
||||
| `refresh` | **Legacy — accepted but ignored.** State models are updated by the WS delta after success. |
|
||||
|
||||
Selection, the search query, and the advanced flag are held in a closure per
|
||||
open call, so `refreshModals()` re-renders (e.g. the processing spinner)
|
||||
re-apply the current state instead of losing it.
|
||||
|
||||
### Toast
|
||||
|
||||
#### `ToastContainer()`
|
||||
|
||||
Reference in New Issue
Block a user