feat: self-host Hobbs Family Organizer (Shield TV dashboard) on hf-pdocker-02:8340

- tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0: release bundle (Next.js app + Shield APK v1.4 + source)
- compose/hf-pdocker-02/hobbs-organizer: override (local-disk secrets, watchtower off), .env (port 8340), Shield provisioning script
- CLAUDE.md: service tree + internal ports
Trusted-LAN mode. Secrets live only on .100 /var/lib/hobbs-organizer/secrets (not in git).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E9NnVEico3WvFxLyjy1iW3
This commit is contained in:
2026-11-04 18:27:22 -06:00
parent 122ea392d1
commit e9c7e9df1a
68 changed files with 12831 additions and 0 deletions
@@ -0,0 +1,250 @@
# Hobbs' Family Organizer App — self-hosted package
This release moves the organizer off ChatGPT Sites and onto your own container host. The dashboard, API routes, calendar parser, weather, FRED market ticker, RSS headlines, QR handoff, and Shield wrapper are included.
The local deployment does **not** require ChatGPT, an OpenAI account, or an OpenAI API key. It is stateless and has no database or persistent application volume.
## What still uses the Internet
The container needs outbound DNS and HTTPS for:
- `calendar.google.com` — private Google Calendar ICS feeds
- `api.weather.gov` — seven-day National Weather Service forecast
- `fred.stlouisfed.org` — public FRED series pages used by the ticker
- the configured OpenAI, Google AI, DeepMind, NVIDIA, Ars Technica, and Wired RSS feeds
No inbound Internet access is required. Keep the service on your LAN, VPN, or behind your existing reverse proxy.
## Package contents
- `Dockerfile` — production Node 22 image
- `compose.yaml` — hardened Compose deployment on port 8080 by default
- `app/` — complete Next.js dashboard and API source
- `container/start.mjs` — validates secrets, then drops the app to uid/gid 10001
- `scripts/configure.*` — creates runtime secrets without printing them
- `scripts/backup.*` and `scripts/restore.*` — configuration backup and restore
- `secrets/` — ignored runtime secret location and a calendar template
- `shield/` — signed v1.4 APK, provisioning scripts, and Android source
- `MIGRATION.md` — ordered cutover and rollback procedure
## Requirements
- A Linux, Windows, or macOS host with Docker Engine/Desktop and Docker Compose v2
- Outbound HTTPS from the container host
- One to ten private Google Calendar `basic.ics` URLs
- For Shield deployment: Android Platform Tools (`adb`) and network debugging enabled on the Shield
The image builds for both `linux/amd64` and `linux/arm64` when the selected Node base image supports the host architecture.
## Quick start
### Linux or macOS
```bash
chmod +x scripts/*.sh
./scripts/configure.sh --mode trusted-lan
docker compose config --quiet
docker compose up -d --build --wait
curl -fsS http://127.0.0.1:8080/api/health
```
### Windows PowerShell
```powershell
Set-ExecutionPolicy -Scope Process Bypass
.\scripts\configure.ps1 -Mode trusted-lan
docker compose config --quiet
docker compose up -d --build --wait
Invoke-RestMethod http://127.0.0.1:8080/api/health
```
The configuration script:
1. copies `.env.example` to `.env` if needed;
2. prompts privately for each Google Calendar ICS URL;
3. writes `secrets/calendar_ical_urls.json` with restrictive permissions;
4. creates `secrets/organizer_access_token.txt`, empty for trusted-LAN mode or populated with a random token for bearer mode.
After startup, open `http://SERVER-IP:8080/` or the HTTPS name provided by your reverse proxy.
## Access modes
### Trusted LAN
Use this when firewall/VLAN/VPN rules already restrict the dashboard:
```bash
./scripts/configure.sh --mode trusted-lan
```
```powershell
.\scripts\configure.ps1 -Mode trusted-lan
```
The token file exists but is empty. Anyone who can reach the published port can view calendar details, so bind to the intended interface and enforce access in your network or reverse proxy.
### Bearer token
Use this for the Shield or a reverse proxy that can inject an authorization header:
```bash
./scripts/configure.sh --mode bearer --rotate-token
```
```powershell
.\scripts\configure.ps1 -Mode bearer -RotateToken
```
Every route except `/api/health` then requires:
```text
Authorization: Bearer TOKEN
```
The Shield wrapper injects this header into every same-origin request. An ordinary browser address bar cannot add it, so browser access in bearer mode should go through an authenticated reverse proxy that injects the header. The configuration scripts intentionally do not print the token.
## Configuration
Edit `.env` for nonsecret settings:
| Setting | Default | Purpose |
|---|---|---|
| `HOBBS_BIND_ADDRESS` | `0.0.0.0` | Host interface used by Docker |
| `HOBBS_HTTP_PORT` | `8080` | Published HTTP port |
| `ORGANIZER_TIMEZONE` | `America/Chicago` | Clock and calendar timezone |
| `WEATHER_LOCATION` | `Sapulpa, OK` | Display label |
| `WEATHER_LATITUDE` | `35.9987` | Sunrise/sunset latitude |
| `WEATHER_LONGITUDE` | `-96.11417` | Sunrise/sunset longitude |
| `NWS_FORECAST_URL` | `https://api.weather.gov/gridpoints/TSA/37,98/forecast` | NWS grid forecast |
| `WEATHER_USER_AGENT` | example value | NWS client identifier; add a monitored contact |
Keep these secrets out of `.env`, Git, image layers, and tickets:
- `secrets/calendar_ical_urls.json`
- `secrets/organizer_access_token.txt`
Private Google Calendar URLs act like passwords. Because the current URLs were previously shared during development, reset/regenerate each private ICS URL in Google Calendar before the final production cutover, then run the configure script again.
## Operations
Status and logs:
```bash
docker compose ps
docker compose logs --tail=200 organizer
curl -fsS http://127.0.0.1:8080/api/health
```
The health endpoint never calls Google, FRED, NWS, or RSS providers. It reports liveness plus booleans for calendar configuration and authentication, but no secret values.
Stop and start:
```bash
docker compose stop
docker compose start
```
Update after replacing the release files:
```bash
docker compose build --pull
docker compose up -d --wait
docker image prune
```
`docker image prune` is optional and removes only unused images after confirmation.
## Backup and restore
The app has no database. Back up only `.env` and the two runtime secret files.
Linux/macOS:
```bash
./scripts/backup.sh
./scripts/restore.sh --force backups/hobbs-organizer-config-YYYYMMDD-HHMMSS.tar.gz
```
Windows:
```powershell
.\scripts\backup.ps1
.\scripts\restore.ps1 -Archive .\backups\hobbs-organizer-config-YYYYMMDD-HHMMSS-XXXXXXXX.zip -Force
```
Backup archives contain private calendar URLs and possibly the bearer token. Store them in an encrypted or tightly access-controlled backup system.
## Reverse proxy and HTTPS
HTTPS with a stable internal DNS name is recommended, for example `organizer.home.arpa`. Sample Caddy and Nginx configurations are in `examples/`.
If you use an internal certificate authority, install its root certificate in the Shield's user certificate store. The wrapper trusts system and user-installed CAs but always rejects certificate errors; it has no trust-all bypass.
For direct LAN HTTP, the Shield configuration script requires both an explicit `-AllowInsecureHttp` switch and a private IP, `.local`, `.lan`, or `.home.arpa` host.
## NVIDIA Shield deployment
Install Android Platform Tools, enable network debugging, and authorize the computer on the Shield. Upgrading the existing package preserves the hosted rollback token and the per-device 6:30 AM schedule flag.
```powershell
$adb = "C:\path\to\platform-tools\adb.exe"
& $adb connect 192.168.86.179:5555
& $adb -s 192.168.86.179:5555 install -r .\shield\Hobbs-Family-Organizer-Local-v1.4.apk
```
For HTTPS with bearer authentication:
```powershell
.\shield\configure-shield.ps1 `
-Device 192.168.86.179:5555 `
-BaseUrl https://organizer.home.arpa/ `
-TokenFile .\secrets\organizer_access_token.txt `
-AdbPath $adb
```
For trusted-LAN HTTP:
```powershell
.\shield\configure-shield.ps1 `
-Device 192.168.86.179:5555 `
-BaseUrl http://192.168.86.50:8080/ `
-AllowInsecureHttp `
-AdbPath $adb
```
The script streams the token through ADB standard input and never places it in the process arguments. Repeat for the second Shield using its address. Do not change the package ID: `com.dayline.tv` is intentionally preserved so existing app-private data survives `adb install -r`.
The wrapper is debuggable so the scripts can use Android `run-as` to provision private files. After the migration, disable network debugging on each Shield and enable it again only during controlled maintenance.
To return a Shield to the existing hosted Site:
```powershell
.\shield\restore-hosted-site.ps1 -Device 192.168.86.179:5555 -AdbPath $adb
```
That removes only the local URL/token flags and keeps the old hosted `service_token` intact.
## Build from source
Container application:
```bash
npm ci
npm run lint
npm run build
npm start
```
For local `npm start`, set `CALENDAR_ICAL_URLS` to a JSON array and optionally set `ORGANIZER_ACCESS_TOKEN`. The Compose path is preferred because it loads secrets from files and applies the security profile.
Android source is under `shield/source/`. It targets Android SDK 35, Java 17, and Gradle 8.9/Android Gradle Plugin 8.7.3. The included APK is already signed with the same certificate as the installed v1.3 app. The private signing key is deliberately not included in this package.
## Troubleshooting
- `calendarConfigurationValid: false`: rerun `scripts/configure.*` and confirm the secret contains a JSON array of Google `basic.ics` URLs.
- Dashboard returns `401`: token mode is enabled; provision the same token to the Shield or reverse proxy.
- Weather is empty: verify the NWS grid URL, outbound HTTPS, DNS, and `WEATHER_USER_AGENT`.
- FRED shows demo data: allow outbound HTTPS to `fred.stlouisfed.org`; the feed uses public pages and no API key.
- Shield shows setup required: confirm the APK is installed and rerun `shield/configure-shield.ps1`.
- Shield rejects HTTPS: install the internal CA certificate or use a certificate trusted by Android; never bypass validation.
- Container does not become healthy: run `docker compose logs organizer` and validate that both secret files exist.