diff --git a/AGENTS.md b/AGENTS.md index 7309d11..4b11c4f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -189,8 +189,9 @@ The Hub interface is frozen in `central_ai_llm_master/docs/ask-contract.md`. ## 11. Build order -**STATUS — 8 of 10 done (checked 2026-10-14).** Keep this list current: tick a box -when its DoD passes. **The project is DONE only when every box is ticked.** +**STATUS — 9 of 10 done — only Phase 4 outstanding (checked 2026-10-14).** Keep +this list current: tick a box when its DoD passes. **The project is DONE only when +every box is ticked.** - [x] Phase 1 — Skeleton - [x] Phase 2 — Devices and state @@ -201,7 +202,7 @@ when its DoD passes. **The project is DONE only when every box is ticked.** - [x] Phase 7 — Telemetry and usage - [x] Phase 8 — Overrides - [x] Phase 9 — Hub and alerts -- [ ] **Phase 10 — Documentation** ← REMAINING +- [x] Phase 10 — Documentation **PROCESS RULE:** at project start, create one Paperclip issue per phase, status `backlog`, assigned to the agent. Close each as its DoD passes. Do **not** create diff --git a/README.md b/README.md index 711630b..f394d83 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,13 @@ 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 9 — Hub and alerts** (AGENTS.md §11), -on top of Phases 1–8: +**Project status: 9 of 10 phases done.** Phases 1–3 and 5–10 are complete (Phase 10, +this documentation set, included). **Phase 4 — the Windows computer client — is +deliberately deferred and is NOT built.** There is no Python computer client and +no Windows process blocking in this repository. The only enforcement client is +the kids' Android app in `android/`. See `AGENTS.md` §11. + +This repository contains **Phases 1–3 and 5–10** (AGENTS.md §11): - Go service with its own image (`Dockerfile`) and compose file. - SQLite schema for the whole data model (§7), applied through versioned @@ -53,8 +58,54 @@ on top of Phases 1–8: applied) or has been offline too long. Every alert is recorded in `alerts` and the audit log, with its delivery outcome. -Enforcement itself (the kids' clients) arrives in later phases. Nothing here -touches the router, Pi-hole, or DNS. +Nothing here touches the router, Pi-hole, or DNS. Enforcement is device-side and +kids-only. + +## Architecture + +![Internet Use Control architecture](docs/diagrams/architecture.png) + +```mermaid +flowchart LR + SAM["Sam
browser dashboard"] + ADMIN["Admin app
kotlin_phone_app_admin"] + HUB["Hub :8788
read-only /ask"] + CADDY["Caddy on .35
webrestrict.lab.audasmedia.com.au"] + CS["Control Service
Go + SQLite :8791"] + APPRISE["Apprise :8210
alerts + tamper reports"] + + subgraph MQ["Mosquitto on .13 - two planes"] + TLS["8883 TLS
per-device creds + topic ACLs"] + PLAIN["1883 plain + anonymous"] + end + + PHONE["Kids' Android client
Kotlin, Accessibility + overlay"] + + SAM -->|HTTPS| CADDY + CADDY --> CS + ADMIN -->|"Bearer token /api/v1"| CS + HUB -->|"Bearer token /ask"| CS + CS -->|"retained config"| TLS + TLS -->|"own config topic"| PHONE + PHONE -->|"status + telemetry"| TLS + TLS -->|reports| CS + CS --> APPRISE +``` + +**Interactive map:** +(Archify, 9/9 showcase checks). The Mermaid source is `docs/architecture.mmd`, +the rendered image is `docs/diagrams/architecture.png`, and the Archify +specification is `docs/archify/architecture.architecture.json`. + +## Documentation set + +| Layer | Artefact | Audience | +|---|---|---| +| Gitea | this `README.md` | developers | +| Obsidian | portfolio note in `010 inbox/` on `.27`, filed by the sorter | humans | +| Mermaid + Archify | `docs/architecture.mmd`, `docs/diagrams/architecture.png`, `docs/architecture-map.html` | architects | +| Vikunja | project **Internet Use Control** (id 20), copy in `docs/vikunja-tasks.json` | humans | +| Outline | `docs/outline/00-index.md` … `04-agent-runbook.md` (Simplified Technical English) | AI agents | ## Quick start (on `.13`) @@ -133,7 +184,24 @@ with per-device credentials and a topic ACL ## MQTT (Phase 3) Broker: the shared **Mosquitto on `.13`**. A second broker is not added -(§10 rule 6). +(§10 rule 6). **The broker has two planes. They are separate listeners on the +same container, and only one of them is trusted.** + +| Plane | Port | Security | Who uses it | +|---|---|---|---| +| **Plain** | **1883** | Plain text, **anonymous** | Legacy and other house consumers. Unchanged, and not used by this project. | +| **TLS** | **8883** | **TLS**, per-device credentials, topic ACLs | The Control Service and every kid device. | + +On the 8883 plane a device may read **only** its own +`restrict//config` topic, and write **only** its own +`restrict//status` and `restrict//telemetry` topics. The Control +Service is the only identity that may publish config. Without that ACL a child +could publish a fake "unlocked". The listener, the password file and the ACL are +in `deploy/mosquitto/conf.d/webrestrict.conf` and `webrestrict.acl`; +`deploy/mosquitto/README.md` holds the design, the live apply and the rollback. + +Caveat to carry forward: `per_listener_settings true` is deprecated in Mosquitto +2.1 and is removed in 3.0. Pin the broker image tag before an upgrade. | Topic | Direction | Retained | Payload | |---|---|---|---| @@ -480,6 +548,14 @@ while the device is locked (FR-16). There is **no Device Owner**, no factory reset and no managed profile; the child can disable it, and the enforcement is the tamper alert (FR-17). +- **Telemetry** comes from **Usage Access** (`UsageCollector.kt`), reported per + app and per day over the device's own telemetry topic. +- **Site filtering** uses a device-local **`VpnService`** (`SiteFilterVpnService.kt`, + `SiteFilter.kt`, `DnsMessage.kt`, `IpPacket.kt`). No router, no Pi-hole, no + change to the house DNS (FR-18). +- **The block decision** is in `BlockEngine.kt`, from the config payload in + `DeviceConfig.kt`. + A tamper report rides the device's own status topic, because that is the only topic its broker ACL may write: @@ -533,6 +609,12 @@ deploy/caddy/ Caddy vhost for webrestrict.lab.audasmedia.com.au deploy/mosquitto/ TLS listener, credentials, topic ACL, verifier deploy/verify-phase8.sh lock latency proof deploy/verify-phase9.sh Hub + alerts proof +docs/architecture.mmd Mermaid source for the diagram in this README +docs/diagrams/ rendered Mermaid PNG +docs/archify/ Archify specification for the interactive map +docs/architecture-map.html interactive Archify map (published to the maps site) +docs/outline/ AI-context collection (Simplified Technical English) +docs/vikunja-tasks.json copy of the Vikunja board tasks ``` ## Caddy @@ -541,13 +623,16 @@ deploy/verify-phase9.sh Hub + alerts proof `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 9) +## Non-goals (Phase 10) +- **Phase 4, the Windows computer client, is not built.** It is deliberate. The + Python client, process blocking, and the Windows overlay warning do not exist. + The project stands at **9 of 10** because of this single gap. - No grant button in the dashboard. Grant is on the admin API; the dashboard has lock and unlock. Adding the button is a later UI change. - No per-child or per-device keycode. One code, every device (§10 rule 3). -- No HTTP telemetry ingest and no kids' clients (Phases 4–5). The topic and - payload contract above is what they implement. +- No HTTP telemetry ingest. Telemetry arrives over MQTT only. The kids' phone + client exists (Phase 5). The kids' computer client does not (Phase 4, deferred). - No Alerts screen in the dashboard yet. The alert history is in `alerts` and is readable with `control alerts`; the screen is a later UI change. - The Hub is wired to this service: the running Hub on `.13:8788` routes diff --git a/docs/archify/architecture.architecture.json b/docs/archify/architecture.architecture.json new file mode 100644 index 0000000..1939eaa --- /dev/null +++ b/docs/archify/architecture.architecture.json @@ -0,0 +1,290 @@ +{ + "schema_version": 1, + "diagram_type": "architecture", + "meta": { + "title": "Internet Use Control", + "output": "architecture-map.html", + "quality_profile": "showcase", + "views": [ + { + "id": "control-path", + "label": "Control path", + "focus": [ + "sam", + "caddy", + "control", + "broker", + "phone" + ], + "note": "A change in the dashboard reaches the phone through retained MQTT config." + }, + { + "id": "admin-and-hub", + "label": "Admin app and Hub", + "focus": [ + "adminapp", + "hub", + "control" + ], + "note": "Both call the Control Service over HTTP with a bearer token." + }, + { + "id": "two-planes", + "label": "Two MQTT planes", + "focus": [ + "broker", + "legacy", + "control", + "phone" + ], + "note": "8883 is TLS with ACLs. 1883 stays plain for legacy consumers." + } + ] + }, + "components": [ + { + "id": "sam", + "type": "external", + "label": "Sam", + "sublabel": "browser dashboard", + "pos": [ + 40, + 300 + ], + "size": [ + 120, + 60 + ] + }, + { + "id": "caddy", + "type": "security", + "label": "Caddy (.35)", + "sublabel": "TLS + vhost", + "pos": [ + 240, + 300 + ], + "size": [ + 130, + 60 + ], + "tag": "webrestrict.lab" + }, + { + "id": "control", + "type": "backend", + "label": "Control Service", + "sublabel": "Go + SQLite :8791", + "pos": [ + 450, + 300 + ], + "size": [ + 140, + 60 + ], + "tag": "source of truth" + }, + { + "id": "broker", + "type": "messagebus", + "label": "Mosquitto (.13)", + "sublabel": "8883 TLS + topic ACLs", + "pos": [ + 670, + 300 + ], + "size": [ + 150, + 60 + ], + "tag": "two planes" + }, + { + "id": "phone", + "type": "frontend", + "label": "Kids' Android client", + "sublabel": "Kotlin, Accessibility + overlay", + "pos": [ + 900, + 300 + ], + "size": [ + 150, + 60 + ] + }, + { + "id": "adminapp", + "type": "external", + "label": "Admin app", + "sublabel": "kotlin_phone_app_admin", + "pos": [ + 450, + 130 + ], + "size": [ + 140, + 60 + ] + }, + { + "id": "hub", + "type": "external", + "label": "Hub", + "sublabel": "read-only /ask :8788", + "pos": [ + 450, + 470 + ], + "size": [ + 140, + 60 + ] + }, + { + "id": "apprise", + "type": "cloud", + "label": "Apprise (.35)", + "sublabel": "alerts :8210", + "pos": [ + 670, + 480 + ], + "size": [ + 140, + 60 + ] + }, + { + "id": "legacy", + "type": "external", + "label": "Legacy consumers", + "sublabel": "1883 plain + anonymous", + "pos": [ + 900, + 480 + ], + "size": [ + 150, + 60 + ] + } + ], + "boundaries": [ + { + "kind": "region", + "label": "Home lab - Docker on .13, Caddy and Apprise on .35", + "wraps": [ + "caddy", + "control", + "broker", + "apprise" + ] + } + ], + "connections": [ + { + "id": "sam-to-caddy", + "from": "sam", + "to": "caddy", + "label": "HTTPS", + "variant": "emphasis" + }, + { + "id": "caddy-to-control", + "from": "caddy", + "to": "control" + }, + { + "id": "admin-to-control", + "from": "adminapp", + "to": "control", + "label": "Bearer token /api/v1", + "variant": "dashed", + "labelDy": 24 + }, + { + "id": "hub-to-control", + "from": "hub", + "to": "control", + "label": "Bearer token /ask", + "variant": "dashed" + }, + { + "id": "control-to-broker", + "from": "control", + "to": "broker", + "label": "retained config" + }, + { + "id": "broker-to-phone", + "from": "broker", + "to": "phone", + "label": "own config topic", + "labelDy": -22 + }, + { + "id": "phone-to-broker", + "from": "phone", + "to": "broker", + "label": "status + telemetry", + "variant": "dashed", + "fromSide": "top", + "toSide": "top", + "via": [ + [ + 975, + 240 + ], + [ + 745, + 240 + ] + ] + }, + { + "id": "control-to-apprise", + "from": "control", + "to": "apprise", + "label": "alerts + tamper" + }, + { + "id": "broker-to-legacy", + "from": "broker", + "to": "legacy", + "label": "1883 plain", + "variant": "dashed" + } + ], + "cards": [ + { + "dot": "cyan", + "title": "Control Service", + "items": [ + "Go and SQLite are the single source of truth", + "Publishes each device config retained", + "Serves dashboard, admin API and Hub API" + ] + }, + { + "dot": "amber", + "title": "MQTT two planes", + "items": [ + "8883 is TLS with per-device credentials and ACLs", + "A device reads only its own config topic", + "1883 stays plain and anonymous for legacy use" + ] + }, + { + "dot": "rose", + "title": "Enforcement and gap", + "items": [ + "Accessibility overlay blocks by interruption", + "Tamper alert is the real enforcement, not Device Owner", + "Phase 4 Windows client is deferred and not built" + ] + } + ] +} diff --git a/docs/architecture-map.html b/docs/architecture-map.html new file mode 100644 index 0000000..f2d9c08 --- /dev/null +++ b/docs/architecture-map.html @@ -0,0 +1,13703 @@ + + + + + + + Internet Use Control Diagram + + + + + + + + + + + +
+ +
+
+
+

