commit f621a30a95a9073bbf98ad98b94632a2016d609c Author: Sam Rolfe Date: Sun Oct 11 17:24:04 2026 +1100 Initial spec: AGENTS.md build spec + archived original notes diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..23c0a75 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# Secrets +.env +.env.* +*.local +*.keystore + +# Go +bin/ +*.exe +*.test +*.out + +# Python +__pycache__/ +*.pyc +.venv/ +venv/ + +# Node / web +node_modules/ +dist/ +*.visual-check.* + +# Android / Kotlin +.gradle/ +build/ +local.properties +*.apk + +# OS / editors +.DS_Store +*.swp + + +# Pi agent generated memory (local state, not source) +.pi/memory-vault/ diff --git a/.pi/settings.json b/.pi/settings.json new file mode 100644 index 0000000..96535ea --- /dev/null +++ b/.pi/settings.json @@ -0,0 +1,16 @@ +{ + "packages": [ + { + "source": "/home/sam/.agents", + "extensions": [ + "extensions/pi-config/index.ts", + "extensions/filechanges/index.ts", + "extensions/ask-user-question/index.ts" + ], + "skills": [ + "skills/obsidian-cli", + "skills/npm-security" + ] + } + ] +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..59d8122 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,233 @@ +# Internet Use Control + +> Build spec for Paperclip. Follow the phases in section 11 in order. Do not skip a +> phase's Definition of Done. When something is unclear, ask Sam — do not guess. +> +> **Read `SYSTEM_MAP.md` and `TRANSPORT_DESIGN.md` in this folder first.** They are +> the component picture and the MQTT design. This file is the build spec. + +--- + +## 1. Purpose and outcome + +A system that limits the kids' screen use on their own devices — Android phones and +Windows computers. It applies schedules, blocklists and allowlists on the device, +collects usage, and lets Sam override. + +**Outcome (the single success statement):** +> Harry's phone and Finn's computer follow their schedule without anyone touching +> them, Sam can see their usage, and Sam can change a limit from his phone in +> seconds. + +## 2. Scope + +**In scope** +- Per-device weekly schedules. +- A blocklist of apps (Android package, Windows process) and sites (hostname). +- An allowlist during a block (for example, Spotify at night, school sites). +- Usage telemetry per device, app, and domain. +- Push state to devices; collect presence and telemetry. +- On-device warnings and a status view for the kid. +- An admin override (keycode on device; remote from the admin app). +- Alerts to Sam when a limit is hit. + +**Out of scope** +- Anything house-wide. **No router change. No Pi-hole. No DNS server change.** +- The kids' Android **client** is this project. The **admin app** is not — it lives + in `kotlin_phone_app_admin`. +- Chores, quests, or earning screen time. Removed by decision. Do not build it. + +## 3. Users and roles + +| User | Can do | +|---|---| +| **Sam** | Everything. Session login to the dashboard. Uses the admin app to override. | +| **Jo** | Read-only (optional, later). | +| **Harry / Finn** | See their own status on their device. Cannot change rules. | + +## 4. Functional requirements + +| ID | Requirement | +|---|---| +| FR-1 | Register devices: name each one, assign it to a child, choose its platform. | +| FR-2 | Weekly schedules: independent on/off windows per weekday, per device. | +| FR-3 | Blocklist: apps (Android package name, Windows process name) and sites (hostname). | +| FR-4 | Allowlist: things that stay available during a block (Spotify, school sites). | +| FR-5 | Publish each device's desired state to MQTT, **retained**, on its own topic. | +| FR-6 | Presence: know if a device is online, using the broker's Last Will and Testament. | +| FR-7 | Telemetry: receive usage by device, app, and domain, and store a rolling log. | +| FR-8 | Web dashboard: devices, schedules, blocklists, allowlists, and usage. | +| FR-9 | Kids' app view: what is blocked now, until when, why, and an alert when a block starts. | +| FR-10 | Override: a keycode on the device, and remote control from the admin app. | +| FR-11 | Admin app API: lock, unlock, grant time, and query state. | +| FR-12 | Hub API: `GET /capabilities`, `POST /ask` (read-only), `POST /notify`. | +| FR-13 | Alerts via Apprise when a limit is hit or a device goes offline too long. | +| FR-14 | Fail safe: when a device cannot reach the broker, it keeps its last state. | +| FR-15 | Audit log: every state change, who made it, and when. | + +## 5. Screens and UX flow + +**Screens (web dashboard)** +1. **Login** — session form. +2. **Dashboard** — every device as a card: name, child, platform, status dot, current state, time left. +3. **Schedule editor** — a weekly grid per device. +4. **Blocklist / allowlist editor** — one list, tagged by platform. +5. **Usage** — per device: top apps, top domains, by day and week. +6. **Alerts** — recent limit hits and warnings. + +**Kids' device view (in the kids' client app)** +- What is blocked now. +- How long until it lifts. +- Why (schedule, limit reached). +- An alert when a block starts. + +**Main flow** +``` +Sam opens dashboard → sees Harry's phone "locked, 42 min left" + → grants 30 min → the phone unlocks within one second +``` + +## 6. Look and feel + +Reuse the Family Home Lab design system. + +- Source: `/home/sam/Docker/Containers/family-home-lab/DESIGN.md` +- Tokens: `/home/sam/Docker/Containers/family-home-lab/portal/static/tokens.css` +- Copy `tokens.css` into this project. + +Canvas `#f6f5f4`, one action blue `#0075de`, Inter, 8px grid, pill CTAs, hairline +borders. Include dark mode. Works on a phone browser. Touch targets ≥44px. + +## 7. Data model + +SQLite inside the Control Service. Suggested tables: + +| Table | Holds | +|---|---| +| `devices` | id, name, child, platform, topic, online, last-seen | +| `schedules` | device id, weekday, start, end | +| `blocklist` | device id (or group), kind (`app`, `site`), value | +| `allowlist` | device id (or group), kind, value | +| `state` | device id, locked, until, allow (JSON), updated-at | +| `telemetry` | device id, app, domain, seconds, day | +| `overrides` | device id, actor, action, until, created-at | +| `audit` | actor, action, device id, detail, created-at | + +Retention: telemetry rolls up daily and is kept long. The audit log is append-only +and size-capped. + +## 8. Integrations + +| System | Use | +|---|---| +| **MQTT broker** | The **existing Mosquitto on `.13`**. Per-device credentials and topic ACLs. | +| **Apprise** | Alerts to Sam. Running on `.35:8210`. | +| **Home Assistant** | Optional: publish device state with MQTT discovery, for dashboards. | +| **Hub** | `POST /ask` (read-only), `POST /notify`, `GET /capabilities`. | +| **Admin app** | Separate project. Calls this service's API directly. | + +The Hub interface is frozen in `central_ai_llm_master/docs/ask-contract.md`. + +**Control never goes through the Hub.** The admin app calls this service directly. + +## 9. Technology (locked) + +| Item | Value | +|---|---| +| Backend | **Go** | +| UI | **HTMX** + server-rendered HTML | +| Database | **SQLite** | +| Transport | **MQTT** (existing Mosquitto) + HTTP for setup and queries | +| Design | Family Home Lab `tokens.css` | +| Host | `.13`, Docker | +| Container | **This project's own** image and compose file. | +| Port | Allocate one (suggest **8791**). Record it once chosen. | +| URL | `webrestrict.lab.audasmedia.com.au` via Caddy on `.35` | +| Auth | **Separate session login.** | +| Kids' phone client | **Kotlin**, Android Device Owner | +| Kids' computer client | **Python** (`psutil`, `pystray`) | + +## 10. Rules and constraints + +1. **Enforcement is device-side and kids-only.** No router, no Pi-hole, no + house-wide change. +2. **Fail safe is "keep last state".** The device caches its last config. +3. **One admin keycode**, shared by every device. +4. **No mobile profile.** Away from home behaves the same as at home. +5. Only the **Control Service** publishes config. Everything else writes to it over + HTTP. +6. MQTT uses TLS and per-device credentials with topic ACLs. A device may read only + its own config and write only its own status. +7. The kids' app never changes its own rules. It only obeys and reports. +8. Models go through **OmniRoute**. Secrets come from `environment.d`. +9. `/ask` is read-only. +10. **Never hard-delete** a device or its history. + +## 11. Build order + +**Phase 1 — Skeleton** +Go service, SQLite, config, session login, Dockerfile, compose, Caddy route. +*DoD:* the dashboard runs on `.13`, login works, `/health` returns ok. + +**Phase 2 — Devices and state** +Device registry, dashboard cards, manual lock/unlock in the UI, state in SQLite. +*DoD:* a device can be added and its state changed from the dashboard. + +**Phase 3 — MQTT** +Publish retained state per device. Subscribe to status and telemetry. Presence via LWT. +*DoD:* a test subscriber receives the state, and a dropped subscriber shows offline. + +**Phase 4 — Computer client** +Python client: subscribe to config, apply process blocking, report telemetry, overlay +warning, keep last state. +*DoD:* a real Windows machine blocks a listed process during a scheduled window. + +**Phase 5 — Phone client** +Kotlin Device Owner app: suspend listed packages, overlay, status view, telemetry. +*DoD:* a real Android phone suspends a listed app during a window. + +**Phase 6 — Schedules, blocklists, allowlists** +Editors in the dashboard. Allowlist honoured during a block. +*DoD:* Spotify stays available while other apps are blocked. + +**Phase 7 — Telemetry and usage** +Store and display usage by device, app, and domain. +*DoD:* the dashboard shows a week of real usage for one device. + +**Phase 8 — Overrides** +Device keycode and the admin app API (lock, unlock, grant). +*DoD:* the admin app locks a device and it locks in under one second. + +**Phase 9 — Hub and alerts** +`/capabilities`, `/ask`, `/notify`, and Apprise alerts. +*DoD:* the Hub asks "how long have the kids been on screens today?" and gets a correct answer. + +**Phase 10 — Documentation** +*DoD:* project-docs run; Gitea, Obsidian, Outline, Vikunja, and the map are updated. + +## 12. Acceptance criteria + +- A real device follows its weekly schedule without intervention. +- A listed app is blocked on the real phone and computer. +- Spotify stays available during a block, when allowlisted. +- The admin app locks a device in under one second. +- Usage is visible per device for at least a week. +- The Hub answers a screen-time question correctly. +- When the broker is unreachable, the device keeps its last state. +- Everything runs in its own container on its own port, behind Caddy. + +## 13. Decisions and remaining questions + +Resolved: +1. **Device-side only**, kids only. No router, no Pi-hole. +2. **MQTT** with retained messages and LWT presence. `TRANSPORT_DESIGN.md`. +3. **Keep last state.** One admin keycode. No mobile profile. +4. **Go** for the service. **Kotlin** for the kids' phone client. **Python** for the computer client. +5. **Admin app** is a separate project (`kotlin_phone_app_admin`); it calls this service. +6. **Chores to unlock time** removed. Not in scope. + +Remaining: +1. Pass-through apps during a lock: confirm the list (Spotify, school sites). +2. Remote shutdown or sleep for a kid's computer, or blocking only? +3. Windows client: a service, or a scheduled task? +4. Telemetry retention: how long to keep daily rollups? diff --git a/ORIGINAL_NOTES.md b/ORIGINAL_NOTES.md new file mode 100644 index 0000000..a03c24d --- /dev/null +++ b/ORIGINAL_NOTES.md @@ -0,0 +1,38 @@ +# Internet Use Control — Original Notes (archived) + +> Historical pre-planning notes. **Superseded by `AGENTS.md`.** Do not build from this. + + +> Kept for provenance. The sections above replace these. Do not build from this. + +``` +Project Goal & Philosophy +- A cross-platform screening system (Android + Windows) that helps kids + self-regulate evening screen usage. A gentle enforcement layer, a reminder + system (overlays, warnings, sounds), and a notifier via Apprise. +- A centralized Docker dashboard for scheduling, logging, and remote management. + +Components (original) +- Central Orchestrator (Go + HTMX + Tailwind), SQLite, Docker, behind Caddy at + webrestrict.lab.audasmedia.com.au. Session login. +- Windows Client (Python + psutil + pystray): background loop, warning overlays, + .wav countdowns, process termination, domain logging, ACPI sleep/shutdown, + 4-digit parent PIN. +- Android Agent (Kotlin, Device Owner): ForegroundService, setPackagesSuspended, + WindowManager overlay, usage reporting. + +Original phases +1. Go + HTMX Docker environment; SQLite tables for schedules and blocklists. +2. Client telemetry API (POST /api/log, GET /api/client-config); Apprise triggers. +3. Windows monitor script. +4. Kotlin Device Owner workspace. +5. LAN control integration. + +Ideas (kept) +- Usage stats for kids: total time, top apps, by day and week. +- Some apps allowed through: Spotify, school sites and timetables. + +Superceded by the decisions in this spec: the 30-second poll is replaced by MQTT; +the router/DNS and "network hooks" ideas are dropped in favour of device-side +enforcement; the Rust idea is dropped; the chores idea is removed. +``` diff --git a/SYSTEM_MAP.md b/SYSTEM_MAP.md new file mode 100644 index 0000000..0bf7a08 --- /dev/null +++ b/SYSTEM_MAP.md @@ -0,0 +1,144 @@ +# Internet Use Control — system map + +**Date:** 2026-10-10 +**Purpose:** name every piece once, clearly. No more "Go service" or "admin app" +guessing. + +--- + +## The pieces + +| Name | What it is | Where it runs | Language | +|---|---|---|---| +| **Control Service** | The brain **and** the source of truth. Holds schedules, blocklists, per-device state, usage. Serves the web dashboard. | `.13`, Docker | Go + SQLite | +| **Web Dashboard** | Your setup screen in a browser. Schedules, blocklists, devices, usage. **This is the desktop admin.** | Served by the Control Service | HTMX | +| **Broker** | The message bus. Pushes state to devices. Collects their reports. | `.13`, Mosquitto (already running) | — | +| **Kids' phone app** | Enforces limits on a kid's phone. Shows what is blocked, and for how long. | Kids' phones | Kotlin | +| **Kids' computer client** | Same job on a kid's laptop or desktop. | Kids' computers | Python | +| **Admin app** | Your override and approval surface. | **Your phone** | Kotlin | +| **Hub** | A **peer**. Asks questions only. Never controls. | `.13` | separate project | + +**There is no separate desktop admin app.** The dashboard in a browser *is* the +desktop admin. If you want a native desktop app later, that is a new decision. + +--- + +## Where the truth lives + +**SQLite inside the Control Service, on `.13`.** Not MQTT, not the devices, not the +Hub. + +The broker is only a **delivery pipe**. It is fast and it remembers the last +message, but it is not the database. + +--- + +## Who writes what + +| Action | First goes to | Then | +|---|---|---| +| Change a schedule | Control Service (HTTP) | saved in SQLite → published to MQTT | +| Lock or unlock now | Control Service (HTTP) | same | +| You enter the device keycode | Control Service (HTTP) | same | +| Device reports usage | Broker (MQTT) | Control Service writes it to SQLite | +| Hub asks "screen time today?" | Control Service (HTTP) | reads SQLite, answers | + +**Only the Control Service publishes config to devices.** Everything else writes +to the Control Service over HTTP. + +That is the answer to "who publishes config": **the Control Service, and only the +Control Service.** One writer. One truth. + +**"Config"** means the desired state for **one device**: locked or not, until +when, the allowed list, the blocked list. + +--- + +## Data flow + +```mermaid +flowchart LR + WD["Web Dashboard
(browser)"] + AA["Admin app
(your phone)"] + HUB["Hub
(peer)"] + CS["Control Service
Go + SQLite
on .13"] + MQ["Broker
Mosquitto on .13"] + KP["Kids' phone app
(Kotlin)"] + KC["Kids' computer client
(Python)"] + + WD -->|HTTP| CS + AA -->|HTTP| CS + HUB -->|HTTP /ask| CS + CS -->|publish state| MQ + MQ -->|deliver state| KP + MQ -->|deliver state| KC + KP -->|status + usage| MQ + KC -->|status + usage| MQ + MQ -->|reports| CS +``` + +--- + +## Config carries per device + +Each device has its own state. That is how "Harry's phone off, desktop on" works: + +``` +restrict/harry_phone/config {"locked": true, "until": "21:00", "allow": ["spotify"]} +restrict/harry_desktop/config {"locked": false} +``` + +The Control Service publishes each message, **retained**, so a device that +reconnects gets its current state at once. + +--- + +## Fail state, and where the device is + +The device must decide what to do when it **cannot reach the broker**. It also +needs to know whether it is **at home or away**, because the two cases differ. + +| Situation | How the device knows | Behaviour | +|---|---|---| +| **At home, broker down** | On the home Wi-Fi, broker unreachable | **Keep last state** | +| **Away from home** | Not on the home Wi-Fi | **Keep last state** | +| **Long outage** | No contact for N hours | **Keep last state** | + +**One rule: keep last state.** An outage changes nothing. The child cannot escape +by going offline, and a server restart does not lock them out. + +The device **caches its last config on itself**, so the rule always has something +to follow. + +### Overrides (two) + +1. **On the device** — **one admin keycode, shared by every device**. For a broken + phone, a laptop, or when you are away. Stored hashed. Changeable in the + Control Service. +2. **Admin app** — remote lock, unlock, or grant time. + +### Honest limit + +A child can turn off Wi-Fi and use mobile data to look "away". Accepted. The goal +is **reasonable friction**, not a fortress. + +--- + +## Kids' app interface + +A simple screen on the kid's device: + +- **What is blocked** right now. +- **How long** until it lifts. +- **Why** (schedule, limit reached). +- An **alert** when a block starts. + +--- + +## Decisions (2026-10-10) + +1. **No mobile profile.** Away from home behaves the same as at home: keep last state. +2. **One admin keycode**, shared by every device. +3. **Keep last state indefinitely.** The admin keycode overrides it. + +These are settled. Do not re-open them unless something changes. diff --git a/TRANSPORT_DESIGN.md b/TRANSPORT_DESIGN.md new file mode 100644 index 0000000..830894e --- /dev/null +++ b/TRANSPORT_DESIGN.md @@ -0,0 +1,157 @@ +# Internet Control — transport design (MQTT vs polling) + +**Date:** 2026-10-10 +**Question from Sam:** can the internet controls use a broadcast mechanism +(Mosquitto / MQTT) as the source of truth? Devices check in, apply state, and +block on device. Then Harry's phone can be off while his desktop stays on. + +--- + +## Short answer + +**Yes — and it is better than polling. But do not make the broker the database.** + +Use both, each for its own job: + +| Layer | Job | +|---|---| +| **MQTT (Mosquitto)** | Push state, presence, and telemetry. Instant. | +| **HTTP API** | Setup and queries: schedules, blocklists, the portal, the Hub. | +| **The service (Go) + SQLite** | The **source of truth**. MQTT is transport, not storage. | + +Mosquitto is already installed. Home Assistant already speaks MQTT. This is proven +ground. + +--- + +## Why MQTT beats the 30-second poll + +| Property | Poll every 30 s | MQTT | +|---|---|---| +| Enforcement delay | up to 30 s | instant | +| Presence (online/offline) | must be inferred | free, via Last Will and Testament | +| Reconnect state | next poll | **retained** message delivers it at once | +| Per-device state | possible | natural (one topic per device) | +| Device discovery | service must know addresses | device announces itself | + +--- + +## Topic design + +| Topic | Direction | Retained | Payload | +|---|---|---|---| +| `restrict//config` | service → device | **yes** | desired state | +| `restrict//status` | device → service | yes | online, app, usage | +| `restrict//telemetry` | device → service | no | periodic usage | +| `restrict//cmd` | service → device | no | one-shot command | +| `restrict//ack` | device → service | no | command result | + +### Example — the "phone off, desktop on" case + +``` +restrict/harry_phone/config {"locked": true, "until": "2026-10-10T21:00Z", "allow": ["spotify"]} +restrict/harry_desktop/config {"locked": false} +restrict/finn_phone/config {"locked": true, "until": "2026-10-10T21:00Z"} +``` + +Each device subscribes only to its own config. Retained means a phone that +reconnects is locked again **immediately**, with no waiting. + +--- + +## Security (do not skip this) + +- TLS, plus a per-device username/password or a client certificate. +- **Topic ACLs.** A device may read only **its own** config and write only **its + own** status. Without this, a child can publish a fake "unlocked". +- Rotate device credentials when a device is removed. + +--- + +## Keep the HTTP API + +- Admin portal: schedules, blocklists, usage history. +- The Hub: `/ask` (*"how long have the kids been on screens?"*), `/capabilities`. +- Approval callbacks. + +Setup and questions are request/response. That is what HTTP is for. Do not force +them through MQTT. + +--- + +## Enforcement: device only, kids only (corrected) + +Rules apply to the **kids' devices only**. Nothing house-wide. No router change. +No Pi-hole. So filtering happens **on the device**: + +| Target | How (device-local) | +|---|---| +| An app (YouTube, a game) | Block the app. Android Device Owner `setPackagesSuspended`. Windows process kill. | +| A website by name | A **device-local** filter. Android: a local `VpnService`. Windows: hosts file or a firewall rule. | +| Everything except Spotify | Block all, allow a short list — on the device. | + +MQTT carries the commands. The device applies them to itself. + +--- + +## Anti-bypass (honest) + +The client runs on the device, so a determined child can interfere. Reasonable +steps, not a fortress: + +- **Android Device Owner** — the app cannot be uninstalled or force-stopped. +- **Windows service** with auto-restart and a watchdog. +- A child can buy another SIM. That is accepted. Aim for **reasonable friction**, + not a lockdown. + +--- + +## Fail safe (when a device cannot reach the broker) + +The device follows instructions. If it cannot hear the broker, what should it do? + +- **Fail-open:** assume "not locked". A child turns off Wi-Fi and plays games. +- **Fail-closed:** assume "locked". A server reboot locks everyone out. +- **Keep last state:** keep following the last instruction. An outage changes + nothing. + +**Recommended: keep last state.** Use a **retained** MQTT message, so a +reconnecting device gets the current instruction at once. Add an **admin +override** for real exceptions (travel, holidays). + +The open question: after a **very long** outage, does the device still keep the +last state, or does it lock? + +--- + +## The broker + +Use the **existing Mosquitto on `.13`**. Do not add a second broker. Isolation is +done with **per-device credentials and topic ACLs**. A second broker is optional +hardening only. + +--- + +## Who publishes config + +See `SYSTEM_MAP.md`. Short answer: **the Control Service, and only the Control +Service.** Everything else writes to it over HTTP. + +--- + +## Recommendation + +1. SQLite in the service stays the **source of truth**. +2. The service publishes desired state to MQTT (**retained**). +3. Devices subscribe, apply **instantly**, and block locally. +4. Telemetry returns over MQTT; the service writes it to SQLite. +5. HTTP stays for setup and Hub queries. +6. **Do not** make the broker the database. +7. Use a **per-device** topic so state is per device, not per family. + +## Open questions + +1. Fail safe: when the broker is unreachable, lock or keep the last state? +2. One broker only, or one broker per VLAN? (Mosquitto is single today.) +3. Should device state also appear in Home Assistant (MQTT discovery)? +4. Who publishes config — only the Go service, or also the admin app directly?