Pull daily instead of on day 25; skip past dates and weekend misreads
All checks were successful
Deploy / deploy (push) Successful in 46s
All checks were successful
Deploy / deploy (push) Successful in 46s
The monthly day-25 pull pushed whatever month the district's permalink served. September's PDF was still live on 9/29, so the 9/25 pull re-pushed September and October would not have been pulled until 10/25. Home Assistant now dedups lunch events per date, so pull every day and pick up a new month the evening it's posted. - push only today onward; skip weekend dates that have items (a misread date: Fri 9/25's menu once landed on Sat 9/26) and log a warning - settings schema is just calendar_run_time; load_settings keeps only known keys and startup rewrites the file, dropping stale SMS-era data - UI: remove day-of-month field and the "no dedup" warning, fix buttons that reverted to "This Week" labels - docs: daily scheduler, HA queued mode + dedup, calendar is iCloud Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a809aa5a5a
commit
3d374c4417
3 changed files with 105 additions and 116 deletions
104
CLAUDE.md
104
CLAUDE.md
|
|
@ -11,12 +11,17 @@ user's request in favor of Home Assistant calendar events exclusively. If
|
|||
you find references to texting/SMS/phone numbers anywhere (docs, old
|
||||
commits), they describe the pre-2026-09-03 app, not the current one.
|
||||
|
||||
**2026-09-29: monthly → daily pulls, HA-side dedup.** See "Pull scheduler"
|
||||
and "Home Assistant integration" below. Anything describing a day-25
|
||||
monthly pull or "no dedup, re-pushing creates duplicates" describes the
|
||||
pre-2026-09-29 app.
|
||||
|
||||
**2026-09-08: pivoted from Nutrislice to a Claude-read PDF, and from
|
||||
weekly to monthly pulls.** The district discontinued Nutrislice (the old
|
||||
scraper hit `kyrene.api.nutrislice.com`, a clean per-day JSON API) in
|
||||
favor of publishing one monthly menu PDF per grade band on
|
||||
`kyrene.org/family-student-resources/food-services/school-menus`. See
|
||||
"Menu source" and "Monthly pull scheduler" below for how that's handled
|
||||
"Menu source" and "Daily pull scheduler" below for how that's handled
|
||||
now. Anything describing a weekly Nutrislice pull describes the
|
||||
pre-2026-09-08 app.
|
||||
|
||||
|
|
@ -54,16 +59,15 @@ pre-2026-09-08 app.
|
|||
fail-soft-into-`_status`-error behavior as a Home Assistant push failure.
|
||||
`GET /api/integrations` reports both keys' configured/not-configured
|
||||
state.
|
||||
- Settings (calendar pull schedule) persist to `/data/settings.json`, not
|
||||
environment variables — env vars (`CALENDAR_RUN_TIME`,
|
||||
`CALENDAR_RUN_DAY_OF_MONTH`) are defaults used only the very first time,
|
||||
before a settings file exists. `load_settings()` merges the file's
|
||||
contents *over* `_DEFAULTS` rather than returning the file verbatim —
|
||||
this matters because the server's settings.json predates several schema
|
||||
changes (SMS era, then the weekly-schedule era) and is missing whatever
|
||||
keys the current schema expects; without the merge, the scheduler would
|
||||
silently come up with zero jobs after every deploy until someone happened
|
||||
to re-save the Schedule card in the UI.
|
||||
- Settings (just `calendar_run_time`) persist to `/data/settings.json`, not
|
||||
environment variables — the env var `CALENDAR_RUN_TIME` is a default used
|
||||
only before a settings file exists. `_DEFAULTS` is the complete schema:
|
||||
`load_settings()` merges the file *over* `_DEFAULTS` (so an old file
|
||||
missing a key still yields a valid schedule instead of zero jobs) and
|
||||
**keeps only keys in `_DEFAULTS`**; startup rewrites the file with that
|
||||
result. That pruning (2026-09-29) removed the SMS era's phone numbers /
|
||||
carrier gateway / vacations and the old `calendar_run_day_of_month` from
|
||||
the server's settings.json — they had lingered, unread, since the pivots.
|
||||
- **No vacation/no-push date ranges (removed 2026-09-03, briefly existed).**
|
||||
A day with no posted menu already produces an empty `items` list from
|
||||
`get_month_menu()`, and `_push_calendar()` already skips pushing when
|
||||
|
|
@ -77,53 +81,51 @@ pre-2026-09-08 app.
|
|||
does anything externally-visible. 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` per user
|
||||
choice). `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: re-pushing the
|
||||
same date's menu creates a second event — this is why the default
|
||||
schedule is a single day/month, not daily (see below).
|
||||
**`mode: parallel`, not the default `single` (fixed 2026-09-03 after a
|
||||
real bug):** a push fires one webhook POST per school day back-to-back
|
||||
with no delay. Under `mode: single`, HA rejects any webhook trigger that
|
||||
arrives while a previous run is still executing (`calendar.create_event`
|
||||
itself takes ~600-700ms) — most days in a batch were silently dropped as
|
||||
`failed_single`, so only one day's event ever actually got created per
|
||||
push. Found via `ha_get_automation_traces` after the user reported a
|
||||
manual test push not showing up on the calendar. Each invocation is
|
||||
independent (one `calendar.create_event` call, no shared state), so
|
||||
`parallel` is the correct mode per HA's own guidance ("independent
|
||||
per-entity actions → parallel"), not just a workaround. The webhook id is
|
||||
`calendar.family_2` (the shared iCloud "Family" calendar, via CalDAV —
|
||||
there are two entities both named "Family" in this HA instance,
|
||||
`calendar.family` and `calendar.family_2`; this integration deliberately
|
||||
targets `_2` per user choice). `calendar.family_2` supports
|
||||
`create_event` and `get_events` but not `delete_event` — a bad push can't
|
||||
be cleaned up via the HA API, only by hand in the Calendar app.
|
||||
**Automation mode history — `queued` since 2026-09-29.** The app fires
|
||||
one webhook POST per school day back-to-back (HA answers each POST
|
||||
immediately, so the app never waits on the calendar write). `single`
|
||||
(default) dropped all but the first as `failed_single` (fixed 2026-09-03
|
||||
→ `parallel`); `parallel` then sent ~20 simultaneous CalDAV PUTs on a
|
||||
month push, overflowing iCloud (connection pool full + ~10× `500
|
||||
Internal Server Error` on the 2026-09-25 push). Now `queued` (max 40):
|
||||
one write at a time, nothing dropped. **Dedup (2026-09-29):** before
|
||||
creating, the automation reads the date with `calendar.get_events` and
|
||||
stops if a `🍱 Mariposa Lunch` event already exists — that's what makes
|
||||
daily pulls safe. It verifies the event saved and retries once after 15s.
|
||||
Pre-dedup pushes left 2-3 copies on most early-September days; those
|
||||
have to be deleted by hand. 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.
|
||||
- **Monthly pull scheduler (weekly → monthly, 2026-09-08):** calendar pulls
|
||||
run on their own schedule (`calendar_run_time`/`calendar_run_day_of_month`
|
||||
in settings, `schedule` jobs tagged `"calendar"`), completely decoupled
|
||||
from anything else — the tag/isolation pattern
|
||||
(`schedule.clear("calendar")` instead of a bare `schedule.clear()`) is
|
||||
deliberate so a future second schedule wouldn't stomp this one. The
|
||||
`schedule` library has no native day-of-month trigger, so
|
||||
`apply_calendar_schedule()` registers a *daily* job
|
||||
(`_maybe_push_month`) that no-ops unless `datetime.now().day` matches
|
||||
the configured day — `push_month_to_calendar()` only actually runs on
|
||||
that one day per month. `next_calendar_run()` computes the real next
|
||||
firing directly from settings (handling month/year rollover and clamping
|
||||
the configured day to short months, e.g. day 30 in February → the 28th)
|
||||
for the UI's "Next Scheduled" field, since the underlying `schedule` job's
|
||||
own `next_run` is meaningless here (it's always just "tomorrow"). Default
|
||||
is day 25 at 18:00 — picked because the district tends to post next
|
||||
month's PDF a few days before month-end, so pulling then usually catches
|
||||
the *upcoming* month rather than the one ending. Whatever month is live
|
||||
at the permalink when the job fires is what gets pushed — there's no
|
||||
logic forcing "next month specifically."
|
||||
- **Daily pull scheduler (monthly → daily, 2026-09-29):** `push_month_to_calendar()`
|
||||
runs every day at `calendar_run_time` (default 18:00; `schedule` job
|
||||
tagged `"calendar"` — `schedule.clear("calendar")`, not a bare
|
||||
`schedule.clear()`, so a future second schedule wouldn't stomp it).
|
||||
**Why daily:** the old once-a-month day-25 pull pushed whatever month was
|
||||
live at the permalink; when the district posted late (September 2026: the
|
||||
permalink still served September on 09-29), it silently re-pushed the
|
||||
current month and the next month's menu wouldn't be pulled until the
|
||||
following 25th — most of that month gone. Daily pulls pick up a new month
|
||||
the evening it's posted; HA-side dedup makes the repeats harmless, and the
|
||||
Claude read costs well under a cent a day. Each pull pushes **only today
|
||||
onward** (past dates are skipped — they're already on the calendar or no
|
||||
longer matter) and **skips weekend dates that have items**, logging a
|
||||
warning and noting it in the status result: there's never school lunch on
|
||||
a weekend, so that's a misread date (seen once: Fri 9/25's menu landed on
|
||||
Sat 9/26). Whatever month is live at the permalink is what gets read —
|
||||
there's no logic forcing "next month specifically."
|
||||
- **Manual actions:** "Push This Month to Calendar" (`POST /api/push-calendar`
|
||||
→ `push_month_to_calendar()`, same function the schedule calls) and
|
||||
→ `push_month_to_calendar()`, same function the schedule calls — so it
|
||||
also pushes only today onward and skips weekend misreads) and
|
||||
"Preview This Month's Menu" (`GET /api/preview` → `get_month_menu()`
|
||||
directly, pure dry run, no Home Assistant push, no side effects at all —
|
||||
use this to check the scraper before trusting a scheduled push).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue