--- type: note proposed_folder: 200 projects/Tools Software WebUI proposed_toc: "100 Table of Contents/Projects.md → Tools & Software" proposed_tags: - project - family - architecture - go - kids - time - internet-control - mqtt - android - sqlite - htmx confidence: 0.8 approved: false proposed_for: ca730e053cc5 created: "2026-10-14 12:12" tags: - project - family - internet-control - go - mqtt - android - sqlite - htmx aliases: - Internet Use Control - Kids Screen Time skills: - Go backend development - HTMX server-rendered dashboard UI - SQLite data modelling and versioned migrations - MQTT transport with TLS and per-device topic ACLs - Android Accessibility Service and full-screen overlay (Kotlin) - Android VpnService for device-local site filtering - Versioned HTTP API design for a separate consumer - Docker and Caddy deployment tools: - Go - SQLite - HTMX - Mosquitto - Docker - Caddy - Apprise - Kotlin - Android Accessibility Service - Android VpnService --- # [[Internet Use Control — Kids' Screen Time Limits]] ## What is this project? This project limits screen time on the children's own devices. Harry's phone and Finn's computer should follow their weekly schedule with nobody touching them. Sam sees their usage on one page, and Sam changes a limit from his phone in seconds. The rules apply to the kids' devices only. Nothing changes for the rest of the house. There is no router change, no Pi-hole and no DNS change. The main piece is the **Control Service**. It runs on `.13` in Docker. It holds the schedules, the block lists, the allow lists and the usage in SQLite. It serves a small web dashboard. It is the one place that writes the rules. A device gets its rules over **MQTT**. The Control Service publishes the wanted state for each device as a *retained* message, so a phone that reconnects is limited again at once. The device also sends its status and its usage back over MQTT. If a device cannot reach the broker, it keeps its last rules. ## What it does - **Weekly schedules.** A grid per device, Monday to Sunday. A window means the rules are on. A window can cross midnight. - **Block lists.** An app (Android package name) or a site (hostname). - **Allow lists.** Spotify at night, school sites. The device blocks everything on the block list **except** the allow list. - **Usage.** The dashboard shows a week per device: the daily total, the top apps and the top sites. - **Overrides.** A versioned HTTP API for the admin app (`lock`, `unlock`, `grant`), plus one shared keycode that a child can type on the device for a short exemption. - **Alerts.** Apprise tells Sam when a limit is reached or a device is offline too long. - **A Hub answer.** The family Hub can ask "how long have the kids been on screens today?" and gets a real answer. That question path is read-only. ## Why it matters (what it shows) This is a full home-lab product, not a script. It shows a Go service with no heavy framework, an HTMX dashboard with no build step, a small SQLite store with versioned migrations, real MQTT with TLS, per-device accounts and topic access control, and a versioned API that another project consumes. The Android side is the interesting part. **Device Owner was rejected**, because it needs a factory reset of the children's phones. Instead the phone blocks by **interruption**: an Accessibility Service watches the foreground app, and a full-screen overlay covers a blocked app. The child **can** disable it, and that is accepted. The real enforcement is the **tamper alert**: when the Accessibility Service stops, the app is force-stopped, or the overlay permission is removed, the phone reports it and Sam gets an alert. Site filtering uses a device-local `VpnService`. There is no wipe, no Device Owner and no managed profile. ## Architecture ```mermaid flowchart LR SAM["Sam
browser dashboard"] --> CADDY["Caddy on .35"] CADDY --> CS["Control Service
Go + SQLite :8791"] ADMIN["Admin app"] -->|"/api/v1"| CS HUB["Hub"] -->|"read-only /ask"| CS CS -->|"retained config"| MQ["Mosquitto on .13
8883 TLS + topic ACLs"] MQ -->|"own config topic"| PHONE["Kids' Android client
Kotlin"] PHONE -->|"status + telemetry"| MQ MQ -->|reports| CS CS --> APPRISE["Apprise
alerts + tamper"] ``` A picture of the two MQTT planes: port **1883** stays plain and anonymous for legacy consumers, and port **8883** is TLS with per-device credentials and topic ACLs. A device on 8883 may read only its own config topic and write only its own status and telemetry topics. Without that rule a child could publish a fake "unlocked". ## What is still missing (state it plainly) **The Phase 4 Windows computer client is deliberately deferred and is not built.** There is no Python client and no process blocking on a computer. The project stands at **9 of 10** phases for that single reason. Everything else in the build spec is done and running. ## Where it lives - **Code**: Gitea repo `sam/internet_use_control`. - **Live**: (container on `.13`, port 8791, behind Caddy on `.35`). - **Interactive map**: - **AI context**: `docs/outline/` in the repo (00-index and four documents). - **Tasks**: Vikunja project **Internet Use Control** (id 20). - **Diagram source**: `docs/architecture.mmd`, rendered to `docs/diagrams/architecture.png`. ## Status Built and deployed. Phases 1, 2, 3, 5, 6, 7, 8, 9 and 10 are done. The phones are limited by the Android client. Only the Windows client (Phase 4) remains, and it is deferred on purpose.