Around New Year the empty-rota guard keeps the old year's cached rows
while resolveRange has already moved to the new year's tab, so an
auto-assignment could write an old-tab row index onto the new tab.
Stamp each DutyWeek with the tab it was fetched from and prefer it when
building the assignment range (clock fallback for pre-upgrade caches).
Also: widen the default fetch range to A1:G200 so a full year of rows
plus roster can't be silently truncated, add -race/golangci-lint/
govulncheck gates to CI (container pinned to 1.26.4-alpine), and add
.env.example documenting every configuration variable.
The bot now answers only its predefined commands and stays silent on
anything else, logging ignored commands at debug level.
Tests no longer assert exact reply/reminder wording (which broke on
copy tweaks like 37a81a2) — they check behavior instead: message
counts, the names/handles substituted in, and that /sync persists its
fetch and doesn't leak internal errors.
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.
- 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.