1
0
forked from boos/BoosAPI

Merge pull request #190 from cita-777/codex/site-announcement-timezone-fix

[codex] fix site announcement timezone
This commit is contained in:
Cita
2026-03-20 13:33:12 +08:00
committed by GitHub
4 changed files with 23 additions and 7 deletions
@@ -122,8 +122,10 @@ describe('settings and auth events', () => {
});
expect(response.statusCode).toBe(200);
const body = response.json() as { currentAdminIp?: string };
const body = response.json() as { currentAdminIp?: string; serverTimeZone?: string };
expect(body.currentAdminIp).toBe('203.0.113.5');
expect(typeof body.serverTimeZone).toBe('string');
expect((body.serverTimeZone || '').length).toBeGreaterThan(0);
});
it('rejects proxy token that does not start with sk-', async () => {
+2 -1
View File
@@ -15,7 +15,7 @@ import {
testDatabaseConnection,
type MigrationDialect,
} from '../../services/databaseMigrationService.js';
import { formatUtcSqlDateTime } from '../../services/localTimeService.js';
import { formatUtcSqlDateTime, getResolvedTimeZone } from '../../services/localTimeService.js';
import { extractClientIp, isIpAllowed } from '../../middleware/auth.js';
import { invalidateSiteProxyCache, normalizeSiteProxyUrl } from '../../services/siteProxy.js';
import { performFactoryReset } from '../../services/factoryResetService.js';
@@ -442,6 +442,7 @@ function getRuntimeSettingsResponse(currentAdminIp = '') {
notifyCooldownSec: config.notifyCooldownSec,
adminIpAllowlist: config.adminIpAllowlist,
currentAdminIp,
serverTimeZone: getResolvedTimeZone(),
systemProxyUrl: config.systemProxyUrl,
proxyErrorKeywords: config.proxyErrorKeywords,
proxyEmptyContentFailEnabled: config.proxyEmptyContentFailEnabled,
+10 -3
View File
@@ -39,10 +39,12 @@ export default function SiteAnnouncements() {
const [refreshing, setRefreshing] = useState(false);
const [clearing, setClearing] = useState(false);
const [markingAll, setMarkingAll] = useState(false);
const [serverTimeZone, setServerTimeZone] = useState<string | undefined>(undefined);
const [highlightAnnouncementId, setHighlightAnnouncementId] = useState<number | null>(null);
const rowRefs = useRef<Map<number, HTMLDivElement>>(new Map());
const highlightTimerRef = useRef<number | null>(null);
const viewerTimeZone = useMemo(() => readClientTimeZone(), []);
const displayTimeZone = serverTimeZone || viewerTimeZone;
const siteNameById = useMemo(() => {
const map = new Map<number, string>();
@@ -56,12 +58,17 @@ export default function SiteAnnouncements() {
if (silent) setRefreshing(true);
else setLoading(true);
try {
const [announcementRows, siteRows] = await Promise.all([
const [announcementRows, siteRows, runtimeInfo] = await Promise.all([
api.getSiteAnnouncements(),
api.getSites(),
api.getRuntimeSettings().catch(() => null),
]);
setRows(Array.isArray(announcementRows) ? announcementRows : []);
setSites(Array.isArray(siteRows) ? siteRows : []);
const nextServerTimeZone = typeof runtimeInfo?.serverTimeZone === 'string'
? runtimeInfo.serverTimeZone.trim()
: '';
setServerTimeZone(nextServerTimeZone || undefined);
} catch (error: any) {
toast.error(error?.message || '加载站点公告失败');
} finally {
@@ -219,9 +226,9 @@ export default function SiteAnnouncements() {
<SiteAnnouncementContent content={row.content} />
<div
style={{ marginTop: 8, fontSize: 12, color: 'var(--color-text-muted)' }}
title={viewerTimeZone ? `本地时区:${viewerTimeZone}` : undefined}
title={displayTimeZone ? `本地时区:${displayTimeZone}` : undefined}
>
首次发现:{formatSiteAnnouncementSeenAt(row.firstSeenAt || row.lastSeenAt || '', viewerTimeZone)}
首次发现:{formatSiteAnnouncementSeenAt(row.firstSeenAt || row.lastSeenAt || '', displayTimeZone)}
</div>
</div>
))
+8 -2
View File
@@ -9,6 +9,7 @@ const { apiMock } = vi.hoisted(() => ({
apiMock: {
getSiteAnnouncements: vi.fn(),
getSites: vi.fn(),
getRuntimeSettings: vi.fn(),
markSiteAnnouncementRead: vi.fn(),
markAllSiteAnnouncementsRead: vi.fn(),
clearSiteAnnouncements: vi.fn(),
@@ -41,6 +42,9 @@ describe('SiteAnnouncements page', () => {
apiMock.getSites.mockResolvedValue([
{ id: 9, name: 'Sub Site', platform: 'sub2api' },
]);
apiMock.getRuntimeSettings.mockResolvedValue({
serverTimeZone: 'UTC',
});
apiMock.getSiteAnnouncements.mockResolvedValue([
{
id: 12,
@@ -50,8 +54,8 @@ describe('SiteAnnouncements page', () => {
title: 'Maintenance',
content: 'Window starts at 10:00',
level: 'info',
firstSeenAt: '2026-03-20 10:00:00',
lastSeenAt: '2026-03-20 10:00:00',
firstSeenAt: '2026-03-20 04:23:27',
lastSeenAt: '2026-03-20 04:23:27',
readAt: null,
},
]);
@@ -100,8 +104,10 @@ describe('SiteAnnouncements page', () => {
expect(apiMock.getSiteAnnouncements).toHaveBeenCalled();
expect(apiMock.getSites).toHaveBeenCalled();
expect(apiMock.getRuntimeSettings).toHaveBeenCalled();
expect(JSON.stringify(root!.toJSON())).toContain('站点公告');
expect(JSON.stringify(root!.toJSON())).toContain('Maintenance');
expect(JSON.stringify(root!.toJSON())).toContain('2026/03/20 04:23:27');
const highlightedRows = root!.root.findAll((node) => {
const className = typeof node.props.className === 'string' ? node.props.className : '';