mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-11 14:07:40 +01:00
The largest consumer moves last on purpose: diet_guard was 71% of all sync traffic, and both its surfaces (the PC timer and the phone app) share one prefix, so they move together. Firebase becomes the primary backend when it is set up, with GitHub kept as a mirror so a device that has not moved yet still converges. An unconfigured device keeps syncing exactly as before, which is also the rollback. Revision tracking is new here. The PC hand-rolled its own pull-merge-push instead of calling sync_log, so it had none: every tick re-downloaded every peer's whole food log -- hundreds of KB, 96 times a day -- and re-pushed its own regardless of change. It now reads one small revision map first, skips any peer whose hash is unchanged, and suppresses a no-op push. Every sub-sync (log, budget, food bank, manual bank) takes the client as a parameter, so one swap covers all four. The app could not have reached Firebase at all: nothing called saveAccount(), so loadAccount() always returned null and openFirebase() always returned null. Settings now has a Firebase section that stores the account in the OS keystore and signs in immediately, so a typo surfaces on the screen where it can be fixed rather than as a silently failed background sync hours later. The email and password never touch the repo -- it is public. Platform reaches now degrade instead of failing: an unavailable keystore or application-support directory costs one tick of extra traffic, where before it would have taken sync down entirely. Caught by `on Object` rather than `on Exception`, because an uninitialised binding raises an Error. Verified end to end against the real database, not just in tests: a live PC sync merged 57 days / 339 entries and put 221 KB of food log plus 97 food-bank records into Firebase; a second unchanged tick issued zero pushes; and the Dart client signed in and read back all 339 meals and 97 foods, then completed a push / publish-revision / suppress-no-op round trip under a throwaway prefix. diet_guard: 618 tests at 100% branch coverage. diet_guard_app: 477 tests. Two assertions moved from 4 pushes to 5 -- the extra one is this device's revision, published after the log so a peer can never cache "seen rev X" against a log it never received. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SJ9sxEw3ymzXNjL7SHNpJc
97 lines
5.5 KiB
Python
97 lines
5.5 KiB
Python
"""Constants for the diet_guard calorie tracker and gate."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
# --- Daily target -----------------------------------------------------------
|
|
# There is deliberately NO budget number here. It is computed once from
|
|
# biometrics at ``init`` time, written to BUDGET_FILE below, and freely
|
|
# editable and synced afterward from either device -- see _budget.py and
|
|
# _sync.py. Read via diet_guard._budget.daily_budget() for over/under
|
|
# decisions; freely shown in local CLI/GUI/app output.
|
|
#
|
|
# Fraction of the budget at which status flips from "on track" to "approaching
|
|
# limit". Also mirrored above 100% as the day-status yellow/red boundary for
|
|
# the calendar -- see _daystatus.py.
|
|
BUDGET_WARN_FRACTION: float = 0.80
|
|
|
|
# --- Storage ----------------------------------------------------------------
|
|
# The food log is personal and high-churn, so it lives in the XDG data dir and
|
|
# is deliberately NOT committed to git (unlike wake_state.json).
|
|
DATA_DIR: Path = Path.home() / ".local" / "share" / "diet_guard"
|
|
FOOD_LOG_FILE: Path = DATA_DIR / "food_log.json"
|
|
|
|
# Revision cache for cross-device sync. Beside the log it describes and
|
|
# cleared with it: skipping an unchanged peer is only sound because that
|
|
# peer's records are already merged into the local log, so state that outlived
|
|
# its log would skip peers whose data had been lost.
|
|
SYNC_STATE_FILE: Path = DATA_DIR / "sync_state.json"
|
|
# The user's personal "food bank": every food they have logged before, with its
|
|
# full macros, keyed by name. This is the ONLY corpus the gate's autocomplete
|
|
# searches -- Open Food Facts is used to *fill* a new food's macros, never to
|
|
# search. Local-only, git-ignored.
|
|
FOOD_BANK_FILE: Path = DATA_DIR / "food_bank.json"
|
|
# Hand-curated bank entries (see _foodbank_manual.py): foods added without
|
|
# ever logging them, so they are NOT derivable from the food log and must
|
|
# sync in their own right. Kept separate from FOOD_BANK_FILE because that
|
|
# one is rewritten wholesale on every log write.
|
|
MANUAL_BANK_FILE: Path = DATA_DIR / "food_bank_manual.json"
|
|
# The budget: a plain JSON dotfile alongside the log, freely editable on this
|
|
# device or the phone app and synced between them (see _sync.py).
|
|
# Git-ignored, never committed. "Hidden" here means never-online (it lives
|
|
# outside the repo) -- the number itself is shown freely in local CLI/GUI/app
|
|
# output.
|
|
BUDGET_FILE: Path = DATA_DIR / ".budget"
|
|
# The effective-from history of that budget (see _budget_history.py), kept in
|
|
# its own file so ``.budget``'s schema stays at v2 and every existing reader
|
|
# of it is untouched. Classifying a *past* day reads this; everything about
|
|
# *today* still reads BUDGET_FILE. Git-ignored, never committed.
|
|
BUDGET_HISTORY_FILE: Path = DATA_DIR / ".budget_history"
|
|
|
|
# --- Estimator (Open Food Facts) -------------------------------------------
|
|
# The default backend is Open Food Facts' "Search-a-licious" full-text search:
|
|
# free, no key, strongest for branded/packaged foods (including fast food).
|
|
# (The older cgi/search.pl endpoint is heavily rate-limited and returns an HTML
|
|
# "temporarily unavailable" page to API clients, and /api/v2/search ignores the
|
|
# query term, so neither is usable here.) Swappable for a local/remote LLM
|
|
# backend later without touching the log or CLI layers.
|
|
OFF_SEARCH_URL: str = "https://search.openfoodfacts.org/search"
|
|
OFF_TIMEOUT_SECONDS: float = 8.0
|
|
OFF_PAGE_SIZE: int = 5
|
|
# Open Food Facts asks API clients to identify themselves with a descriptive
|
|
# User-Agent string so abusive clients can be told apart from polite ones.
|
|
OFF_USER_AGENT: str = "diet_guard/1.0 (personal diet tracker)"
|
|
# Portion assumed when neither --grams nor an OFF serving size is available.
|
|
DEFAULT_PORTION_GRAMS: float = 100.0
|
|
|
|
# --- Gate (log-to-unlock) ---------------------------------------------------
|
|
# The gate is driven by FIXED MEAL SLOTS, not by a gap timer. Starting at the
|
|
# day-start hour, a slot opens every interval; once a slot's hour has passed,
|
|
# that slot must carry a logged meal or the screen locks until it does. This
|
|
# makes tracking fully automatic (you are prompted on a schedule rather than
|
|
# trusted to log voluntarily) and nudges regular eating. Coming home late
|
|
# naturally produces several unlogged elapsed slots at once -> one lock that
|
|
# backfills the whole day, which is the "requirement to access the PC" behavior.
|
|
GATE_DAY_START_HOUR: int = 8 # first slot (08:00); also the "beginning of day"
|
|
GATE_SLOT_INTERVAL_HOURS: int = 4 # slots at 08:00, 12:00, 16:00, 20:00
|
|
# Past this hour the gate never fires, so an unlogged late slot lapses quietly
|
|
# instead of locking you out overnight. (A new day resets all slots at 00:00.)
|
|
GATE_EATING_END_HOUR: int = 22 # exclusive (22:00)
|
|
# flock single-instance guard: stops a timer from stacking lock windows.
|
|
GATE_LOCK_FILE: Path = DATA_DIR / ".gate.lock"
|
|
|
|
# --- Sync (cross-device log merge) ------------------------------------------
|
|
# GitHub is used purely as dumb file storage via the REST Contents API (not a
|
|
# git clone) -- mirrors ~/todo's sync transport. Each device pushes its own
|
|
# full current log as one file under devices/<id>/food_log.json; merging
|
|
# happens client-side (see _sync_merge.py), never via git.
|
|
SYNC_REPO_OWNER: str = "kuhyx"
|
|
SYNC_REPO_NAME: str = "syncs"
|
|
SYNC_DEVICE_ID: str = "pc"
|
|
# A fine-grained GitHub PAT, scoped to just SYNC_REPO_NAME's contents. The
|
|
# user creates this once via github.com (see CLAUDE.md) and saves it here,
|
|
# mode 600. Never committed -- this path is outside the repo entirely.
|
|
SYNC_TOKEN_FILE: Path = Path.home() / ".config" / "diet_guard" / "sync_token"
|
|
SYNC_TIMEOUT_SECONDS: float = 10.0
|