Screen Time

Every parental control app on the market assumes you can install something on the device you want to control. That assumption is wrong roughly four times: the PS5, the Apple TV, the grandmother’s iPad, and whatever Android tablet your kid’s friend brought over for a sleepover. MDM profiles are invasive, device-specific, and one factory reset away from irrelevant.
Screen Time takes a different approach. It runs inside Force Flow on port 4077 and enforces curfews at the network level via Firewalla. No agent on the device. No MDM profile. No app to uninstall. If it has a MAC address and it’s on your WiFi, it follows the rules of the haus.
The key insight that makes this work: the network is the only authority that can’t be uninstalled. Every device in the haus routes through Firewalla. If a MAC address exists on this subnet, Firewalla can pause it, block specific services on it, or cut it off entirely. No app to delete. No profile to remove. No clever workaround that doesn’t involve physically leaving the building.
Enforcement Model
Section titled “Enforcement Model”
Three tiers, because not every screen is the same screen.
Tier 1: Personal Apple Devices
Section titled “Tier 1: Personal Apple Devices”Not blocked. Apple’s built-in Screen Time handles Albert’s iPhone and iPad. Doubling up with network-level blocking would create conflicts and confuse the device-level reporting that Andreanne actually checks. Leave Apple to Apple.
Tier 2: Shared Gaming Devices
Section titled “Tier 2: Shared Gaming Devices”Hard curfew. The VRVANA-PC and any auto-discovered consoles (PS5, Switch, Xbox, Steam Deck) block at Albert’s curfew time and unblock at 10:00. No phone-presence heuristics, no “is he awake?” guessing. Curfew means curfew. Parents who want to use the PS5 after hours can manually unblock via the API, PWA, or by asking an agent.
Previously this tier used phone-presence detection (phone on WiFi = child awake), but it wasn’t reliable enough. A phone charging in the bedroom with WiFi on would keep the PS5 blocked all night. A phone in airplane mode would unblock everything immediately. Hard curfew is simpler and more predictable.
Tier 3: Screens (TVs and PCs)
Section titled “Tier 3: Screens (TVs and PCs)”Hard curfew with service-level precision. Apple TVs and the VRVANA-PC get a fixed cutoff at 22:00 (10 PM), unblock at 10:00. The First Floor Apple TV doubles as the HomeKit hub, so it uses service-level blocking: Netflix, YouTube, Disney+, Crave, Crunchyroll, and 10 other streaming apps get blocked. Bell Fibe TV and HomeKit traffic are preserved. The haus doesn’t stop being smart just because it’s bedtime.
Schedule Intelligence
Section titled “Schedule Intelligence”Three schedule tiers, resolved in priority order by _get_effective_schedule():
| Priority | Schedule | Curfew | Wake | Source |
|---|---|---|---|---|
| 1 (highest) | Quebec school holiday | 23:00 | 10:00 | holidays.yaml |
| 2 | Weekend (Fri/Sat night) | 23:00 | 10:00 | Day-of-week check |
| 3 (default) | Weekday | 21:30 | 10:00 | Default config |
Holiday detection uses a curated holidays.yaml with Quebec school calendar dates — semaine de relache, Noel, summer break. The file is maintained manually because Quebec’s school calendar is published as a PDF, which is exactly the kind of government decision this system was built to route around.
Gradual Wind-Down
Section titled “Gradual Wind-Down”Curfew isn’t a cliff — it’s a slope. Three phases of progressive service blocking:
| Offset | Action |
|---|---|
| -30 min | Social media blocked (TikTok, Instagram, Snapchat, Discord, Reddit, Twitter, Facebook) |
| -15 min | Gaming blocked (Steam, Epic, Roblox, Minecraft, Fortnite) |
| 0 min | Everything blocked (full curfew) |
Wind-down blocks are explicitly cleaned up at wake time. The system tracks which MACs had services blocked during wind-down and sends batch-unblock commands when daytime resumes. No orphaned DNS rules surviving into the next day.
The service_categories config in devices.yaml defines which domains belong to which category. Adding a new service is one YAML entry, not a code change.
Smart Features
Section titled “Smart Features”Homework Mode
Section titled “Homework Mode”POST /screen/homework activates a focused mode: gaming, social media, and streaming are blocked. Educational sites, Google Workspace, and reference tools remain open. Triggered manually via PWA, Siri shortcut, or Home Assistant. Deactivates automatically after 2 hours or via DELETE /screen/homework.
Earned Credits
Section titled “Earned Credits”Albert can earn screen time extensions by doing real things in the real world:
| Preset | Duration | Description |
|---|---|---|
chores | 30 min | Haus tasks verified by parent |
devoirs | 30 min | Homework completion |
lecture | 30 min | Reading (physical book, not a screen) |
exercise | 30 min | Physical activity |
Credits are capped at 90 minutes per day. They accumulate via POST /screen/credit and are consumed via POST /screen/credit/redeem. Unused credits do not roll over. This is a parental control system, not a loyalty program.
Guest Device Handling
Section titled “Guest Device Handling”New devices on the network are auto-discovered via ARP scanning and classified by OUI (manufacturer) lookup. Unknown devices get haus rules applied immediately. Parents approve or remove guests via the PWA. Guest permissions expire after 24 hours.
MAC randomization detection: if a device with a locally-administered MAC bit appears and no known device has disconnected, it’s flagged as a potential randomized address. Not bulletproof, but it catches the obvious cases.
Weekly Digest
Section titled “Weekly Digest”Every Sunday at 6 PM, a digest is sent via Signal and push notification. Contents: average bedtime for the week, number of overrides, credits earned and redeemed, any guest device activity. It’s the parental equivalent of a sprint retrospective, except the sprint is “did the 12-year-old go to bed on time.”
Enforcement Resilience
Section titled “Enforcement Resilience”The system enforces curfews through Firewalla’s API, but APIs fail. The Firewalla Purple’s policy:delete endpoint has a known firmware bug where it returns “invalid policy” for every PID. If you trust the API alone, devices get blocked at night and never unblocked in the morning.
Three layers of defense:
1. Persisted Block State
Section titled “1. Persisted Block State”Every block and unblock is recorded in a blocked_state SQLite table. On startup, Force Flow restores in-memory state from the DB. A restart at 3 AM no longer means the system “forgets” what’s blocked and skips the morning unblock.
2. SSH Iptables Fallback
Section titled “2. SSH Iptables Fallback”The Firewalla bridge wraps every policy:delete call with an SSH fallback. If the API fails, the bridge SSHes directly into the Firewalla box and removes the iptables rules by their comment tag. Same for policy:create — if the MAC-level block rule can’t be created via API (stale policy ID), the bridge adds DROP rules via SSH. Batch operations use a single SSH call for all PIDs.
3. Daily Safety Net
Section titled “3. Daily Safety Net”Every enforcement tick checks: if it’s daytime and a device should be unblocked, send the unblock command even if the in-memory state says “not blocked.” This fires once per day per device. It’s idempotent — a redundant unblock on an already-unblocked device is a no-op. But if the state was lost (restart, crash, Firewalla reboot), the safety net catches it within 30 seconds of the next enforcement cycle.
Migration Export
Section titled “Migration Export”GET /export on the bridge (port 1984) dumps the full Firewalla configuration: all hosts, active policies, custom DNS records, and service domain mappings. Combined with the firewalla-export.sh and firewalla-import.sh scripts, the entire enforcement state can be backed up and restored on a new Firewalla box (hello, Gold Pro) without reconfiguring anything by hand.
Interfaces
Section titled “Interfaces”Screen Time exposes four interfaces because every family member interacts with the haus differently.
Progressive Web App served directly from Force Flow. iOS standalone mode (Add to Home Screen), dark OLED theme, French UI. All features accessible: override, homework mode, credits, guest management, schedule view. Resolves via local DNS — no Tailscale required when home.
Home Assistant
Section titled “Home Assistant”Full integration in packages/screen_time.yaml:
- REST commands for all actions (override, homework, credit)
- Sensors for curfew state, active blocks, phone presence
- Lovelace card with current status, quick actions, schedule display
- Automations for wind-down triggers and Sonos announcements
iOS Shortcuts (6 Siri Shortcuts)
Section titled “iOS Shortcuts (6 Siri Shortcuts)”| Shortcut | Action |
|---|---|
| ”Mode devoirs” | Activate homework mode |
| ”Fin devoirs” | Deactivate homework mode |
| ”Credit corvees” | Grant chores credit |
| ”Credit lecture” | Grant reading credit |
| ”Prolonger 30 min” | 30-minute curfew extension |
| ”Couvre-feu maintenant” | Immediate full curfew |
All shortcuts route through Tailscale to http://100.0.0.25:4077. French invocations. Works from any family member’s iPhone via “Hey Siri.”
Holocron Panel
Section titled “Holocron Panel”Native React component in the-holocron Electron app. Glass-morphism card with Framer Motion transitions. Moon icon in the nav bar. Displays: curfew countdown ring, phone presence indicator, quick action buttons, weekly schedule, per-screen status. Designed to match the Holocron’s existing dark aesthetic without looking like a corporate parental control dashboard.
API Reference
Section titled “API Reference”All endpoints on port 4077 under the /screen prefix.
| Method | Path | Description |
|---|---|---|
| GET | /screen/status | Current curfew state, active blocks, phone presence |
| POST | /screen/override | Temporary curfew override (duration in minutes) |
| POST | /screen/block | Manually block a device by MAC |
| POST | /screen/unblock | Manually unblock a device by MAC |
| GET | /screen/report | Usage report (daily/weekly/custom range) |
| GET | /screen/devices | All known devices and their current state |
| POST | /screen/assign | Assign a device to a family member |
| POST | /screen/reload | Reload config from devices.yaml |
| GET | /screen/schedule | Current effective schedule and next transition |
| POST | /screen/homework | Activate homework mode |
| GET | /screen/homework/status | Homework mode state and remaining time |
| DELETE | /screen/homework | Deactivate homework mode |
| POST | /screen/credit | Grant earned credit (preset + optional note) |
| GET | /screen/credits | Credit balance and history for today |
| POST | /screen/credit/redeem | Redeem accumulated credits |
| GET | /screen/digest | Generate digest on demand (also sent weekly) |
| GET | /screen/guests | List detected guest devices |
| POST | /screen/guest/approve | Approve a guest device |
| DELETE | /screen/guest/remove | Remove a guest device |
| GET | /pwa/ | PWA index |
| GET | /pwa/{filename} | PWA static assets |
| GET | /pwa/manifest.json | PWA manifest |
| GET | /pwa/sw.js | PWA service worker |
Configuration
Section titled “Configuration”The entire system is driven by ~/.sanctum/screen-time/devices.yaml:
family: - name: Albert role: child phone_mac: "FA:CE:DE:CA:CA:01" # Presence detection anchor curfew_enabled: true
- name: Bert role: parent phone_mac: "FA:CE:DE:CA:CA:02"
- name: Andreanne role: parent phone_mac: "FA:CE:DE:CA:CA:03"
- name: Lise Phenix role: grandparent phone_mac: "FA:CE:DE:CA:CA:04" curfew_enabled: false # Grand-maman goes to bed when she wants
shared_devices: - name: PS5 mac: "FA:CE:DE:CA:CA:10" tier: gaming # Presence-based blocking bound_to: Albert
- name: VRVANA-PC mac: "FA:CE:DE:CA:CA:11" tier: gaming bound_to: Albert
screens: - name: Living Room Apple TV mac: "FA:CE:DE:CA:CA:20" tier: screen is_homekit_hub: true # Service-mode: block streaming, keep HomeKit hard_curfew: "22:00"
service_categories: social: - tiktok.com - instagram.com - snapchat.com - youtube.com gaming: - store.steampowered.com - playstation.net - xboxlive.com - epicgames.com streaming: - netflix.com - disneyplus.com - primevideo.com
credits: presets: chores: 30 devoirs: 30 lecture: 30 exercise: 30 daily_cap_minutes: 90 rollover: false
guest_rules: auto_apply_haus_rules: true require_parent_approval: true expiry_hours: 24
api_token: "redacted-but-you-get-the-idea"Troubleshooting
Section titled “Troubleshooting””Device didn’t block”
Section titled “”Device didn’t block””Two blocking modes exist and they fail differently:
- Soft pause (service-level): DNS block rules per domain, scoped to the device MAC. Check that the domain list in
service_categoriesis current. Some services use CDN domains that rotate.nslookupfrom the device to verify what it’s resolving. - Hard block (MAC-level): ACL=false plus iptables DROP rules. If ACL-only is set (no iptables rules), existing connections survive — the device stays online until it tries to open something new. Check the bridge logs for “[pause] MAC block rule API failed” — this means the API couldn’t create the rule and the SSH fallback should have kicked in.
”Device didn’t unblock in the morning”
Section titled “”Device didn’t unblock in the morning””Check in this order:
- Force Flow running?
lsof -i :4077on manoir. If it restarted, theblocked_stateDB should have restored state. Check logs for “RESTORED blocked state.” - Bridge running?
lsof -i :1984. The bridge handles the actual Firewalla API calls and SSH fallback. - Stale iptables rules? SSH to Firewalla and check:
sudo iptables -S | grep <MAC>. If rules exist but Force Flow thinks the device is unblocked, the safety net should catch it within 30 seconds. If not, manually unblock viaPOST /screen/unblock. - Stale DNS rules?
POST /host/:mac/unruleson the bridge (port 1984) batch-removes all DNS blocks for a MAC with SSH fallback.
”URL doesn’t resolve” (force.home)
Section titled “”URL doesn’t resolve” (force.home)”Three DNS layers in the haus, and they don’t always agree:
| Layer | Resolver | Where it applies |
|---|---|---|
| Firewalla DNS | Network-wide | All devices on WiFi |
| Tailscale MagicDNS | Tailnet only | Devices with Tailscale installed |
/etc/hosts | Per-machine | Mac Mini, dev machines |
If force.home works on the Mac but not on an iPhone, the Firewalla DNS override for force.home is missing. Add it via Firewalla app > DNS > Local DNS Records.
”Wrong schedule applied”
Section titled “”Wrong schedule applied””Check in this order:
- Holiday calendar:
holidays.yamlmight have a stale entry or missing date. Holidays take highest priority — if today is flagged as a holiday, weekend/weekday logic is irrelevant. - Weekend detection: Remember, Friday night (
weekday() == 4) and Saturday night (weekday() == 5) are weekends. Sunday night is a school night. The code uses the night-of, not the next-morning calendar day. - Manual override: An active override (via PWA or API) supersedes all schedule logic. Check
GET /screen/statusforoverride_active: true.
Architecture Decisions
Section titled “Architecture Decisions”Why Python, Not Rust
Section titled “Why Python, Not Rust”Screen Time is a control plane, not a data plane. It makes one API call to Firewalla every few minutes to toggle a rule. It does not process packets, route traffic, or handle any load that would justify the compilation overhead of Rust. The sanctum-proxy is Rust because it handles thousands of requests per second. Screen Time handles dozens of requests per day. Python with a 1700-line module inside Force Flow is the right tool.
Why Not Apple MDM
Section titled “Why Not Apple MDM”MDM (Mobile Device Management) is the “enterprise” answer to parental controls. It’s also:
- Invasive: Full device management profile. Visible to the child. Creates an adversarial dynamic.
- Platform-locked: Doesn’t touch the PS5, the PC, or grandma’s Android tablet.
- Fragile: One “Remove Profile” tap and it’s gone. Network-level enforcement has no uninstall button.
- Overkill: We need bedtime enforcement and homework mode, not remote wipe and app whitelisting.
Apple Screen Time on personal devices + Firewalla at the network layer covers every device in the haus without installing anything on any of them. The only software that runs is on the Mac Mini, which is the one machine in the building that no 12-year-old has the SSH key to.
Service Details
Section titled “Service Details”| Property | Value |
|---|---|
| Port | 4077 (shared with Force Flow) |
| Engine | ~/.sanctum/force-flow/screen_time.py |
| Bridge | ~/.openclaw/firewalla-bridge.js (port 1984) |
| Config | ~/.sanctum/screen-time/devices.yaml |
| Holidays | ~/.sanctum/screen-time/holidays.yaml |
| Database | ~/.sanctum/screen-time/usage.db |
| PWA | ~/.sanctum/screen-time/pwa/ |
| HA Package | ~/.openclaw/homeassistant/packages/screen_time.yaml |
| Holocron | ~/Projects/the-holocron/src/renderer/components/ScreenTimePanel.tsx |
| Export | tools/firewalla-export.sh / firewalla-import.sh |
| Digest | Sunday 18:00 via Signal + push |