Internet Use Control

+
+
+ + + + + + +
+ + Internet Use Control + A architecture diagram generated by Archify. + + + + + + + + + + + + + + + + + + + + + + + + + Home lab - Docker on .13, Caddy and Apprise on .35 + + + + + + + + + + + + + + + Sam · browser dashboard · Architecture component + + + + Sam + browser dashboard + + + + Caddy (.35) · TLS + vhost · Home lab - Docker on .13, Caddy and Apprise on .35 · webrestrict.lab + + + + Caddy (.35) + TLS + vhost + webrestrict.lab + + + + Control Service · Go + SQLite :8791 · Home lab - Docker on .13, Caddy and Apprise on .35 · source of truth + + + + Control Service + Go + SQLite :8791 + source of truth + + + + Mosquitto (.13) · 8883 TLS + topic ACLs · Home lab - Docker on .13, Caddy and Apprise on .35 · two planes + + + + Mosquitto (.13) + 8883 TLS + topic ACLs + two planes + + + + Kids' Android client · Kotlin, Accessibility + overlay · Architecture component + + + + Kids' Android client + Kotlin, Accessibility + overlay + + + + Admin app · kotlin_phone_app_admin · Architecture component + + + + Admin app + kotlin_phone_app_admin + + + + Hub · read-only /ask :8788 · Architecture component + + + + Hub + read-only /ask :8788 + + + + Apprise (.35) · alerts :8210 · Home lab - Docker on .13, Caddy and Apprise on .35 + + + + Apprise (.35) + alerts :8210 + + + + Legacy consumers · 1883 plain + anonymous · Architecture component + + + + Legacy consumers + 1883 plain + anonymous + + + + + + HTTPS + + + + + Bearer token /api/v1 + + + + Bearer token /ask + + + + retained config + + + + own config topic + + + + status + telemetry + + + + alerts + tamper + + + + 1883 plain + + + + + Legend + + + Frontend + + + + Backend + + + + Cloud + + + + Security + + + + Message bus + + + + External + + + +

