# 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`, 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. 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 ```bash 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://: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.