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:
@@ -122,8 +122,10 @@ describe('settings and auth events', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(response.statusCode).toBe(200);
|
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(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 () => {
|
it('rejects proxy token that does not start with sk-', async () => {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
testDatabaseConnection,
|
testDatabaseConnection,
|
||||||
type MigrationDialect,
|
type MigrationDialect,
|
||||||
} from '../../services/databaseMigrationService.js';
|
} 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 { extractClientIp, isIpAllowed } from '../../middleware/auth.js';
|
||||||
import { invalidateSiteProxyCache, normalizeSiteProxyUrl } from '../../services/siteProxy.js';
|
import { invalidateSiteProxyCache, normalizeSiteProxyUrl } from '../../services/siteProxy.js';
|
||||||
import { performFactoryReset } from '../../services/factoryResetService.js';
|
import { performFactoryReset } from '../../services/factoryResetService.js';
|
||||||
@@ -442,6 +442,7 @@ function getRuntimeSettingsResponse(currentAdminIp = '') {
|
|||||||
notifyCooldownSec: config.notifyCooldownSec,
|
notifyCooldownSec: config.notifyCooldownSec,
|
||||||
adminIpAllowlist: config.adminIpAllowlist,
|
adminIpAllowlist: config.adminIpAllowlist,
|
||||||
currentAdminIp,
|
currentAdminIp,
|
||||||
|
serverTimeZone: getResolvedTimeZone(),
|
||||||
systemProxyUrl: config.systemProxyUrl,
|
systemProxyUrl: config.systemProxyUrl,
|
||||||
proxyErrorKeywords: config.proxyErrorKeywords,
|
proxyErrorKeywords: config.proxyErrorKeywords,
|
||||||
proxyEmptyContentFailEnabled: config.proxyEmptyContentFailEnabled,
|
proxyEmptyContentFailEnabled: config.proxyEmptyContentFailEnabled,
|
||||||
|
|||||||
@@ -39,10 +39,12 @@ export default function SiteAnnouncements() {
|
|||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
const [clearing, setClearing] = useState(false);
|
const [clearing, setClearing] = useState(false);
|
||||||
const [markingAll, setMarkingAll] = useState(false);
|
const [markingAll, setMarkingAll] = useState(false);
|
||||||
|
const [serverTimeZone, setServerTimeZone] = useState<string | undefined>(undefined);
|
||||||
const [highlightAnnouncementId, setHighlightAnnouncementId] = useState<number | null>(null);
|
const [highlightAnnouncementId, setHighlightAnnouncementId] = useState<number | null>(null);
|
||||||
const rowRefs = useRef<Map<number, HTMLDivElement>>(new Map());
|
const rowRefs = useRef<Map<number, HTMLDivElement>>(new Map());
|
||||||
const highlightTimerRef = useRef<number | null>(null);
|
const highlightTimerRef = useRef<number | null>(null);
|
||||||
const viewerTimeZone = useMemo(() => readClientTimeZone(), []);
|
const viewerTimeZone = useMemo(() => readClientTimeZone(), []);
|
||||||
|
const displayTimeZone = serverTimeZone || viewerTimeZone;
|
||||||
|
|
||||||
const siteNameById = useMemo(() => {
|
const siteNameById = useMemo(() => {
|
||||||
const map = new Map<number, string>();
|
const map = new Map<number, string>();
|
||||||
@@ -56,12 +58,17 @@ export default function SiteAnnouncements() {
|
|||||||
if (silent) setRefreshing(true);
|
if (silent) setRefreshing(true);
|
||||||
else setLoading(true);
|
else setLoading(true);
|
||||||
try {
|
try {
|
||||||
const [announcementRows, siteRows] = await Promise.all([
|
const [announcementRows, siteRows, runtimeInfo] = await Promise.all([
|
||||||
api.getSiteAnnouncements(),
|
api.getSiteAnnouncements(),
|
||||||
api.getSites(),
|
api.getSites(),
|
||||||
|
api.getRuntimeSettings().catch(() => null),
|
||||||
]);
|
]);
|
||||||
setRows(Array.isArray(announcementRows) ? announcementRows : []);
|
setRows(Array.isArray(announcementRows) ? announcementRows : []);
|
||||||
setSites(Array.isArray(siteRows) ? siteRows : []);
|
setSites(Array.isArray(siteRows) ? siteRows : []);
|
||||||
|
const nextServerTimeZone = typeof runtimeInfo?.serverTimeZone === 'string'
|
||||||
|
? runtimeInfo.serverTimeZone.trim()
|
||||||
|
: '';
|
||||||
|
setServerTimeZone(nextServerTimeZone || undefined);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.error(error?.message || '加载站点公告失败');
|
toast.error(error?.message || '加载站点公告失败');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -219,9 +226,9 @@ export default function SiteAnnouncements() {
|
|||||||
<SiteAnnouncementContent content={row.content} />
|
<SiteAnnouncementContent content={row.content} />
|
||||||
<div
|
<div
|
||||||
style={{ marginTop: 8, fontSize: 12, color: 'var(--color-text-muted)' }}
|
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>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const { apiMock } = vi.hoisted(() => ({
|
|||||||
apiMock: {
|
apiMock: {
|
||||||
getSiteAnnouncements: vi.fn(),
|
getSiteAnnouncements: vi.fn(),
|
||||||
getSites: vi.fn(),
|
getSites: vi.fn(),
|
||||||
|
getRuntimeSettings: vi.fn(),
|
||||||
markSiteAnnouncementRead: vi.fn(),
|
markSiteAnnouncementRead: vi.fn(),
|
||||||
markAllSiteAnnouncementsRead: vi.fn(),
|
markAllSiteAnnouncementsRead: vi.fn(),
|
||||||
clearSiteAnnouncements: vi.fn(),
|
clearSiteAnnouncements: vi.fn(),
|
||||||
@@ -41,6 +42,9 @@ describe('SiteAnnouncements page', () => {
|
|||||||
apiMock.getSites.mockResolvedValue([
|
apiMock.getSites.mockResolvedValue([
|
||||||
{ id: 9, name: 'Sub Site', platform: 'sub2api' },
|
{ id: 9, name: 'Sub Site', platform: 'sub2api' },
|
||||||
]);
|
]);
|
||||||
|
apiMock.getRuntimeSettings.mockResolvedValue({
|
||||||
|
serverTimeZone: 'UTC',
|
||||||
|
});
|
||||||
apiMock.getSiteAnnouncements.mockResolvedValue([
|
apiMock.getSiteAnnouncements.mockResolvedValue([
|
||||||
{
|
{
|
||||||
id: 12,
|
id: 12,
|
||||||
@@ -50,8 +54,8 @@ describe('SiteAnnouncements page', () => {
|
|||||||
title: 'Maintenance',
|
title: 'Maintenance',
|
||||||
content: 'Window starts at 10:00',
|
content: 'Window starts at 10:00',
|
||||||
level: 'info',
|
level: 'info',
|
||||||
firstSeenAt: '2026-03-20 10:00:00',
|
firstSeenAt: '2026-03-20 04:23:27',
|
||||||
lastSeenAt: '2026-03-20 10:00:00',
|
lastSeenAt: '2026-03-20 04:23:27',
|
||||||
readAt: null,
|
readAt: null,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -100,8 +104,10 @@ describe('SiteAnnouncements page', () => {
|
|||||||
|
|
||||||
expect(apiMock.getSiteAnnouncements).toHaveBeenCalled();
|
expect(apiMock.getSiteAnnouncements).toHaveBeenCalled();
|
||||||
expect(apiMock.getSites).toHaveBeenCalled();
|
expect(apiMock.getSites).toHaveBeenCalled();
|
||||||
|
expect(apiMock.getRuntimeSettings).toHaveBeenCalled();
|
||||||
expect(JSON.stringify(root!.toJSON())).toContain('站点公告');
|
expect(JSON.stringify(root!.toJSON())).toContain('站点公告');
|
||||||
expect(JSON.stringify(root!.toJSON())).toContain('Maintenance');
|
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 highlightedRows = root!.root.findAll((node) => {
|
||||||
const className = typeof node.props.className === 'string' ? node.props.className : '';
|
const className = typeof node.props.className === 'string' ? node.props.className : '';
|
||||||
|
|||||||
Reference in New Issue
Block a user