mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-10 18:07:20 +01:00
Pulls the day's delivered dishes and their real per-portion macros out of
panel.kuchniavikinga.pl, so a catering meal is one keypress instead of five
retyped numbers.
A delivered meal is not an eaten meal, so nothing is ever logged unattended:
dishes land in the *curated* bank, and entries are written only after an
explicit confirmation. Auto-logging would let the gate satisfy its own
checkpoint from a delivery note, and would record what the courier dropped off
rather than what was eaten.
Three surfaces:
- `diet-guard kuchnia [--log] [--yes]`
- the lock screen's "Today's delivery" button, which prefills the meal form
dish by dish (the user still clicks Log & Continue)
- a warm-up after each logged meal, on the existing background thread
All event-driven; no new systemd timer. The two automatic paths go through
`refresh_delivery_once`, guarded to one fetch per day.
The panel is an undocumented white-labelled Dietly API; the endpoint map was
recovered from its JS bundles and confirmed against live responses. Notes,
including the traps, are in docs/kuchnia-wikinga.md.
Load-bearing details:
* Macros are per portion and `weight` is that portion in grams -- proven, not
assumed: 4P+4C+9F reproduces the stated calories within ~1% on every meal of
the captured day. `_energy_is_consistent` re-runs that check per dish and
drops any that fails; a per-100g mix-up is invisible in a payload and would
silently skew every logged meal by a factor of several.
* `bank_dishes` compares nutritional fields before calling `add_manual_entry`,
which restamps `t` unconditionally. Without the compare, every refresh
republishes the whole curated bank to every peer. Tests assert on call count.
* Duplicate log suppression reads today's synced log, not a local marker:
`log_meal` mints a fresh uuid, so a re-import after marker loss would merge
as duplicates on every peer.
* The gate hook is in `on_focus_ready`, not `_cli_gate._should_lock`:
`gate_is_due()` never reads the food bank, so a refresh there could not
change the lock decision while adding a third-party round trip to the ~105ms
`gate --check` fast path.
* `requests` is lazy-imported so the gate's not-due tick does not pay for it.
* Credentials and the cached session cookie live in ~/.config/diet_guard at
mode 600, outside the synced tree. The credentials file is written by hand,
never by the package -- the same contract as sync_token.
`_GateDashboard` is merged into `_GateMealFlow`, and both "pull it in from
elsewhere" flows share one `_PullFlows` mixin: pylint's max-parents counts
every MRO entry, so the gate's controller chain had no room for a new class.
Also adds a side-by-side row for the two pull buttons -- stacking them cost
42px and pushed the tab past the viewport at 1366x768.
989 tests, 100% branch coverage, pre-commit clean.
96 lines
3.4 KiB
Python
96 lines
3.4 KiB
Python
"""Walk the panel's endpoints to today's delivered dishes.
|
|
|
|
Three calls, and the shape of the walk is not what the endpoint names suggest:
|
|
|
|
1. ``company/customer/order/active-ids`` -> ``[orderId]``.
|
|
2. ``company/customer/order/{orderId}`` -> the order, which **embeds every
|
|
delivery** with its date. No enumeration call is needed, but the embedded
|
|
``deliveryMeals`` carry ids only -- no dish names, no macros.
|
|
3. ``company/general/menus/delivery/{deliveryId}/new`` -> the day's actual
|
|
menu, with names and per-portion nutrition.
|
|
|
|
Step 3 is the important correction: the obvious-looking
|
|
``.../deliveries/{id}/details`` **404s** (and 400s when keyed by date), which
|
|
was confirmed against three separate delivery days. The menu endpoint is keyed
|
|
by the opaque ``deliveryId``, never by the date.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
from diet_guard._kuchnia_errors import KuchniaError
|
|
from diet_guard._kuchnia_parse import parse_menu
|
|
|
|
if TYPE_CHECKING: # pragma: no cover - typing only
|
|
from datetime import date
|
|
|
|
from diet_guard._kuchnia_client import PanelSession
|
|
from diet_guard._kuchnia_parse import Dish
|
|
|
|
|
|
def _active_order_id(session: PanelSession) -> object | None:
|
|
"""Return the first active order id, or None when there is no active order."""
|
|
payload = session.get_json("company/customer/order/active-ids")
|
|
if not isinstance(payload, list) or not payload:
|
|
return None
|
|
return payload[0]
|
|
|
|
|
|
def _delivery_id_for(payload: object, wanted: str) -> object | None:
|
|
"""Return the delivery id whose date is ``wanted``.
|
|
|
|
Args:
|
|
payload: The decoded order.
|
|
wanted: An ISO ``YYYY-MM-DD`` date.
|
|
|
|
Returns:
|
|
The delivery id, or None when that day has no (undeleted) delivery.
|
|
"""
|
|
if not isinstance(payload, dict):
|
|
return None
|
|
deliveries = payload.get("deliveries")
|
|
if not isinstance(deliveries, list):
|
|
return None
|
|
for delivery in deliveries:
|
|
if not isinstance(delivery, dict):
|
|
continue
|
|
# ``deleted`` marks a cancelled day; it still appears in the list.
|
|
if delivery.get("deleted"):
|
|
continue
|
|
if str(delivery.get("date")) == wanted:
|
|
return delivery.get("deliveryId")
|
|
return None
|
|
|
|
|
|
def fetch_dishes(session: PanelSession, day: date) -> list[Dish]:
|
|
"""Return the dishes delivered on ``day``.
|
|
|
|
Args:
|
|
session: An authenticated session.
|
|
day: The delivery date to fetch.
|
|
|
|
Returns:
|
|
The day's dishes, empty when nothing is delivered that day.
|
|
|
|
Raises:
|
|
KuchniaError: When the panel cannot be read.
|
|
"""
|
|
order_id = _active_order_id(session)
|
|
if order_id is None:
|
|
return []
|
|
order = session.get_json(f"company/customer/order/{order_id}")
|
|
delivery_id = _delivery_id_for(order, day.isoformat())
|
|
if delivery_id is None:
|
|
return []
|
|
menu = session.get_json(f"company/general/menus/delivery/{delivery_id}/new")
|
|
dishes = parse_menu(menu)
|
|
if not dishes:
|
|
# A delivery exists but nothing survived parsing: either the menu is
|
|
# not published yet, or its units failed the consistency check. Both
|
|
# are "no data", not a crash -- but they are worth distinguishing from
|
|
# "no delivery" in the caller's message.
|
|
msg = f"catering menu for {day.isoformat()} has no usable dishes"
|
|
raise KuchniaError(msg)
|
|
return dishes
|