Phase 6: schedules, blocklists, allowlists (editors + effective state)
- Weekly schedule editor per device: a Monday-Sunday grid of blocking windows, including windows that cross midnight. - Blocklist/allowlist editor per device: app (Android package / Windows process) and site (hostname) entries, tagged by the device platform. - Effective desired state: a manual lock, an active schedule window, or neither. While blocked the allowlist is honoured (Spotify stays while other apps are blocked). Published retained with reason schedule/manual. - Boundary-aware republish loop so a window turns on and off on time, and manual lock expiry is cleared promptly. - CONTROL_TIMEZONE (default Australia/Sydney) with embedded tzdata, so wall-clock windows are evaluated in the family zone, not the container. - Every change is written to the append-only audit log (FR-15). Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
75
README.md
75
README.md
@@ -5,8 +5,8 @@ Internet Use Control project (`SYSTEM_MAP.md`). It holds schedules, blocklists,
|
||||
per-device state and usage, and serves the web dashboard. Only it publishes
|
||||
device config.
|
||||
|
||||
This repository currently contains **Phase 3 — MQTT** (AGENTS.md §11), on top of
|
||||
Phases 1 and 2:
|
||||
This repository currently contains **Phase 6 — Schedules, blocklists,
|
||||
allowlists** (AGENTS.md §11), on top of Phases 1–3:
|
||||
|
||||
- Go service with its own image (`Dockerfile`) and compose file.
|
||||
- SQLite schema for the whole data model (§7), applied through versioned
|
||||
@@ -26,9 +26,18 @@ Phases 1 and 2:
|
||||
`restrict/<device>/config`; device status and telemetry are subscribed and
|
||||
written to SQLite; presence comes from the broker's Last Will and Testament.
|
||||
The broker is transport only — SQLite stays the source of truth.
|
||||
- **Weekly schedules**: a per-device grid of blocking windows, Monday to Sunday,
|
||||
including windows that cross midnight. The Control Service computes the
|
||||
*effective* state — manual lock or active window — and republishes it at each
|
||||
window boundary, so a schedule turns on and off with nobody touching it.
|
||||
- **Blocklist / allowlist**: per-device `app` (Android package or Windows
|
||||
process) and `site` (hostname) entries. While a device is blocked, the device
|
||||
blocks every blocklist value **except** the allowlist values (Spotify at
|
||||
night). That allow decision is in the published config.
|
||||
- Every edit is written to the append-only audit log (FR-15).
|
||||
|
||||
Enforcement, schedules, blocklists, the admin API and the kids' clients arrive
|
||||
in later phases. Nothing here touches the router, Pi-hole, or DNS.
|
||||
Enforcement itself (the kids' clients), the admin API, usage screens and the Hub
|
||||
arrive in later phases. Nothing here touches the router, Pi-hole, or DNS.
|
||||
|
||||
## Quick start (on `.13`)
|
||||
|
||||
@@ -44,7 +53,7 @@ docker compose up -d --build
|
||||
|
||||
# 3. Prove it.
|
||||
curl -s http://127.0.0.1:8791/health
|
||||
# {"status":"ok","service":"control-service","version":"0.3.0-phase3",...}
|
||||
# {"status":"ok","service":"control-service","version":"0.4.0-phase6",...}
|
||||
```
|
||||
|
||||
Open <http://127.0.0.1:8791/> in a browser. It redirects to `/login`. Sign in
|
||||
@@ -69,6 +78,7 @@ reads only the process environment.
|
||||
| `CONTROL_ADMIN_PASSWORD_HASH` | — | bcrypt hash (option B, preferred) |
|
||||
| `CONTROL_APP_ENV` | `production` | Label shown in the footer |
|
||||
| `CONTROL_LOG_LEVEL` | `info` | `debug`/`info`/`warn`/`error` |
|
||||
| `CONTROL_TIMEZONE` | `Australia/Sydney` | IANA zone weekly windows are evaluated in |
|
||||
| `CONTROL_MQTT_ENABLED` | `true` | Turn the broker link on or off |
|
||||
| `CONTROL_MQTT_BROKER` | `tcp://host.docker.internal:1883` | Broker URL; `tls://…:8883` once the TLS listener exists |
|
||||
| `CONTROL_MQTT_CLIENT_ID` | `control-service` | MQTT client id |
|
||||
@@ -80,7 +90,7 @@ reads only the process environment.
|
||||
Generate a bcrypt hash without host Go:
|
||||
|
||||
```bash
|
||||
docker run --rm --entrypoint /app/control webrestrict/control:0.3.0-phase3 \
|
||||
docker run --rm --entrypoint /app/control webrestrict/control:0.4.0-phase6 \
|
||||
hash-password 'your-password'
|
||||
```
|
||||
|
||||
@@ -124,13 +134,17 @@ Broker: the shared **Mosquitto on `.13`**. A second broker is not added
|
||||
|
||||
```json
|
||||
{"v":1,"device_id":1,"name":"Harry's phone","platform":"android",
|
||||
"locked":true,"until":"2026-10-13T21:00:00Z","allow":[],"block":[],
|
||||
"generated_at":"2026-10-13T03:30:27Z"}
|
||||
"locked":true,"until":"2026-10-13T21:00:00Z",
|
||||
"allow":["com.spotify.music"],"block":["com.youtube.android","roblox.exe"],
|
||||
"reason":"schedule","generated_at":"2026-10-13T03:30:27Z"}
|
||||
```
|
||||
|
||||
`allow` and `block` are always arrays, never `null`. They are read from the
|
||||
`allowlist` and `blocklist` tables, which get their editors in Phase 6; until
|
||||
then they are empty. A client must ignore unknown fields, so this shape can grow.
|
||||
`allow` and `block` are always arrays, never `null`. `locked` and `until` are the
|
||||
**effective** state: a `reason` of `schedule` means a weekly window is active,
|
||||
`manual` means a lock was set from the dashboard. While `locked` is true the
|
||||
device blocks every `block` value **except** the `allow` values (FR-4), so the
|
||||
allowlist is the exception list. A client must ignore unknown fields, so this
|
||||
shape can grow.
|
||||
|
||||
### Status payload (device → service)
|
||||
|
||||
@@ -174,12 +188,35 @@ docker exec webrestrict-control /app/control mqtt-clients
|
||||
| `POST` | `/devices/{id}` | Updates name, child, platform. |
|
||||
| `POST` | `/devices/{id}/lock` | Locks now; optional `minutes` (0 = until unlocked). |
|
||||
| `POST` | `/devices/{id}/unlock` | Unlocks now. |
|
||||
| `GET` | `/devices/{id}/schedule` | Weekly schedule editor (grid of windows). |
|
||||
| `POST` | `/devices/{id}/schedule` | Replaces the device's windows; `clear=1` clears them. |
|
||||
| `GET` | `/devices/{id}/lists` | Blocklist / allowlist editor. |
|
||||
| `POST` | `/devices/{id}/lists` | Adds one entry (`table=block|allow`, `kind=app|site`, `value`). |
|
||||
| `POST` | `/devices/{id}/lists/{entryID}/delete` | Removes one entry. |
|
||||
|
||||
Every form POST needs the `csrf` field from the session cookie. State changes
|
||||
append to the `audit` table and republish the device's retained config. Devices
|
||||
are never hard-deleted (§10 rule 10).
|
||||
|
||||
Phase 3 adds no HTTP endpoints.
|
||||
## Schedules, blocklists and allowlists (Phase 6)
|
||||
|
||||
A **schedule** is a set of weekly blocking windows per device: a weekday
|
||||
(Monday = 0 … Sunday = 6), a local `HH:MM` start and end, and an end earlier than
|
||||
the start means the window crosses midnight (Monday 20:00–07:00 ends Tuesday
|
||||
07:00). A window means restrictions are **ON** during that time.
|
||||
|
||||
The **effective state** is a manual lock, an active window, or neither. The
|
||||
Control Service computes it and publishes it retained. A manual lock with no end
|
||||
time (“until I unlock”) never lifts; otherwise the later of the manual end and
|
||||
the covering window's end is used. A background loop republishes at the next
|
||||
window boundary, so a window starts and ends on time without a poll.
|
||||
|
||||
A **blocklist** entry is blocked while a window is active or the device is
|
||||
locked. An **allowlist** entry (each entry is `app` or `site`) is the exception
|
||||
list: the device blocks every blocklist value *except* the allowlist. For
|
||||
example, with `com.spotify.music` allowlisted and `com.youtube.android` blocked,
|
||||
Spotify stays available and YouTube does not. Hostnames are lower-cased; app
|
||||
values keep their case.
|
||||
|
||||
## Development
|
||||
|
||||
@@ -206,6 +243,7 @@ cmd/control/ service entry point (+ hash-password, mqtt-clients)
|
||||
internal/config/ environment configuration
|
||||
internal/store/ SQLite open, migrations, audit, devices + state
|
||||
devices.go device registry, desired state, lock expiry
|
||||
schedules.go weekly windows and the effective state
|
||||
reports.go presence and telemetry written from broker reports
|
||||
lists.go allow/block lists and device lookup by topic
|
||||
migrations/001_init.sql, 002_telemetry_unique.sql
|
||||
@@ -215,7 +253,8 @@ internal/mqtt/ the broker link: retained config out, reports in
|
||||
link.go paho client, subscriptions, publish, TLS verification
|
||||
internal/web/ HTTP handlers, templates, static assets
|
||||
devices.go device registry + manual lock/unlock handlers
|
||||
templates/ login, dashboard, device form, HTMX partials
|
||||
schedules.go weekly schedule and block/allow editors
|
||||
templates/ login, dashboard, device form, editors, HTMX partials
|
||||
static/ tokens.css (Family Home Lab), app.css, htmx, Inter fonts
|
||||
Dockerfile own image (multi-stage, CGO-free)
|
||||
docker-compose.yml own container, port 8791
|
||||
@@ -230,12 +269,14 @@ deploy/mosquitto/ TLS listener, credentials, topic ACL, verifier
|
||||
`webrestrict.lab.audasmedia.com.au`. The terminating Caddy runs on `.35`
|
||||
(sam-Ubuntu1); see that file for the two proxy patterns and the apply commands.
|
||||
|
||||
## Non-goals (Phase 3)
|
||||
## Non-goals (Phase 6)
|
||||
|
||||
- No schedules, blocklists or allowlists (Phase 6). The config payload reads
|
||||
those tables, but nothing edits them yet.
|
||||
- No group/global list editing from the UI. The data model supports a NULL
|
||||
`device_id` that applies to every device, and the effective state merges it,
|
||||
but the editor manages per-device rows only.
|
||||
- No usage screens (Phase 7). Telemetry is stored, not yet shown.
|
||||
- No admin API (Phase 8).
|
||||
- No kids' clients (Phases 4–5). The topic and payload contract above is what
|
||||
they implement.
|
||||
they implement. The clients apply the allowlist exception; the Control Service
|
||||
only publishes it.
|
||||
- No Hub contact (Phase 9).
|
||||
|
||||
Reference in New Issue
Block a user