feat(restart): host-resolvable timezone dropdown + heal-on-read#103
Merged
Conversation
Codecov Report❌ Patch coverage is
📢 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>
0d1f30d to
43b5046
Compare
5 tasks
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The graceful-restart feature called
TimeZoneInfo.FindSystemTimeZoneByIdwith 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
LoadPersistedSettingsAfter deserializing, probe the configured timezone via
FindSystemTimeZoneById. If it doesn't resolve (or is null/empty), fall back toTimeZoneInfo.Local.Id(TimeZoneInfo.Utc.IdifLocalhas 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/timezonesendpointProjects
TimeZoneInfo.GetSystemTimeZones()to{ id, displayName, baseUtcOffsetMinutes }, sorted byBaseUtcOffsetthenDisplayName. The IDs returned are whatever the runtime accepts back throughFindSystemTimeZoneByIdon 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.vueswaps theInputTextfor a PrimeVueSelectbound to that endpoint withoptionLabel=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.cscovers 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
GracefulRestartFeatureTestspass🤖 Generated with Claude Code