lunch-notifier/CLAUDE.md
Barely Removable d3498849d3
All checks were successful
Deploy / deploy (push) Successful in 33s
Document calendar target switch to calendar.family_2
Two HA entities are both named "Family" (calendar.family and
calendar.family_2); the automation now targets _2 per user preference.
Note it only supports create_event, not delete, so a bad push has to be
cleaned up manually in Google Calendar rather than via the HA API.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KsJqAZ82Veyf5oihBr71S5
2026-09-03 08:56:05 -07:00

5.3 KiB

Lunch Notifier

Single-file Flask app that scrapes the daily Mariposa (Kyrene school district) lunch menu and texts it to a list of phone numbers via email-to-SMS carrier gateways (e.g. 4805551234@vtext.com for Verizon). Runs as one Docker container on the user's Unraid home server.

Architecture

  • app.py — everything: Flask web UI/API, APScheduler-style job scheduling (schedule library), and the email-to-SMS send logic (smtplib via Gmail, app password auth). The HTML/CSS/JS frontend is embedded as a Python string constant (HTML) rather than a separate template file — no build step, no template engine.
  • scraper.py — fetches and parses the school's published lunch menu.
  • scheduler.py — day-of-week/time scheduling helpers used by app.py.
  • Settings (phone numbers, send time, days, vacation date ranges) persist to /data/settings.json, not environment variables — env vars (PHONE_NUMBER, RUN_TIME, RUN_DAYS) are defaults used only the very first time, before a settings file exists.
  • Vacation ranges suppress sends without disabling the schedule — checked fresh on every run via is_on_vacation().
  • Home Assistant integration (2026-09-02, HA side built 2026-09-03): if HA_WEBHOOK_URL is set, every successful menu fetch also POSTs {"date": ..., "items": [...]} to that webhook, independent of whether the SMS send itself succeeds. On the HA side (automation.lunch_menu_family_calendar, http://192.168.86.207:8123), a local_only webhook trigger runs calendar.create_event against calendar.family_2 (the "Family" Google Calendar entity — there are two entities both named "Family" in this HA instance, calendar.family and calendar.family_2; this integration deliberately targets _2), producing an all-day event with the menu as the description (bulleted). This makes the scrape here the single source of truth — HA doesn't poll or re-scrape anything. calendar.family_2 only supports create_event, not delete_event — a bad push can't be cleaned up via the HA API, only manually in Google Calendar. No duplicate-prevention either way: re-pushing the same date's menu creates a second event. The webhook id is a random token that lives only in the server's .env (HA_WEBHOOK_URL) and in the automation's trigger config — never committed here. If the calendar push stops working, check both ends: GET /api/integrations on this app reports whether HA_WEBHOOK_URL is set at all, and ha_get_automation_traces on automation.lunch_menu_family_calendar shows whether HA is even receiving the webhook.
  • Manual actions (2026-09-03): besides "Send Lunch Menu Now" (full job — scrape, HA push, text everyone), the UI has "Push to Calendar Only" (POST /api/push-calendar → push_calendar_only(), scrape + HA push, no texts — for testing the calendar sync without spamming phones) and "Preview Today's Menu" (GET /api/preview → _scrape_menu(), pure dry run, no side effects at all). carrier_gateway is editable in the Schedule card (dropdown of common US carriers + custom domain). Gmail credentials and HA_WEBHOOK_URL stay env-only, not settings.json — the UI has no auth, so secrets aren't made editable there; GET /api/integrations just exposes whether each is configured (booleans, not values) so the UI can show a badge.

Local development

pip install -r requirements.txt
python3 app.py

Needs GMAIL_USER and GMAIL_APP_PASSWORD env vars to actually send (a Gmail app password, not the account password — required since Gmail disallows plain SMTP auth with the real password). Visit http://localhost:2323.

Deployment

Target: Unraid server, container path /mnt/user/appdata/lunch-notifier, built from Dockerfile/docker-compose.yml in this repo. Container name Lunch-Notifier, port 2323. No reverse proxy — accessed directly via http://<server-ip>:2323 on the LAN.

Deploy mechanism: Forgejo push-to-deploy, same pattern as the other three projects on this box (card-grader, n64-tracker, arr-summary) — see card-grader's CLAUDE.md for the full pipeline mechanics (runner config, HTTPS-not-SSH git auth, known tradeoffs). In short: push to main on the forgejo remote (https://git.hippofam.com/ninja_hippo/lunch-notifier.git) triggers .forgejo/workflows/deploy.yml, which syncs into /mnt/user/appdata/lunch-notifier (excluding .git, .env, data/) and runs docker compose up -d --build.

Persistent state, never touched by deploys: data/settings.json (the volume-mounted /data inside the container) holds phone numbers, schedule, and vacation ranges — this is live user data, not deploy-managed. .env holds GMAIL_USER/GMAIL_APP_PASSWORD/PHONE_NUMBER etc. and is server-only, gitignored, never committed.

Migration note (2026-08-25): this container originally used a Docker named volume (lunch-notifier_lunch-settings) instead of a bind mount. That meant Unraid's Appdata Backup plugin — which only knows about /mnt/user/appdata and /mnt/cache/appdata as backup sources — silently skipped it entirely; the settings data was never included in any backup. Migrated to a bind mount (./data:/data) with the existing settings copied over first and verified intact before the old named volume was deleted.