Skip to content

feat(restart): host-resolvable timezone dropdown + heal-on-read#103

Merged
AdaInTheLab merged 1 commit into
mainfrom
feat/timezone-dropdown
Jun 4, 2026
Merged

feat(restart): host-resolvable timezone dropdown + heal-on-read#103
AdaInTheLab merged 1 commit into
mainfrom
feat/timezone-dropdown

Conversation

@AdaInTheLab

Copy link
Copy Markdown
Collaborator

Summary

The graceful-restart feature called TimeZoneInfo.FindSystemTimeZoneById with whatever string was in the persisted config. On .NET Framework 4.8 / Windows, IANA IDs like "America/Los_Angeles" don't resolve, so the 30-second tick threw, the catch logged "bad schedule config" forever, and the daily restart never fired. Observed in the wild with "ScheduledTimezone":"UTC-5" — not even an IANA ID — where nobody realized the feature was a silent no-op. The free-text UI labeled "TIMEZONE (IANA)" was actively setting users up to type in strings the host couldn't parse.

Two-part fix

1. Heal-on-read in LoadPersistedSettings

After deserializing, probe the configured timezone via FindSystemTimeZoneById. If it doesn't resolve (or is null/empty), fall back to TimeZoneInfo.Local.Id (TimeZoneInfo.Utc.Id if Local has no Id), log one INFO line about the heal, and persist the corrected JSON back to the settings row.

The next boot is clean — no spam, no recurring warning, no second heal. The tick path itself is unchanged: it still gates on FindSystemTimeZoneById, so a hand-edited bad value in the DB between boots is still caught and warned about (just now without the silent-feature-failure mode).

2. New GET /api/server/timezones endpoint

Projects TimeZoneInfo.GetSystemTimeZones() to { id, displayName, baseUtcOffsetMinutes }, sorted by BaseUtcOffset then DisplayName. The IDs returned are whatever the runtime accepts back through FindSystemTimeZoneById on THIS host — Windows registry IDs on .NET Framework / Windows, IANA on .NET Core / Linux. That's the point: we hand the panel a list of strings we know will round-trip, and the panel saves the raw Id verbatim. Authorize-only, no role gate — same as /api/server/info, since the list is host metadata, not anything sensitive.

3. Frontend

SettingsView.vue swaps the InputText for a PrimeVue Select bound to that endpoint with optionLabel=displayName, optionValue=id, filter enabled (the list can be 100+ entries). Editable mode is only enabled if the fetch fails so admins can still hand-type a value as an escape hatch. i18n strings updated across all eight locales (en/de/fr/es translated; ja/ko/zh-CN/zh-TW carry English placeholders pending translation — consistent with the existing pattern in this file).

Tests

src/KitsuneCommand.Tests/Services/GracefulRestartFeatureTests.cs covers both paths: unresolvable timezone heals to Local and persists exactly once; resolvable timezone passes through without touching the DB so we don't churn the settings row on every boot.

Test plan

  • Build passes on Windows .NET Framework 4.8
  • New GracefulRestartFeatureTests pass
  • Deploy to a Windows box with a stored IANA-format TZ — confirm INFO heal-on-read line + DB row updated to a Windows TZ ID
  • Open Settings → Server Restart in browser — confirm dropdown populates with host-resolvable IDs, current selection survives reload
  • Sanity: still works on Linux (IANA IDs round-trip natively)

🤖 Generated with Claude Code

@codecov

codecov Bot commented Jun 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 63.63636% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
frontend/src/views/SettingsView.vue 66.66% 3 Missing ⚠️
frontend/src/api/server.ts 50.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

The graceful-restart feature called TimeZoneInfo.FindSystemTimeZoneById
with whatever string was in the persisted config. On .NET Framework 4.8
/ Windows, IANA IDs like "America/Los_Angeles" don't resolve, so the
30-second tick threw, the catch logged "bad schedule config" forever,
and the daily restart never fired. Observed in the wild with
"ScheduledTimezone":"UTC-5" — not even an IANA ID — where nobody
realized the feature was a silent no-op. The free-text UI labeled
"TIMEZONE (IANA)" was actively setting users up to type in strings
the host couldn't parse.

