Initial spec: AGENTS.md build spec + archived original notes

This commit is contained in:
2026-10-11 17:24:04 +11:00
commit f621a30a95
6 changed files with 624 additions and 0 deletions

36
.gitignore vendored Normal file
View File

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

16
.pi/settings.json Normal file
View File

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

233
AGENTS.md Normal file
View File

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

38
ORIGINAL_NOTES.md Normal file
View File

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

144
SYSTEM_MAP.md Normal file
View File

@@ -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<br/>(browser)"]
AA["Admin app<br/>(your phone)"]
HUB["Hub<br/>(peer)"]
CS["Control Service<br/>Go + SQLite<br/>on .13"]
MQ["Broker<br/>Mosquitto on .13"]
KP["Kids' phone app<br/>(Kotlin)"]
KC["Kids' computer client<br/>(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.

157
TRANSPORT_DESIGN.md Normal file
View File

@@ -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/<device>/config` | service → device | **yes** | desired state |
| `restrict/<device>/status` | device → service | yes | online, app, usage |
| `restrict/<device>/telemetry` | device → service | no | periodic usage |
| `restrict/<device>/cmd` | service → device | no | one-shot command |
| `restrict/<device>/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?