Vacation mode

Correspondence vacation pauses a player’s turn clock across all active games while a stint is live, using 14 × 24-hour blocks (336 hours) of pause time per UTC quota year. This page describes how the feature is implemented today (backend + front).

Single path for clock state

See Games and moves for lazy timeout collapse (me_dashboard, get_game, timeout move).

Product rules (summary)

Rule Detail
Scope Universal (casual, tournament, event, solo; hard and soft clocks)
Allotment 14 × 86_400_000 ms pause per UTC quota year
Start Immediate or scheduled future datetime
End Fixed end (editable until passed) or open-ended until stop or quota exhausted
Stop Anytime; cancel scheduled stint before start without charge

Policy gate: lib/vacation/policy.ts (vacationSchedulePolicyError — extensible; no game-type blocks today).


Storage and sync

Authoritative stint lives on the private USER row (vacationStartsAt, vacationEndsAt, vacationOpenEnded, vacationStintStartedAt, vacationPauseMsUsed, vacationQuotaYear). Writes go through lib/vacation/mutations.ts; lazy end-of-stint / quota rollover via lib/vacation/persist.ts (finalizeVacationIfNeeded).

Public directory mirror: When a stint is live (isVacationStintLive), lib/vacation/publicMirror.ts sets USERS.onVacation = true; otherwise it REMOVEs the attribute. Scheduled-but-not-started stints do not set the mirror. Sync runs after successful schedule/update/stop, after finalize writes, and when player_about loads a human profile (keeps directory aligned on profile views).

Surface What clients see
user_names Optional onVacation: true on human entries (lib/public/catalog.ts)
player_about { about?, vacation? } — vacation is a full snapshot (scheduled/active, times, open-ended, blocks/quota remaining); omitted when no stint on file
me_profile / me_dashboard vacation snapshot on the authenticated user
Dashboard games / get_game Per-player onVacation / vacationScheduled; on-clock players also get effectiveRemainingMs, clockPaused, plus clockDisplayServerTime on the game slice

See Public queries (user_names, player_about) and Auth queries — Vacation.


Auth API

Query Handler
schedule_vacation lib/vacation/authHandlers.ts → mutations.ts
update_vacation same
stop_vacation same

Success body: { vacation: VacationSnapshot }. Validation failures return 400 with { message: "<code>" } (vacation_stint_active, vacation_no_quota, vacation_invalid_range, etc.).


Core modules (node-backend)

Module Role
lib/vacation/resolve.ts Stint state, snapshots, live-window detection
lib/vacation/clockDisplay.ts Display seeds for games (effectiveRemainingMs, pause flags)
lib/vacation/dashboardClock.ts Dashboard game slice enrichment
lib/clockElapsed.ts Effective elapsed ms with vacation overlap

Server paths that derive live clock

File Notes
lib/gameTimeout.ts Lazy sweep + get_game; remainingBankMs / wouldTimeOut + vacation windows
lib/games/playHandlers.ts Move timeUsed, pie paths, opponent timeout()
lib/games/timeloss.ts Auth timeloss check
lib/profile/me.ts nextGame urgency sort
utils/yourturn.ts Email cron (< 24h urgent)
lib/games/getGame.ts Game payload clock display + player vacation flags
lib/meQuery.ts / lib/profile/me.ts me_* vacation snapshot + dashboard game fields

Documented non-paths (no live turn-clock math)

File Reason
lib/challenges/authHandlers.ts, lib/tournaments/createTournamentPairingGame.ts, lib/events/authHandlers.ts, lib/games/solo.ts Initial bank at create
lib/games/playHandlers.ts abandoned 30-day activity heuristic
lib/gameProjector.ts lastMoveTime in index sk only
lib/botOutbound.ts Stored bank hours for bot API
lib/dashboardGames.ts, lib/playerGameMarks.ts Pass-through / summary
crons/src/functions/starttournaments.ts, crons/src/functions/standingchallenges.ts Types / challenge metadata
crons/src/functions/records-move-times.ts Analytics on recorded move times

Tests: test/vacation.test.ts, test/clockElapsed.test.ts, test/vacationMutations.test.ts, test/vacationPublicMirror.test.ts, test/gameTimeout.test.ts (vacation case).


Front (apfront)

Server seeds: clockDisplayServerTime, per on-clock player effectiveRemainingMs / clockPaused, and onVacation / vacationScheduled on every player in game/dashboard payloads. Public user_names.onVacation drives list/challenge badges without an extra profile fetch; player_about.vacation drives profile copy.

Area Files
Clock display front/src/lib/gameClockDisplay.js; chips in GameMove/preview/moveEntryUtils.js, CardTurnBar.js; timeloss in MoveEntry.js, useDockMoveEntry.js, useGameMoveSession.js
Dashboard Me/MyTurnTable.js, TheirTurnTable.js, Me.js
Settings UserSettingsModal.js, VacationSettingsPanel.js, lib/vacationApi.js
Public indicators OnVacationBadge.js, Players.js, Player.js, challenge pickers; profile detail via PlayerAboutSection.js, PlayerVacationDetail.js, lib/playerVacationTooltip.js

Challenge settings modals (start/inc/max/hard) and “last activity” timestamps are unrelated to live vacation elapsed math.

Follow-up: optional bin/check-clock-derivation.mjs in the front repo mirroring backend guard patterns.


Regression guard

From node-backend root:

npm run check:clock-derivation
npm run check:clock-derivation -- --strict

--strict exits non-zero when non-allowlisted files under lib/ and utils/ match forbidden live-clock patterns. The allowlist is empty for production clock paths.