Two-part fix:

1. Heal-on-read in LoadPersistedSettings. After deserializing, probe
   the configured timezone via FindSystemTimeZoneById. If it doesn't
   resolve (or is null/empty), fall back to TimeZoneInfo.Local.Id
   (TimeZoneInfo.Utc.Id if Local has no Id), log one INFO line about
   the heal, and persist the corrected JSON back to the settings row.
   The next boot is clean — no spam, no recurring warning, no second
   heal. The tick path itself is unchanged: it still gates on
   FindSystemTimeZoneById, so a hand-edited bad value in the DB
   between boots is still caught and warned about (just now without
   the silent-feature-failure mode).

2. New GET /api/server/timezones endpoint. Projects
   TimeZoneInfo.GetSystemTimeZones() to { id, displayName,
   baseUtcOffsetMinutes }, sorted by BaseUtcOffset then DisplayName.
   The IDs returned are whatever the runtime accepts back through
   FindSystemTimeZoneById on THIS host — Windows registry IDs on
   .NET Framework / Windows, IANA on .NET Core / Linux. That's the
   point: we hand the panel a list of strings we know will round-
   trip, and the panel saves the raw Id verbatim. Authorize-only,
   no role gate — same as /api/server/info, since the list is host
   metadata, not anything sensitive.

3. SettingsView.vue swaps the InputText for a PrimeVue Select bound
   to that endpoint with optionLabel=displayName, optionValue=id,
   filter enabled (the list can be 100+ entries). Editable mode is
   only enabled if the fetch fails so admins can still hand-type a
   value as an escape hatch. i18n strings updated across all eight
   locales (en/de/fr/es translated; ja/ko/zh-CN/zh-TW carry English
   placeholders pending translation — consistent with the existing
   pattern in this file).

Tests in src/KitsuneCommand.Tests/Services/GracefulRestartFeatureTests.cs
cover both paths: unresolvable timezone heals to Local and persists
exactly once; resolvable timezone passes through without touching the
DB so we don't churn the settings row on every boot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@AdaInTheLab AdaInTheLab force-pushed the feat/timezone-dropdown branch from 0d1f30d to 43b5046 Compare June 4, 2026 00:20
@AdaInTheLab AdaInTheLab merged commit f247529 into main Jun 4, 2026
3 checks passed
@AdaInTheLab AdaInTheLab deleted the feat/timezone-dropdown branch June 4, 2026 00:21
AdaInTheLab added a commit that referenced this pull request Jun 4, 2026
Cuts v2.8.2. Three fixes from the live Windows prod box plus the
timezone field on the Server Restart panel grows up into a host-
resolvable dropdown. No new pages, no schema changes — patch release
per docs/RELEASES.md conventions.

What lands:

- #101 fix(websocket): stop LogCallbackEvent broadcast recursion in
  shutdown. ThreadStatic re-entrancy guard + suppressFailureLogging
  fallback to Console.Error so a failed log-broadcast can't fire a
  fresh LogCallbackEvent and recurse to stack overflow.
- #102 fix(restart): skip systemctl probe on Windows; route through
  OS-aware strategy. New Core/OsRestartStrategy.cs picks per OS;
  Windows goes straight to in-game shutdown + NSSM AppExit Restart
  bounce, no more wasted 5s probe or misleading warning.
- #103 feat(restart): host-resolvable timezone dropdown + heal-on-read.
  LoadPersistedSettings heals an unresolvable TZ ID to TimeZoneInfo
  .Local and persists. New GET /api/server/timezones endpoint feeds a
  PrimeVue Select in Settings → Server Restart, replacing the free-
  text input that admins kept filling with strings the host couldn't
  parse.

Version bumps:
- src/KitsuneCommand/ModInfo.xml: 2.8.1 → 2.8.2
- frontend/package.json: 2.7.4 → 2.8.2 (reconciles prior drift —
  frontend version was stuck at 2.7.4 since v2.8.0)
- CHANGELOG.md: promote [Unreleased] → [2.8.2] - 2026-06-04

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant