Skip to content

restrictions: add a Family Time Controls Wizard - #147

Merged
ispyisail merged 1 commit into
masterfrom
feat/family-time-controls-wizard
Jul 30, 2026
Merged

restrictions: add a Family Time Controls Wizard#147
ispyisail merged 1 commit into
masterfrom
feat/family-time-controls-wizard

Conversation

@ispyisail

Copy link
Copy Markdown
Owner

Stacked on #145 (base branch is refactor/dedup-wg-client-config, not master) for a clean stack, though this feature itself doesn't depend on the WireGuard dedup work at all, it's independent of #146 too. Fine to retarget to master directly if preferred once #144/#145 land; opened against the same base as the other two wizard PRs to keep the merge order simple.

What this adds

The Restrictions page today is an accurate, general-purpose firewall rule builder (IP/port/protocol/URL matching, ingress vs egress, address types). Nothing about it says "parental controls" to a parent who just wants to block a kid's tablet at bedtime.

This adds a "Quick Setup: Family Time Controls" callout that walks through: pick one or more device groups, pick a time range and days, done. No firewall jargon anywhere in it.

A different reuse shape than #146, and why

#146 (the WireGuard wizard) drove the real add-client modal programmatically, because that form is small (~15 fields). The Restrictions page's own Add Rule form is much bigger (~40 fields: IP tables, ports, protocols, transport type, URL matching), none of which this wizard's scope (group + schedule + always-full-block) ever touches.

So this wizard writes the restriction_rule UCI section directly via uci.set(), a small, explicit subset of that schema, then calls the real, unmodified saveChanges() for persistence. That function updates uciOriginal and calls resetData() itself (this page has no reload, unlike the WireGuard page), so the newly-created rule shows up in the normal table, fully rendered, with no extra work.

The override control needed zero new UI

createReenableCell() (the "30 min / 1 hr / 4 hr / Until Tomorrow" picker) is already unconditionally attached to every restriction_rule row by resetData(). A rule this wizard creates gets it automatically the moment saveChanges() re-renders the table, i.e. as soon as the wizard's own "Save Changes" button is clicked. Verified directly: after running the wizard end to end, the resulting row's reenable cell has real reenable_btn_30m/reenable_btn_tomorrow buttons, not stand-ins.

Multi-group, not one-rule-per-group

Per the RFC's own finding: make_nftables_rules.c (:832, :842) already splits local_addr on commas and emits one @setname match per GROUP: token, with an explicit source comment that named sets must never be combined with {} syntax. So local_addr = "GROUP:Kids,GROUP:Teens" was already the backend's preferred shape; the frontend just never exposed selecting more than one. This wizard writes exactly that comma-joined form for a multi-group selection, one rule, not N rules, verified against the same parsing logic (no whitespace, no braces, each token a bare GROUP: entry).

A harness fix this exposed, not worked around

tests/restrictions/harness.js had no DOM fixture at all before this (it only ever needed to exercise the reenable-cell widget in isolation). Calling the wizard's own saveChanges() end to end meant, for the first time, exercising resetData()'s full render path, which unconditionally depends on every field of the general-purpose Add Rule form existing (same as the real page: those fields are always in the DOM, just not visible). Extended the harness with the full field set (mirrored across rule_/exception_ prefixes, confirmed identical via diff of restriction_template vs whitelist_template) rather than stubbing the real functions away. Also converted restStr/UI to a Proxy fallback (same pattern already used in the WireGuard harness) so an i18n key the wizard doesn't touch reads back as a harmless placeholder instead of crashing table-header rendering with undefined.

Testing

14 new tests:

  • wizard-rule-shape.test.js (6): the created section is a genuine restriction_rule, field-for-field matches a hand-built full-block/group-targeted/scheduled rule, a specific day selection is written (not defaulted to every day), a same-day time range stays a single span, an overnight range correctly splits into two comma-joined ranges, persists correctly through the real saveChanges().
  • wizard-multigroup.test.js (4): single vs multi-group shape, no whitespace/braces, group order preserved.
  • wizard-override.test.js (4): drives the real reenableSetPreset/reenableSetTomorrow/saveChanges() on a wizard-created rule specifically, both override shapes (fixed duration and next-local-midnight), plus re-enabling before save correctly cancels the pending timer.

Full suite: 36/36 (22 pre-existing + 14 new).

page_lint.test.sh: 11 pass, 3 fail. Confirmed pre-existing by running the identical check against master: byte-for-byte the same 3 failures. This page's restriction.sh/restrictions.js filename split (a known, pre-existing convention, not something this PR touches) trips the lint tool's naming assumption.

Full node lane, run on this branch specifically: initially showed 15 additional failures, all in tests/wireguard/wizard-*.test.js, testing #146's openRemoteAccessWizard(), which this branch never had (it's a sibling of #146, not stacked on it, so this checkout's wireguard.js doesn't include that code). Confirmed as a branch-topology artifact, not a regression, by excluding those files: clean, only the same 2 unrelated pre-existing failures (access-dropbear-preserve, vlan/switchinfo-emit), reconfirmed pre-existing on this branch by stashing this entire diff and re-running.

node --check clean, tabs verified, no em dashes, i18n BOM intact.

Not verified

The override's real-world timing: restriction_reenable.sh's cron sweep actually flipping a rule back on at the computed time. Only the UCI-value math (reenable_at lands within the expected window, or at the exact computed next-local-midnight value) is tested here, not the shell script's execution on a real router.

Out of scope

Category-based content filtering (a genuinely different, URL/domain-list-driven feature tracked in docs/dns-threat-blocking-plan.md). Per-app blocking, bandwidth limits, reporting/dashboards. Any new blocking mechanism, if you're expecting nftables rule changes here, there are none.

Compresses picking a device group, setting a block schedule, and saving into
one guided flow. Writes the restriction_rule section directly rather than
driving the ~40-field general-purpose Add Rule form: the wizard's scope
(group + schedule + always-full-block) never touches the IP-table/port/
protocol/URL-matching fields that make up most of that form.

Supports selecting multiple groups in one rule (make_nftables_rules.c already
splits local_addr on commas and emits one @setname match per GROUP: token).
The "give more time" override needs no new UI: createReenableCell already
attaches to every restriction_rule row saveChanges()/resetData() render.
@ispyisail
ispyisail changed the base branch from refactor/dedup-wg-client-config to master July 30, 2026 08:17
@ispyisail
ispyisail merged commit 7c25f5e into master Jul 30, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant