Migrate Sheets auth from an API key to a service-account credential
(GOOGLE_CREDENTIALS_FILE) with the read+write spreadsheets scope, add a
WriteSheet primitive, and use it to actively manage empty duty weeks in
the weekly reminder.
Each fire now handles a fixed this-week/next-week model (replacing the
DUTY_AHEAD_DAYS announce):
- this week empty -> pick the two people with the fewest duties (random
tie-break), write their names into cols B/C, and announce
- this week filled -> announce who is on duty
- next week empty -> ask people to volunteer before it is auto-assigned
Col G ("Liczba dyżurów") is a live formula, so names-only writes keep the
fewest-duties selection fair across weeks. Selection, row tracking, range
building, and every fire() branch are unit-tested; live tests for sheet
read/write are env-gated.
Resolve the current-year sheet tab per fetch instead of freezing it at
startup, so a long-running process follows the New Year rollover without a
restart. Guard against the regression this introduces: a successful fetch
that returns no duty weeks (e.g. a freshly created, empty next-year tab) now
keeps the last-good cache instead of clobbering it with empty data.
Surface a silently-stale cache — which is not a crash, so process-level
supervision can't catch it — by escalating to a loud error log once no sync
has succeeded for 6h (new Store.LastSynced accessor backs the check).
Throttle manual /sync to once per minute across the chat to avoid spamming
the Google Sheets API; the check-and-set is mutex-guarded since Telegram
dispatches each update in its own goroutine.
Update README accordingly.
Read each member's Telegram handle from the new sheet column F (duty
count moves to G) and mention on-duty people as @handle in the weekly
reminder, so they get a real notification. Names without a handle, or
not found in the people list, fall back to plain text.
- sheet.go: add colHandle, Person.Handle; parse handle column
- scheduler.go: resolve names to @mentions via handlesByName/mention
- service.go: widen default SHEET_RANGE to column G
- add per-request timeout to the Sheets fetch so a hung call can't block /sync
- stop leaking raw sync errors to chat; log detail, reply generically
- migrate logging from log to structured log/slog
- drop duplicate godotenv.Load from ReadSheet (LoadConfig loads .env at startup)
- add multi-stage distroless Dockerfile + .dockerignore; embed tzdata via import
- document single-instance long-polling constraint in README
- translate sheet.go comments to English
- move package doc to doc.go; remove superseded root main.go and live_test.go
Add Reminder that posts a weekly cleaning-duty reminder to a Telegram
group on a configurable schedule (REMINDER_WEEKDAY/REMINDER_HOUR), in
Europe/Warsaw time, announcing the duty DUTY_AHEAD_DAYS out. Reminders
are disabled unless GROUP_CHAT_ID is set. Wire ReminderConfig parsing
into LoadConfig with validation, document the commands and reminder
config in README, and cover config loading with tests.
LoadConfig read SPREADSHEET_ID, but the .env defines GOOGLE_SHEETS_ID,
so config loading failed. The default SHEET_RANGE was also a hardcoded
'Dyżury!A1:F100', but the spreadsheet tabs are named per year (e.g.
'2026'), producing a 400 'Unable to parse range'. Read GOOGLE_SHEETS_ID
and derive the default range from the current year.
Add a RUN_LIVE-gated integration test that drives the full
LoadConfig -> Service -> Syncer.Sync -> Store path against the live
Sheet, and gitignore the generated rota.json cache.