Phase 10: publish the documentation set + tick AGENTS.md 9 of 10

Phase 10 (the last phase of this project) is the project-docs pass. Update the
Gitea README, add the Mermaid source and rendered PNG, build the interactive
Archify map, add the Vikunja board tasks, and write the Outline AI-context
collection in Simplified Technical English.

Deliverables:
- README.md: status header, Phase 4 gap statement, "Architecture" section with
  embedded Mermaid + image + Archify link, "Documentation set" table, an
  explicit two-plane MQTT section (1883 plain+anonymous, 8883 TLS+ACLs), and
  the Android client's Usage Access and VpnService paths.
- AGENTS.md section 11: tick Phase 10, leave Phase 4 unticked, status line now
  "9 of 10 done - only Phase 4 outstanding".
- docs/architecture.mmd + docs/diagrams/architecture.png (mmdc).
- docs/archify/architecture.architecture.json + docs/architecture-map.html
  (Archify, showcase, 9/9 checks, 0 errors).
- docs/outline/00-index.md .. 04-agent-runbook.md (STE).
- docs/vikunja-tasks.json (Vikunja project "Internet Use Control", id 20).
- Obsidian note written to 010 inbox on .27; the obsidian-sorter proposed a
  folder and waits for the vault owner's approval.

Phase 4 (Python Windows computer client) is deliberately deferred and is NOT
built. Every document says so.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-10-14 12:13:21 +11:00
parent 5a9e56bb1f
commit eb300b6701
12 changed files with 14593 additions and 11 deletions

View File

@@ -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

101
README.md
View File

@@ -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<br/>browser dashboard"]
ADMIN["Admin app<br/>kotlin_phone_app_admin"]
HUB["Hub :8788<br/>read-only /ask"]
CADDY["Caddy on .35<br/>webrestrict.lab.audasmedia.com.au"]
CS["Control Service<br/>Go + SQLite :8791"]
APPRISE["Apprise :8210<br/>alerts + tamper reports"]
subgraph MQ["Mosquitto on .13 - two planes"]
TLS["8883 TLS<br/>per-device creds + topic ACLs"]
PLAIN["1883 plain + anonymous"]
end
PHONE["Kids' Android client<br/>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:** <https://maps.lab.audasmedia.com.au/internet_use_control/docs/>
(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/<device>/config` topic, and write **only** its own
`restrict/<device>/status` and `restrict/<device>/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

View File

@@ -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"
]
}
]
}

13703
docs/architecture-map.html Normal file

File diff suppressed because it is too large Load Diff

29
docs/architecture.mmd Normal file
View File

@@ -0,0 +1,29 @@
flowchart LR
SAM["Sam<br/>browser dashboard"]
ADMIN["Admin app<br/>kotlin_phone_app_admin"]
HUB["Hub :8788<br/>read-only /ask"]
CADDY["Caddy on .35<br/>webrestrict.lab.audasmedia.com.au"]
CS["Control Service<br/>Go + SQLite :8791"]
APPRISE["Apprise :8210<br/>alerts + tamper reports"]
subgraph MQ["Mosquitto on .13 - two planes"]
TLS["8883 TLS<br/>per-device creds + topic ACLs"]
PLAIN["1883 plain + anonymous"]
end
PHONE["Kids' Android client<br/>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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

47
docs/outline/00-index.md Normal file
View File

@@ -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`.

View File

@@ -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/<device>/config` topic. It
writes only its own `restrict/<device>/status` and
`restrict/<device>/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/`.

View File

@@ -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
```

117
docs/outline/03-code-map.md Normal file
View File

@@ -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`.

View File

@@ -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 <noreply@paperclip.ing>`.
## 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.

21
docs/vikunja-tasks.json Normal file
View File

@@ -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 }
]
}