+ + + + + + + + +
+ + +
+
+
+
+

Control Service

+
+
    +
  • • Go and SQLite are the single source of truth
  • +
  • • Publishes each device config retained
  • +
  • • Serves dashboard, admin API and Hub API
  • +
+
+ +
+
+
+

MQTT two planes

+
+
    +
  • • 8883 is TLS with per-device credentials and ACLs
  • +
  • • A device reads only its own config topic
  • +
  • • 1883 stays plain and anonymous for legacy use
  • +
+
+ +
+
+
+

Enforcement and gap

+
+
    +
  • • Accessibility overlay blocks by interruption
  • +
  • • Tamper alert is the real enforcement, not Device Owner
  • +
  • • Phase 4 Windows client is deferred and not built
  • +
+
+
+ +
+ + + + diff --git a/docs/architecture.mmd b/docs/architecture.mmd new file mode 100644 index 0000000..84defac --- /dev/null +++ b/docs/architecture.mmd @@ -0,0 +1,29 @@ +flowchart LR + SAM["Sam
browser dashboard"] + ADMIN["Admin app
kotlin_phone_app_admin"] + HUB["Hub :8788
read-only /ask"] + CADDY["Caddy on .35
webrestrict.lab.audasmedia.com.au"] + CS["Control Service
Go + SQLite :8791"] + APPRISE["Apprise :8210
alerts + tamper reports"] + + subgraph MQ["Mosquitto on .13 - two planes"] + TLS["8883 TLS
per-device creds + topic ACLs"] + PLAIN["1883 plain + anonymous"] + end + + PHONE["Kids' Android client
Kotlin, Accessibility + overlay"] + OTHER["Other house consumers"] + + SAM -->|HTTPS| CADDY + CADDY --> CS + ADMIN -->|"Bearer token /api/v1"| CS + HUB -->|"Bearer token /ask"| CS + CS -->|"retained config"| TLS + TLS -->|"own config topic"| PHONE + PHONE -->|"status + telemetry"| TLS + TLS -->|reports| CS + CS --> APPRISE + PLAIN -.->|legacy| OTHER + + click CS "https://maps.lab.audasmedia.com.au/internet_use_control/docs/" "View Archify map" + click TLS "https://maps.lab.audasmedia.com.au/internet_use_control/docs/" "View Archify map" diff --git a/docs/diagrams/architecture.png b/docs/diagrams/architecture.png new file mode 100644 index 0000000..a4419c7 Binary files /dev/null and b/docs/diagrams/architecture.png differ diff --git a/docs/outline/00-index.md b/docs/outline/00-index.md new file mode 100644 index 0000000..64052cf --- /dev/null +++ b/docs/outline/00-index.md @@ -0,0 +1,47 @@ +# Internet Use Control — AI Document Index + +> **Target**: AI agents. +> **Writing style**: ASD-STE100 Simplified Technical English (`ste-writing`). + +## Document Catalog (inject the exact file) + +Read this index. Then inject only the file that you need. Do not search all files. + +| Document Title | Path | Scope / Description | +| :--- | :--- | :--- | +| **System Overview** | `docs/outline/01-system-overview.md` | States the purpose, the components, the MQTT planes and the missing Phase 4 client. | +| **Configuration and Env** | `docs/outline/02-config-and-env.md` | Lists the environment variables, the config files, the ports and the secrets. | +| **Code Map** | `docs/outline/03-code-map.md` | Shows the folder tree, the entry points and the Go dependencies. | +| **Agent Runbook** | `docs/outline/04-agent-runbook.md` | Gives the commands to build, test, deploy, verify and resume work. | + +## Source Documents (read before the outline) + +| Document | Path | Scope | +| :--- | :--- | :--- | +| **Build spec** | `AGENTS.md` | The phases and the definition of done. Section 11 holds the phase checklist. | +| **System map** | `SYSTEM_MAP.md` | Names every component and states where the truth lives. | +| **Transport design** | `TRANSPORT_DESIGN.md` | The MQTT topics, the payloads, the security rules and the fail-safe rule. | +| **README** | `README.md` | The technical reference. Setup, endpoints, payloads and verification. | + +## System Summary + +The Control Service limits the screen use of two children. It holds weekly +schedules, blocklists, allowlists and usage in SQLite. It publishes the desired +state of each device to MQTT with the retained flag. It serves an HTMX dashboard, +a versioned admin API and a read-only Hub API. + +## Core Configuration + +- **Env file**: `environment.d/control-service.env` (gitignored). +- **Template**: `environment.d/control-service.env.example`. +- **Live port**: `8791` on `.13`. +- **Public URL**: `https://webrestrict.lab.audasmedia.com.au`. +- **State**: SQLite at `/data/control.db` inside the container. + +## Agent Restart Steps + +1. Read the Document Catalog above. Inject only the required file. +2. Read `AGENTS.md` section 11. Confirm the phase checklist. +3. Check the tree state: `git status`. +4. Check the recent commits: `git log -n 5 --oneline`. +5. Run the test suite: `make test`. diff --git a/docs/outline/01-system-overview.md b/docs/outline/01-system-overview.md new file mode 100644 index 0000000..11eed4e --- /dev/null +++ b/docs/outline/01-system-overview.md @@ -0,0 +1,97 @@ +# Internet Use Control — System Overview + +> **Target**: AI agents. **Writing style**: ASD-STE100 Simplified Technical English. + +## Purpose + +The project limits the screen use of two children, Harry and Finn. It applies +per-device weekly schedules, blocklists and allowlists. It collects usage. It +lets Sam override a rule in seconds. The work applies to the children's devices +only. No router change, no Pi-hole and no house-wide rule. + +## Components + +| Component | What it is | Where it runs | Language | +| :--- | :--- | :--- | :--- | +| **Control Service** | The brain and the source of truth. Schedules, blocklists, state and usage. | `.13`, Docker, port `8791` | Go + SQLite | +| **Web dashboard** | The admin screen in a browser. HTMX, server-rendered. | Served by the Control Service | HTMX | +| **Broker** | The message bus. Pushes state to devices and collects their reports. | `.13`, Mosquitto | — | +| **Kids' phone client** | Enforces the rules on a phone and shows the blocked state. | The children's phones | Kotlin | +| **Admin app** | The remote override surface. A separate project. | Sam's phone | Kotlin | +| **Hub** | A peer. It asks questions only. It never controls. | `.13`, port `8788` | separate project | + +## Data Flow + +1. Sam changes a rule in the dashboard or the admin app. +2. The Control Service writes the change to SQLite and to the audit log. +3. The Control Service publishes the device config to MQTT, retained. +4. The device receives its own config topic and applies the rule. +5. The device reports status and telemetry on its own topics. +6. The Control Service writes the report to SQLite. + +The Control Service is the only writer of config. Everything else writes to the +Control Service over HTTP. + +## MQTT Has Two Planes + +The shared Mosquitto on `.13` has two listeners. State them correctly. + +| Plane | Port | Security | Use | +| :--- | :--- | :--- | :--- | +| **Plain** | `1883` | Plain text, anonymous | Legacy and other consumers. Unchanged. | +| **TLS** | `8883` | TLS, per-device credentials, topic ACLs | The Control Service and every kid device. | + +On the TLS plane a device reads only its own `restrict//config` topic. It +writes only its own `restrict//status` and +`restrict//telemetry` topics. The Control Service is the only identity +that may publish config. `deploy/mosquitto/` holds the ACL, the accounts and the +verifier. + +One caveat: `per_listener_settings true` is deprecated in Mosquitto 2.1 and is +removed in 3.0. Pin the broker image tag, or rework the two planes. + +## Enforcement Model (the phone client) + +The phone client blocks by **interruption**. It does not suspend an app. + +- An **Accessibility Service** detects the foreground app. +- A full-screen **overlay** covers a blocked app. +- **Usage Access** supplies telemetry. +- A device-local **VpnService** filters sites. + +The client is **not** a Device Owner. There is no factory reset and no managed +profile. A child can disable the client. That is accepted. The enforcement is the +**tamper alert**: when the Accessibility Service stops, the app is force-stopped, +or the overlay permission is revoked, the client reports the event and Sam gets +an alert (FR-17). + +## Fail Safe + +The device keeps its last config when it cannot reach the broker (FR-14). An +outage changes nothing. The device caches its config on disk. One shared admin +keycode overrides the state. + +## What Is Missing (state this plainly) + +**Phase 4, the Windows computer client, is deliberately deferred and NOT built.** +No Python computer client exists in this repository. No Windows process blocking +is implemented or tested. The `docs` and the `README` name this gap on purpose. +Do not describe the computer client as complete. + +## Interfaces + +| Interface | Shape | Consumer | +| :--- | :--- | :--- | +| **Admin API** | `GET /api/v1/admin/devices`, and `lock`, `unlock`, `grant` and `override` under the same prefix. Bearer token. | The separate project `kotlin_phone_app_admin`. | +| **Hub API** | `GET /capabilities`, read-only `POST /ask`, `POST /notify`. Bearer token. | The Hub. Control never goes through the Hub. | +| **Dashboard** | Session login with a signed cookie and CSRF tokens. | Sam in a browser. | + +The admin API shape is small on purpose. Another project consumes it. Keep it +stable and versioned. + +## Related + +- Build spec and phase checklist: `AGENTS.md` section 11. +- Component names: `SYSTEM_MAP.md`. +- MQTT design and payloads: `TRANSPORT_DESIGN.md`. +- Interactive map: `https://maps.lab.audasmedia.com.au/internet_use_control/docs/`. diff --git a/docs/outline/02-config-and-env.md b/docs/outline/02-config-and-env.md new file mode 100644 index 0000000..0683053 --- /dev/null +++ b/docs/outline/02-config-and-env.md @@ -0,0 +1,92 @@ +# Internet Use Control — Configuration and Environment + +> **Target**: AI agents. **Writing style**: ASD-STE100 Simplified Technical English. + +## Configuration Files + +| File | Status | Purpose | +| :--- | :--- | :--- | +| `environment.d/control-service.env` | Live, gitignored | The real values for the container. | +| `environment.d/control-service.env.example` | In git | The template. Copy it and set the values. | +| `docker-compose.yml` | In git | The container, the port and the env file path. | +| `deploy/mosquitto/conf.d/webrestrict.conf` | In git | The TLS listener, the password file and the ACL file. | +| `deploy/mosquitto/conf.d/webrestrict.acl` | In git | The per-device topic ACL. | +| `deploy/mosquitto/clients.env` | In git | The client names for the verifier. No secrets. | +| `deploy/caddy/webrestrict.caddy` | In git | The vhost on `.35`. | + +Secrets come from `environment.d` or from +`~/.config/environment.d/10-secrets.conf`. The service reads only the process +environment. No secret is in the repository. + +## Ports and Addresses + +| Item | Value | +| :--- | :--- | +| Control Service | `8791` on `.13` | +| Public URL | `https://webrestrict.lab.audasmedia.com.au` (Caddy on `.35`) | +| MQTT plain plane | `1883` on `.13`, anonymous | +| MQTT TLS plane | `8883` on `.13`, credentials and ACLs | +| Apprise | `8210` on `.35` | +| Hub | `8788` on `.13` | +| Database | `/data/control.db` inside the container | + +## Environment Variables + +| Variable | Default | Purpose | +| :--- | :--- | :--- | +| `CONTROL_ADDR` | `:8791` | Listen address. | +| `CONTROL_DB_PATH` | `/data/control.db` | SQLite file. | +| `CONTROL_SESSION_SECRET` | required | Signs the session cookie. 16 characters minimum. | +| `CONTROL_SESSION_TTL` | `12h` | Session lifetime. | +| `CONTROL_COOKIE_SECURE` | `true` | Secure cookie flag. Keep it true behind HTTPS. | +| `CONTROL_ADMIN_USERNAME` | `sam` | Dashboard login name. | +| `CONTROL_ADMIN_PASSWORD` | — | Plaintext password, option A. | +| `CONTROL_ADMIN_PASSWORD_HASH` | — | bcrypt hash, option B, preferred. | +| `CONTROL_ADMIN_API_TOKEN` | — | Bearer token for the versioned admin API. Empty answers `503`. | +| `CONTROL_DEVICE_KEYCODE_HASH` | — | bcrypt hash of the shared device keycode. Seeds SQLite at start-up. | +| `CONTROL_APP_ENV` | `production` | Label in the footer. | +| `CONTROL_LOG_LEVEL` | `info` | `debug`, `info`, `warn` or `error`. | +| `CONTROL_TIMEZONE` | `Australia/Sydney` | IANA zone for the weekly windows. | +| `CONTROL_MQTT_ENABLED` | `true` | Turn the broker link on or off. | +| `CONTROL_MQTT_BROKER` | `tcp://host.docker.internal:1883` | Broker URL. The live value is `tls://host.docker.internal:8883`. | +| `CONTROL_MQTT_CLIENT_ID` | `control-service` | MQTT client id. | +| `CONTROL_MQTT_USERNAME` | — | The Control Service broker account. | +| `CONTROL_MQTT_PASSWORD` | — | Its password. | +| `CONTROL_MQTT_CA_FILE` | `/etc/webrestrict/tls/ca.crt` | PEM bundle that verifies the broker. TLS only. | +| `CONTROL_MQTT_INSECURE` | `false` | Allow a plaintext broker with no credentials. The live value is `false`. | +| `CONTROL_HUB_TOKEN` | — | Bearer token for `/capabilities`, `/ask` and `/notify`. Empty answers `503`. | +| `CONTROL_APPRISE_URL` | `http://192.168.20.35:8210` | The house Apprise API. | +| `CONTROL_APPRISE_KEY` | — | Stored Apprise configuration key, option A. | +| `CONTROL_APPRISE_URLS` | — | Stateless Apprise destination list, option B. The lab default. | +| `CONTROL_APPRISE_TOKEN` | — | Optional bearer token for a protected Apprise API. | +| `CONTROL_ALERTS_ENABLED` | `true` | Turn the alert engine on or off. | +| `CONTROL_ALERT_OFFLINE_AFTER` | `30m` | Time before an offline device raises an alert. | +| `CONTROL_ALERT_INTERVAL` | `30s` | How often the alert engine re-evaluates the devices. | + +The service refuses a non-TLS broker unless `CONTROL_MQTT_INSECURE=true` +(AGENTS.md section 10, rule 6). The live deployment does not use that escape +hatch. + +## Secrets + +| Secret | Where it lives | Notes | +| :--- | :--- | :--- | +| Dashboard password | `CONTROL_ADMIN_PASSWORD_HASH` | bcrypt. Preferred over the plaintext form. | +| Admin API token | `CONTROL_ADMIN_API_TOKEN` | A bearer token. Separate from the session login. | +| Hub token | `CONTROL_HUB_TOKEN` | A bearer token. Separate from the admin token. | +| Device keycode | SQLite, bcrypt hash | One code for every device. Seeded from `CONTROL_DEVICE_KEYCODE_HASH`. | +| MQTT accounts | Mosquitto password file | One account per device plus `control-service`. | + +## Commands That Read or Change Configuration + +```bash +# Generate a bcrypt hash without a host Go toolchain. +docker run --rm --entrypoint /app/control webrestrict/control:0.7.0-phase9 \ + hash-password 'your-password' + +# Set the shared device keycode. The code comes from stdin. +docker exec -i webrestrict-control /app/control keycode <<< '246810' + +# Print the MQTT identity of every device. Tokens only, no secrets. +docker exec webrestrict-control /app/control mqtt-clients +``` diff --git a/docs/outline/03-code-map.md b/docs/outline/03-code-map.md new file mode 100644 index 0000000..2c9f938 --- /dev/null +++ b/docs/outline/03-code-map.md @@ -0,0 +1,117 @@ +# Internet Use Control — Code Map + +> **Target**: AI agents. **Writing style**: ASD-STE100 Simplified Technical English. + +## Module + +`gitea.lab.audasmedia.com.au/sam/internet_use_control`, Go 1.25. The image is +CGO-free. SQLite is the pure-Go `modernc.org/sqlite` driver. + +## Folder Tree + +``` +cmd/control/main.go entry point and subcommands +internal/config/ environment configuration and validation +internal/auth/ signed-cookie session login and CSRF +internal/store/ SQLite: open, migrations, queries + store.go connection, migration runner, maintenance loop + devices.go device registry and desired state + schedules.go weekly windows and the effective state + lists.go block and allow lists + reports.go presence, telemetry, the daily rollup + overrides.go override history and the device keycode + alerts.go alert records and per-device dedup state + migrations/001_init.sql … 005_alerts.sql +internal/mqtt/ + link.go paho client, subscriptions, publish, TLS + payload.go the topic and payload contract for the clients +internal/alert/alert.go the limit and offline alert engine +internal/apprise/apprise.go the Apprise API client +internal/hub/ the Hub contract: capabilities, answers, notify +internal/web/ + server.go routes, middleware, session + devices.go the device registry and manual lock/unlock + schedules.go the weekly schedule and list editors + usage.go the usage screen + admin_api.go the versioned admin API and the keycode endpoint + hub_api.go /capabilities, /ask, /notify + templates/ login, dashboard, editors, usage, partials + static/ tokens.css, app.css, theme.js, htmx, fonts +android/ the kids' Android client (Gradle, Kotlin) + app/src/main/java/au/audasmedia/webrestrict/kids/ + KidsApp.kt application class + MainActivity.kt the kid's status screen + BlockAccessibilityService.kt foreground app detection + OverlayController.kt the full-screen block overlay + BlockEngine.kt the block decision + DeviceConfig.kt the config payload model + MqttController.kt the broker link + TamperReporter.kt the FR-17 tamper report + UsageCollector.kt Usage Access telemetry + TelemetryReporter.kt telemetry publish + SiteFilterVpnService.kt the device-local VpnService + SiteFilter.kt the site decision + DnsMessage.kt DNS parsing + IpPacket.kt IP packet parsing + AppPrefs.kt the cached config on disk + BootReceiver.kt start after boot + TelemetryStore.kt local telemetry buffer + tools/emu-proof.sh headless emulator proof +deploy/caddy/ the vhost on .35 +deploy/mosquitto/ the TLS listener, the accounts, the ACL, the verifier +deploy/verify-phase8.sh the lock latency proof +deploy/verify-phase9.sh the Hub and alert proof +docs/ this documentation set +``` + +## Entry Point + +`cmd/control/main.go` starts the web server. It also holds the subcommands: + +| Subcommand | Purpose | +| :--- | :--- | +| `hash-password` | Make a bcrypt hash for a password. | +| `mqtt-clients` | Print the MQTT identity of every device. | +| `rollup` | Rebuild the daily telemetry rollup. | +| `keycode` | Set or generate the shared device keycode. | +| `alerts` | Print recent alerts. | +| `usage` | Print usage from the database. | + +## Dependencies + +| Module | Use | +| :--- | :--- | +| `github.com/eclipse/paho.mqtt.golang` | The MQTT client. | +| `modernc.org/sqlite` | The pure-Go SQLite driver. No CGO. | +| `golang.org/x/crypto` | bcrypt for the password and the keycode hashes. | + +The web layer uses the Go standard library only. The UI uses HTMX 2.0.4, +vendored under `internal/web/static/`. + +## Database Migrations + +| File | Change | +| :--- | :--- | +| `001_init.sql` | Devices, schedules, lists, state, telemetry, overrides, audit. | +| `002_telemetry_unique.sql` | One telemetry row per (device, day, app, domain). | +| `003_usage_rollup.sql` | The daily rollup table. | +| `004_overrides_grant.sql` | The grant column and the override history. | +| `005_alerts.sql` | The alerts table and the dedup state. | + +Migrations apply in order at start-up. Never hard-delete a device or its history +(AGENTS.md section 10, rule 10). + +## Android Client + +The client is a normal Gradle project with its own `gradlew`. The block path is +the Accessibility Service plus the overlay. The site path is the local +`VpnService`. The tamper path is `TamperReporter.kt`. The client keeps the last +config in `AppPrefs.kt`. There is no Device Owner and no managed profile. + +## Known Gaps in the Code + +- **Phase 4, the Windows computer client, does not exist.** No Python client and + no process blocking are in the tree. +- The MQTT `cmd` and `ack` topics are design only. No code uses them. +- The phone client reports telemetry from Usage Access. Site filtering uses the + local `VpnService`. diff --git a/docs/outline/04-agent-runbook.md b/docs/outline/04-agent-runbook.md new file mode 100644 index 0000000..418b943 --- /dev/null +++ b/docs/outline/04-agent-runbook.md @@ -0,0 +1,100 @@ +# Internet Use Control — Agent Runbook + +> **Target**: AI agents. **Writing style**: ASD-STE100 Simplified Technical English. + +## Before You Start + +1. Read `docs/outline/00-index.md`. Inject only the file that you need. +2. Read `AGENTS.md`, section 11. Confirm the phase checklist and the status line. +3. Read `README.md` for the technical reference. +4. Work in `/home/sam/paseo/projects/internet_use_control` only. +5. Bound every Docker command with `timeout`, for example + `timeout 60 docker compose ps`. Never run a Docker command without a bound. + +## Build and Test + +There is no host Go toolchain. Use the cached builder image. + +```bash +make test # go test ./... in golang:1.25-alpine +make vet # go vet ./... +make build # build the container image +``` + +`make test` and `make vet` run the container as the current user. The module +cache stays in `.gocache/`, which is gitignored. No root-owned file enters the +repository. + +## Run and Restart + +```bash +timeout 60 docker compose up -d --build # start or update +timeout 60 docker compose restart control # restart the container +timeout 60 docker compose logs --tail=100 control +timeout 60 docker compose ps +curl -sS http://127.0.0.1:8791/health +``` + +The compose file starts the container `webrestrict-control` on port `8791`. The +image tag is `webrestrict/control:0.7.0-phase9`. + +## Verify the Behaviour + +| Target | Proves | +| :--- | :--- | +| `make mqtt-verify` | TLS, credentials and topic ACLs on a scratch broker. | +| `make verify-phase8` | An API lock reaches a device in under one second. | +| `make verify-phase9` | The Hub answers and the Apprise alerts deliver. | + +`make mqtt-verify` uses a scratch broker on `127.0.0.1:18883`. It never touches +the shared Mosquitto. The two `verify-phase*` targets read the broker +credentials from `deploy/mosquitto/clients.env`. + +## Useful Subcommands + +```bash +timeout 60 docker exec webrestrict-control /app/control rollup +timeout 60 docker exec webrestrict-control /app/control alerts --limit 10 +timeout 60 docker exec webrestrict-control /app/control usage --day 2026-10-13 --detail +timeout 60 docker exec webrestrict-control /app/control mqtt-clients +``` + +## Documentation Commands + +```bash +# Render the Mermaid diagram. +timeout 120 mmdc -i docs/architecture.mmd -o docs/diagrams/architecture.png --scale 2 -b white + +# Validate and deliver the Archify map. +node ~/.agents/skills/archify/bin/archify.mjs validate architecture \ + docs/archify/architecture.architecture.json --quality showcase --json +node ~/.agents/skills/archify/bin/archify.mjs deliver architecture \ + docs/archify/architecture.architecture.json docs/architecture-map.html \ + --quality showcase --json + +# Publish docs/ to the maps site. +~/.agents/skills/project-docs/scripts/publish-maps.sh +``` + +The visual-check of `docs/architecture-map.html` reports a 1-pixel vertical +overflow at 1440×900. All larger viewports pass. The delivered map passes the +9-check showcase profile with 0 errors. + +## Rules That Stop a Task + +1. Never hard-delete a device or its history (AGENTS.md section 10, rule 10). +2. Only the Control Service publishes device config. Everything else writes to it + over HTTP. +3. `/ask` is read-only. It writes nothing. +4. Do not change the router, Pi-hole or DNS. The project is device-side only. +5. The children's client never changes its own rules. It obeys and reports. +6. One commit and one push for a documentation change. Add the co-author trailer + `Co-Authored-By: Paperclip `. + +## Resume Steps + +1. Read `docs/outline/00-index.md`, then `docs/outline/01-system-overview.md`. +2. Run `git status` and `git log -n 5 --oneline`. +3. Run `make test`. +4. Read `AGENTS.md` section 11. Phase 4 is the only unfinished phase. +5. Check the issue board before you start new work. Do not create tasks ad hoc. diff --git a/docs/vikunja-tasks.json b/docs/vikunja-tasks.json new file mode 100644 index 0000000..f7113e8 --- /dev/null +++ b/docs/vikunja-tasks.json @@ -0,0 +1,21 @@ +{ + "title": "Internet Use Control - Human Task Overview", + "description": "High-level task checklist for human review. The live board is the Vikunja project 'Internet Use Control' (id 20). This file is the repo copy.", + "vikunja_project": { + "name": "Internet Use Control", + "id": 20, + "url": "https://vikunja.lab.audasmedia.com.au/projects/20" + }, + "tasks": [ + { "id": 186, "title": "Confirm the Control Service runs on .13 and the dashboard login works", "done": false }, + { "id": 187, "title": "Review the weekly schedule grid per kid and prove a window turns on by itself", "done": false }, + { "id": 188, "title": "Check the allowlist: Spotify stays available while other apps are blocked", "done": false }, + { "id": 189, "title": "Prove the admin app locks a device in under one second", "done": false }, + { "id": 190, "title": "Open the Usage screen and check a week of real usage for one device", "done": false }, + { "id": 191, "title": "Ask the Hub 'how long have the kids been on screens today?' and check the answer", "done": false }, + { "id": 192, "title": "Prove a tamper alert (Accessibility Service disabled) reaches Sam", "done": false }, + { "id": 193, "title": "Prove a device keeps its last state when the broker is unreachable", "done": false }, + { "id": 194, "title": "Decide the Phase 4 Windows client: build it, or close the project at 9 of 10", "done": false }, + { "id": 195, "title": "Review the published docs: README, Obsidian note, Archify map and Outline collection", "done": false } + ] +}