diff --git a/astrbot/dashboard/services/config_service.py b/astrbot/dashboard/services/config_service.py
index 3caa8acd9..803b6b92e 100644
--- a/astrbot/dashboard/services/config_service.py
+++ b/astrbot/dashboard/services/config_service.py
@@ -5,8 +5,10 @@ import copy
import inspect
import os
import traceback
+from datetime import datetime, timezone
from pathlib import Path
from typing import Any
+from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from astrbot.core import file_token_service, logger
from astrbot.core.config.astrbot_config import AstrBotConfig
@@ -474,7 +476,28 @@ class ConfigProfileService:
}
def get_system_config(self) -> dict:
- return self.get_system_schema()
+ """Return the system configuration with the server's effective time.
+
+ Returns:
+ System configuration metadata, an aware UTC timestamp, and the
+ effective UTC offset in minutes.
+ """
+ data = self.get_system_schema()
+ server_utc_time = datetime.now(timezone.utc)
+ timezone_name = str(data["config"].get("timezone") or "").strip()
+ if timezone_name:
+ try:
+ configured_time = server_utc_time.astimezone(ZoneInfo(timezone_name))
+ except (ValueError, ZoneInfoNotFoundError):
+ configured_time = server_utc_time.astimezone()
+ else:
+ configured_time = server_utc_time.astimezone()
+ utc_offset = configured_time.utcoffset()
+ data["server_utc_time"] = server_utc_time.isoformat()
+ data["server_utc_offset_minutes"] = (
+ int(utc_offset.total_seconds() / 60) if utc_offset else 0
+ )
+ return data
def list_profiles(self) -> dict:
return {"info_list": self.acm.get_conf_list()}
diff --git a/dashboard/src/i18n/locales/en-US/features/settings.json b/dashboard/src/i18n/locales/en-US/features/settings.json
index b45649b91..b3d9f7df0 100644
--- a/dashboard/src/i18n/locales/en-US/features/settings.json
+++ b/dashboard/src/i18n/locales/en-US/features/settings.json
@@ -67,6 +67,9 @@
"subtitle": "Rendering strategy, endpoint, and custom templates for text-to-image."
}
},
+ "timePreview": {
+ "label": "AstrBot current time"
+ },
"messages": {
"loadFailed": "Failed to load system config",
"saveSuccess": "System config saved",
diff --git a/dashboard/src/i18n/locales/ru-RU/features/settings.json b/dashboard/src/i18n/locales/ru-RU/features/settings.json
index 9b1ceb946..b034f46c7 100644
--- a/dashboard/src/i18n/locales/ru-RU/features/settings.json
+++ b/dashboard/src/i18n/locales/ru-RU/features/settings.json
@@ -67,6 +67,9 @@
"subtitle": "Стратегия, endpoint и пользовательские шаблоны для рендеринга."
}
},
+ "timePreview": {
+ "label": "Текущее время AstrBot"
+ },
"messages": {
"loadFailed": "Не удалось загрузить системную конфигурацию",
"saveSuccess": "Системная конфигурация сохранена",
diff --git a/dashboard/src/i18n/locales/zh-CN/features/settings.json b/dashboard/src/i18n/locales/zh-CN/features/settings.json
index bf6e94a9b..d657039fd 100644
--- a/dashboard/src/i18n/locales/zh-CN/features/settings.json
+++ b/dashboard/src/i18n/locales/zh-CN/features/settings.json
@@ -67,6 +67,9 @@
"subtitle": "文本转图像渲染策略、端点和自定义模板。"
}
},
+ "timePreview": {
+ "label": "AstrBot 当前时间"
+ },
"messages": {
"loadFailed": "加载系统配置失败",
"saveSuccess": "系统配置已保存",
diff --git a/dashboard/src/views/Settings.vue b/dashboard/src/views/Settings.vue
index 7477f37e7..1ed19d96b 100644
--- a/dashboard/src/views/Settings.vue
+++ b/dashboard/src/views/Settings.vue
@@ -64,7 +64,20 @@
:class="{ 'system-config-group--with-cleanup': group.key === 'tempStorage' }"
@focusout.capture="scheduleSystemConfigAutoSave"
>
-
{{ group.title }}
+
+
{{ group.title }}
+
+
+ {{ tm('systemConfig.timePreview.label') }} ·
+ {{ timezoneTimePreview.offsetLabel }} {{ timezoneTimePreview.localTime }}
+
+
+
(
JSON.stringify(systemConfigData.value || {}) !== systemConfigLastSavedSnapshot.value
));
+const timezoneTimePreview = computed(() => {
+ if (
+ serverUtcEpochMs.value === null ||
+ serverUtcOffsetMinutes.value === null ||
+ serverClockAnchorMs.value === null ||
+ serverClockTickMs.value === null
+ ) {
+ return null;
+ }
+
+ const localDate = new Date(
+ serverUtcEpochMs.value +
+ Math.max(0, serverClockTickMs.value - serverClockAnchorMs.value) +
+ serverUtcOffsetMinutes.value * 60_000
+ );
+ const localIsoTime = localDate.toISOString();
+ const absoluteOffset = Math.abs(serverUtcOffsetMinutes.value);
+ const offsetHours = Math.floor(absoluteOffset / 60);
+ const offsetMinutes = absoluteOffset % 60;
+ const offsetLabel = serverUtcOffsetMinutes.value === 0
+ ? 'UTC'
+ : `UTC${serverUtcOffsetMinutes.value > 0 ? '+' : '-'}${offsetHours}${offsetMinutes ? `:${String(offsetMinutes).padStart(2, '0')}` : ''}`;
+ return {
+ localTime: `${localIsoTime.slice(0, 4)}/${localIsoTime.slice(5, 7)}/${localIsoTime.slice(8, 10)} ${localIsoTime.slice(11, 16)}`,
+ offsetLabel
+ };
+});
+
const systemConfigGroups = computed(() => {
const systemSection = systemConfigMetadata.value?.system_group?.metadata?.system || {};
const systemItems = systemSection.items || {};
@@ -820,6 +866,12 @@ const loadSystemConfig = async () => {
}
systemConfigData.value = res.data.data?.config || {};
systemConfigMetadata.value = res.data.data?.metadata || {};
+ const parsedServerTime = Date.parse(res.data.data?.server_utc_time || '');
+ const parsedUtcOffset = Number(res.data.data?.server_utc_offset_minutes);
+ serverUtcEpochMs.value = Number.isNaN(parsedServerTime) ? null : parsedServerTime;
+ serverUtcOffsetMinutes.value = Number.isFinite(parsedUtcOffset) ? parsedUtcOffset : null;
+ serverClockAnchorMs.value = serverUtcEpochMs.value === null ? null : performance.now();
+ serverClockTickMs.value = serverClockAnchorMs.value;
systemConfigLastSavedSnapshot.value = JSON.stringify(systemConfigData.value || {});
systemConfigRestartRequired.value = false;
} catch (error) {
@@ -864,6 +916,23 @@ const saveSystemConfig = async (configOverride = null, headers = {}, allow2faPro
configSave2faError.value = '';
systemConfigData.value = configPayload;
systemConfigLastSavedSnapshot.value = JSON.stringify(configPayload);
+ serverUtcEpochMs.value = null;
+ serverUtcOffsetMinutes.value = null;
+ serverClockAnchorMs.value = null;
+ serverClockTickMs.value = null;
+ try {
+ const timeRes = await systemConfigApi.get();
+ if (timeRes.data.status === 'ok') {
+ const parsedServerTime = Date.parse(timeRes.data.data?.server_utc_time || '');
+ const parsedUtcOffset = Number(timeRes.data.data?.server_utc_offset_minutes);
+ serverUtcEpochMs.value = Number.isNaN(parsedServerTime) ? null : parsedServerTime;
+ serverUtcOffsetMinutes.value = Number.isFinite(parsedUtcOffset) ? parsedUtcOffset : null;
+ serverClockAnchorMs.value = serverUtcEpochMs.value === null ? null : performance.now();
+ serverClockTickMs.value = serverClockAnchorMs.value;
+ }
+ } catch {
+ // Keep the preview hidden until the backend time can be confirmed.
+ }
systemConfigRestartRequired.value = true;
showToast(res.data.message || tm('systemConfig.messages.saveSuccess'), 'success');
return { success: true };
@@ -1069,6 +1138,9 @@ const resetThemeColors = () => {
onMounted(async () => {
await Promise.all([loadApiKeys(), loadSystemConfig()]);
+ serverClockTimer.value = window.setInterval(() => {
+ serverClockTickMs.value = performance.now();
+ }, 1000);
const hash = window.location.hash;
if (hash.includes('settings-appearance')) {
activeSettingsSection.value = 'appearance';
@@ -1086,6 +1158,10 @@ onMounted(async () => {
});
onUnmounted(() => {
+ if (serverClockTimer.value) {
+ clearInterval(serverClockTimer.value);
+ serverClockTimer.value = null;
+ }
if (systemConfigAutoSaveTimer.value) {
clearTimeout(systemConfigAutoSaveTimer.value);
systemConfigAutoSaveTimer.value = null;
@@ -1331,8 +1407,15 @@ onUnmounted(() => {
margin-bottom: 0;
}
-.system-config-group__title {
+.system-config-group__heading {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
margin-bottom: 12px;
+}
+
+.system-config-group__title {
color: rgb(var(--v-theme-on-surface));
font-size: 1.04rem;
font-weight: 760;
@@ -1340,6 +1423,26 @@ onUnmounted(() => {
line-height: 1.32;
}
+.timezone-time-preview {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ gap: 5px;
+ font-variant-numeric: tabular-nums;
+}
+
+.timezone-time-preview__item {
+ padding: 3px 7px;
+ border: 1px solid rgba(var(--v-theme-primary), 0.16);
+ border-radius: 6px;
+ color: rgb(var(--v-theme-primary));
+ background: rgba(var(--v-theme-primary), 0.055);
+ font-size: 0.72rem;
+ font-weight: 600;
+ line-height: 1.3;
+ white-space: nowrap;
+}
+
.system-config-group :deep(.v-card) {
margin-bottom: 0 !important;
padding-bottom: 0 !important;
@@ -1736,6 +1839,15 @@ onUnmounted(() => {
max-width: none;
}
+ .system-config-group__heading {
+ align-items: flex-start;
+ flex-direction: column;
+ }
+
+ .timezone-time-preview {
+ justify-content: flex-start;
+ }
+
.system-config-group :deep(.config-row) {
padding: 14px 16px;
}
diff --git a/tests/unit/test_config_profile_service.py b/tests/unit/test_config_profile_service.py
new file mode 100644
index 000000000..7b83b8083
--- /dev/null
+++ b/tests/unit/test_config_profile_service.py
@@ -0,0 +1,25 @@
+from datetime import datetime, timezone
+from types import SimpleNamespace
+from unittest.mock import patch
+
+from astrbot.dashboard.services.config_service import ConfigProfileService
+
+
+def test_get_system_config_includes_effective_server_time() -> None:
+ """Verify that the response includes server UTC time and configured offset."""
+ fixed_time = datetime(2026, 8, 7, 2, 31, tzinfo=timezone.utc)
+ service = ConfigProfileService(
+ SimpleNamespace(
+ astrbot_config_mgr=SimpleNamespace(
+ confs={"default": {"timezone": "Asia/Shanghai"}}
+ )
+ )
+ )
+
+ with patch("astrbot.dashboard.services.config_service.datetime") as mock_datetime:
+ mock_datetime.now.return_value = fixed_time
+ result = service.get_system_config()
+
+ assert result["server_utc_time"] == "2026-08-07T02:31:00+00:00"
+ assert result["server_utc_offset_minutes"] == 480
+ assert result["config"]["timezone"] == "Asia/Shanghai"