diff --git a/CHANGELOG.md b/CHANGELOG.md index 1581864..205b4d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,26 @@ pulls notes from — it's the minimum, the GitHub release page is the maximum. command `krestart` already uses the OS-agnostic `GracefulRestartFeature.TriggerNow` (no shell-out) so it didn't need to change. +- **Graceful restart timezone is now host-resolvable.** The + GracefulRestart feature called + `TimeZoneInfo.FindSystemTimeZoneById` with whatever string was in + the persisted config. On .NET Framework 4.8 / Windows, IANA IDs + like `America/Los_Angeles` aren't recognized, so the tick threw, + the catch block logged "bad schedule config" every 30 seconds, and + the daily restart never fired. Observed in the wild with a value + of `"UTC-5"` (not even an IANA ID) where nobody realized the + feature was a no-op. Two-part fix: (1) `LoadPersistedSettings` + now probes the configured timezone, and if it doesn't resolve, + falls back to `TimeZoneInfo.Local` (or `Utc` if Local is unset), + logs one INFO line, and persists the corrected value back to the + DB so the next boot is clean. (2) New `GET /api/server/timezones` + endpoint returns the runtime-resolvable timezone list sorted by + UTC offset; the Settings → Server Restart panel now renders a + PrimeVue `Select` populated from that endpoint instead of a + free-text input that admins would fill with strings the host + couldn't parse. Saves `id` (whatever the runtime accepts: + Windows registry IDs on .NET Framework, IANA on .NET Core), + shows `displayName` to the user. ## [2.8.1] - 2026-05-29 diff --git a/frontend/src/api/server.ts b/frontend/src/api/server.ts index 406e69c..9ddf8c7 100644 --- a/frontend/src/api/server.ts +++ b/frontend/src/api/server.ts @@ -51,3 +51,21 @@ export async function getDashboardStats(): Promise { const response = await apiClient.get('/api/dashboard/stats') return response.data.data } + +/** + * One entry from GET /api/server/timezones. The `id` is whatever string the + * runtime will accept back via TimeZoneInfo.FindSystemTimeZoneById — Windows + * registry IDs on .NET Framework / Windows ("Pacific Standard Time"), IANA + * IDs on .NET Core / Linux ("America/Los_Angeles"). Don't try to interpret + * it; just round-trip it. + */ +export interface TimezoneOption { + id: string + displayName: string + baseUtcOffsetMinutes: number +} + +export async function getTimezones(): Promise { + const response = await apiClient.get('/api/server/timezones') + return response.data.data +} diff --git a/frontend/src/i18n/locales/de.ts b/frontend/src/i18n/locales/de.ts index dccb17f..0833f55 100644 --- a/frontend/src/i18n/locales/de.ts +++ b/frontend/src/i18n/locales/de.ts @@ -877,8 +877,9 @@ const de = { serverRestartEnableHint: 'Wenn aus, pausieren geplante Neustarts. „Jetzt neu starten“ funktioniert weiterhin.', serverRestartTime: 'Tageszeit (HH:mm)', serverRestartTimeHint: 'Wanduhrzeit, zu der das finale Herunterfahren erfolgt. Warnungen beginnen früher gemäß der unten stehenden Stufenliste.', - serverRestartTimezone: 'Zeitzone (IANA)', - serverRestartTimezoneHint: 'z. B. America/Los_Angeles, Europe/Berlin, UTC. Sommerzeit wird automatisch berücksichtigt.', + serverRestartTimezone: 'Zeitzone', + serverRestartTimezonePlaceholder: 'Zeitzone auswählen', + serverRestartTimezoneHint: 'Die Liste stammt vom Server-Host. Sommerzeit wird automatisch berücksichtigt.', serverRestartLadder: 'Warnungs-Stufen', serverRestartLadderSubtitle: 'Jede Stufe sendet eine {minutes}-bewusste Nachricht N Minuten vor dem tatsächlichen Herunterfahren. Die 0-Minuten-Stufe ist die finale „Jetzt herunterfahren“-Zeile.', serverRestartColMinutes: 'Min. davor', diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index d2d4e67..e8d8504 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -898,8 +898,9 @@ const en = { serverRestartEnableHint: 'When off, scheduled restarts pause. Manual "Restart Now" still works.', serverRestartTime: 'Time of day (HH:mm)', serverRestartTimeHint: 'Wall-clock time the final shutdown fires. Warnings start earlier per the ladder below.', - serverRestartTimezone: 'Timezone (IANA)', - serverRestartTimezoneHint: 'e.g. America/Los_Angeles, Europe/Berlin, UTC. DST shifts handled automatically.', + serverRestartTimezone: 'Timezone', + serverRestartTimezonePlaceholder: 'Select a timezone', + serverRestartTimezoneHint: 'List comes from the server host. DST shifts are handled automatically.', serverRestartLadder: 'Warning ladder', serverRestartLadderSubtitle: 'Each step broadcasts a {minutes}-aware message N minutes before the actual shutdown. The 0-minute step is the final "going down now" line.', serverRestartColMinutes: 'Min before', diff --git a/frontend/src/i18n/locales/es.ts b/frontend/src/i18n/locales/es.ts index 490af83..9303146 100644 --- a/frontend/src/i18n/locales/es.ts +++ b/frontend/src/i18n/locales/es.ts @@ -877,8 +877,9 @@ const es = { serverRestartEnableHint: 'Cuando está apagado, los reinicios programados se pausan. «Reiniciar ahora» sigue funcionando.', serverRestartTime: 'Hora del día (HH:mm)', serverRestartTimeHint: 'Hora de reloj a la que se ejecuta el apagado final. Los avisos comienzan antes según la lista de abajo.', - serverRestartTimezone: 'Zona horaria (IANA)', - serverRestartTimezoneHint: 'p. ej. America/Los_Angeles, Europe/Madrid, UTC. Los cambios de horario de verano se gestionan automáticamente.', + serverRestartTimezone: 'Zona horaria', + serverRestartTimezonePlaceholder: 'Seleccione una zona horaria', + serverRestartTimezoneHint: 'La lista proviene del host del servidor. Los cambios de horario de verano se gestionan automáticamente.', serverRestartLadder: 'Lista de avisos', serverRestartLadderSubtitle: 'Cada paso difunde un mensaje consciente de {minutes}, N minutos antes del apagado real. El paso de 0 minutos es la línea final «Apagando ahora».', serverRestartColMinutes: 'Min. antes', diff --git a/frontend/src/i18n/locales/fr.ts b/frontend/src/i18n/locales/fr.ts index 3a10c56..90eae86 100644 --- a/frontend/src/i18n/locales/fr.ts +++ b/frontend/src/i18n/locales/fr.ts @@ -877,8 +877,9 @@ const fr = { serverRestartEnableHint: 'Désactivé, les redémarrages planifiés sont en pause. « Redémarrer maintenant » fonctionne toujours.', serverRestartTime: 'Heure de la journée (HH:mm)', serverRestartTimeHint: 'Heure de l\'horloge à laquelle l\'arrêt final se déclenche. Les avertissements commencent plus tôt selon la liste ci-dessous.', - serverRestartTimezone: 'Fuseau horaire (IANA)', - serverRestartTimezoneHint: 'p. ex. America/Los_Angeles, Europe/Paris, UTC. Les changements d\'heure sont gérés automatiquement.', + serverRestartTimezone: 'Fuseau horaire', + serverRestartTimezonePlaceholder: 'Sélectionner un fuseau horaire', + serverRestartTimezoneHint: 'La liste provient de l\'hôte du serveur. Les changements d\'heure sont gérés automatiquement.', serverRestartLadder: 'Liste d\'avertissements', serverRestartLadderSubtitle: 'Chaque étape diffuse un message conscient de {minutes}, N minutes avant l\'arrêt réel. L\'étape 0 minute est la dernière ligne « Arrêt en cours ».', serverRestartColMinutes: 'Min. avant', diff --git a/frontend/src/i18n/locales/ja.ts b/frontend/src/i18n/locales/ja.ts index 7f5911d..0d485d2 100644 --- a/frontend/src/i18n/locales/ja.ts +++ b/frontend/src/i18n/locales/ja.ts @@ -901,8 +901,9 @@ const ja: Messages = { serverRestartEnableHint: 'When off, scheduled restarts pause. Manual "Restart Now" still works.', serverRestartTime: 'Time of day (HH:mm)', serverRestartTimeHint: 'Wall-clock time the final shutdown fires. Warnings start earlier per the ladder below.', - serverRestartTimezone: 'Timezone (IANA)', - serverRestartTimezoneHint: 'e.g. America/Los_Angeles, Europe/Berlin, UTC. DST shifts handled automatically.', + serverRestartTimezone: 'Timezone', + serverRestartTimezonePlaceholder: 'Select a timezone', + serverRestartTimezoneHint: 'List comes from the server host. DST shifts are handled automatically.', serverRestartLadder: 'Warning ladder', serverRestartLadderSubtitle: 'Each step broadcasts a {minutes}-aware message N minutes before the actual shutdown. The 0-minute step is the final "going down now" line.', serverRestartColMinutes: 'Min before', diff --git a/frontend/src/i18n/locales/ko.ts b/frontend/src/i18n/locales/ko.ts index b597715..a8cee31 100644 --- a/frontend/src/i18n/locales/ko.ts +++ b/frontend/src/i18n/locales/ko.ts @@ -901,8 +901,9 @@ const ko: Messages = { serverRestartEnableHint: 'When off, scheduled restarts pause. Manual "Restart Now" still works.', serverRestartTime: 'Time of day (HH:mm)', serverRestartTimeHint: 'Wall-clock time the final shutdown fires. Warnings start earlier per the ladder below.', - serverRestartTimezone: 'Timezone (IANA)', - serverRestartTimezoneHint: 'e.g. America/Los_Angeles, Europe/Berlin, UTC. DST shifts handled automatically.', + serverRestartTimezone: 'Timezone', + serverRestartTimezonePlaceholder: 'Select a timezone', + serverRestartTimezoneHint: 'List comes from the server host. DST shifts are handled automatically.', serverRestartLadder: 'Warning ladder', serverRestartLadderSubtitle: 'Each step broadcasts a {minutes}-aware message N minutes before the actual shutdown. The 0-minute step is the final "going down now" line.', serverRestartColMinutes: 'Min before', diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index ccf476b..c7a2247 100644 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -901,8 +901,9 @@ const zhCN: Messages = { serverRestartEnableHint: 'When off, scheduled restarts pause. Manual "Restart Now" still works.', serverRestartTime: 'Time of day (HH:mm)', serverRestartTimeHint: 'Wall-clock time the final shutdown fires. Warnings start earlier per the ladder below.', - serverRestartTimezone: 'Timezone (IANA)', - serverRestartTimezoneHint: 'e.g. America/Los_Angeles, Europe/Berlin, UTC. DST shifts handled automatically.', + serverRestartTimezone: 'Timezone', + serverRestartTimezonePlaceholder: 'Select a timezone', + serverRestartTimezoneHint: 'List comes from the server host. DST shifts are handled automatically.', serverRestartLadder: 'Warning ladder', serverRestartLadderSubtitle: 'Each step broadcasts a {minutes}-aware message N minutes before the actual shutdown. The 0-minute step is the final "going down now" line.', serverRestartColMinutes: 'Min before', diff --git a/frontend/src/i18n/locales/zh-TW.ts b/frontend/src/i18n/locales/zh-TW.ts index 06953f0..1201535 100644 --- a/frontend/src/i18n/locales/zh-TW.ts +++ b/frontend/src/i18n/locales/zh-TW.ts @@ -901,8 +901,9 @@ const zhTW: Messages = { serverRestartEnableHint: 'When off, scheduled restarts pause. Manual "Restart Now" still works.', serverRestartTime: 'Time of day (HH:mm)', serverRestartTimeHint: 'Wall-clock time the final shutdown fires. Warnings start earlier per the ladder below.', - serverRestartTimezone: 'Timezone (IANA)', - serverRestartTimezoneHint: 'e.g. America/Los_Angeles, Europe/Berlin, UTC. DST shifts handled automatically.', + serverRestartTimezone: 'Timezone', + serverRestartTimezonePlaceholder: 'Select a timezone', + serverRestartTimezoneHint: 'List comes from the server host. DST shifts are handled automatically.', serverRestartLadder: 'Warning ladder', serverRestartLadderSubtitle: 'Each step broadcasts a {minutes}-aware message N minutes before the actual shutdown. The 0-minute step is the final "going down now" line.', serverRestartColMinutes: 'Min before', diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue index d1bc001..8d7d7bf 100644 --- a/frontend/src/views/SettingsView.vue +++ b/frontend/src/views/SettingsView.vue @@ -13,6 +13,7 @@ import { getVoteSettings, updateVoteSettings } from '@/api/bloodmoonvote' import { getVoteRewardsSettings, updateVoteRewardsSettings, getVoteGrants } from '@/api/voterewards' import { getVipPerksSettings, updateVipPerksSettings } from '@/api/vipperks' import { getRestartSettings, updateRestartSettings, triggerRestartNow } from '@/api/restart' +import { getTimezones, type TimezoneOption } from '@/api/server' import { getTicketSettings, updateTicketSettings } from '@/api/tickets' import { getDiscordSettings, updateDiscordSettings, getDiscordStatus, testDiscordConnection } from '@/api/discord' import { restartServer } from '@/api/serverControl' @@ -516,6 +517,27 @@ const savingRestart = ref(false) const triggeringRestart = ref(false) const restartLeadMinutes = ref(10) +// Timezones the backend host can actually resolve. We fetch from the server +// rather than shipping a static IANA list because on .NET Framework / Windows +// installs the runtime returns Windows registry IDs ("Pacific Standard Time"), +// not IANA. The dropdown shows the friendly DisplayName but saves the raw Id. +const availableTimezones = ref([]) +const loadingTimezones = ref(false) + +async function fetchTimezones() { + loadingTimezones.value = true + try { + availableTimezones.value = await getTimezones() + } catch { + // Non-fatal — the dropdown just stays empty and the existing value still + // shows as the selected label. Don't toast: the restart tab itself toasts + // for the main settings load failure and we don't want to double-noise. + availableTimezones.value = [] + } finally { + loadingTimezones.value = false + } +} + async function fetchRestartSettings() { loadingRestart.value = true try { @@ -802,6 +824,7 @@ onMounted(() => { fetchVoteGrants() fetchVipPerksSettings() fetchRestartSettings() + fetchTimezones() } }) @@ -1711,7 +1734,17 @@ onMounted(() => {
- +