feat: improve operations diagnostics and channel management

This commit is contained in:
hectorzhao
2026-08-09 14:27:19 +08:00
parent 44352aeb2f
commit 4724b9db6a
65 changed files with 1211 additions and 293 deletions
+11 -4
View File
@@ -34,7 +34,7 @@ import {
UserX,
} from 'lucide-react';
import { adminApi } from '@/api/adminApi';
import type { LoginSession } from '@/api/session';
import { getLastUserActivityAt, readSession, type LoginSession } from '@/api/session';
import { AppShell } from '@/layouts/AppShell';
import { PortalSessionBoundary } from '@/layouts/PortalSessionBoundary';
@@ -46,7 +46,13 @@ export function AdminLayout() {
function AdminAuthenticatedLayout({ session }: { session: LoginSession }) {
const [pendingAudits, setPendingAudits] = useState(EMPTY_PENDING_AUDITS);
const [sessionLocked, setSessionLocked] = useState(Boolean(session.locked));
const loadPendingAuditCount = useCallback(() => {
const currentSession = readSession('admin');
if (!currentSession || currentSession.locked
|| Date.now() - getLastUserActivityAt() >= currentSession.idleTimeoutSeconds * 1000) {
return;
}
// This runs globally and on a timer, so it must not fan out through the full dashboard aggregation.
adminApi.getPendingAudits()
.then((counts) => {
@@ -58,7 +64,7 @@ function AdminAuthenticatedLayout({ session }: { session: LoginSession }) {
}, []);
useEffect(() => {
if (session.portal !== 'admin' || session.locked) {
if (session.portal !== 'admin' || sessionLocked) {
return;
}
loadPendingAuditCount();
@@ -72,7 +78,7 @@ function AdminAuthenticatedLayout({ session }: { session: LoginSession }) {
window.removeEventListener('focus', onFocus);
window.removeEventListener('cmpp-audit-count-refresh', onAuditRefresh);
};
}, [loadPendingAuditCount, session.locked, session.portal]);
}, [loadPendingAuditCount, session.portal, sessionLocked]);
return (
<AppShell
@@ -83,6 +89,7 @@ function AdminAuthenticatedLayout({ session }: { session: LoginSession }) {
portal="admin"
userName={session.user.displayName}
userRole="平台管理员"
onSessionLockedChange={setSessionLocked}
auditNotifications={[
{ label: '企业认证待审', count: pendingAudits.enterpriseCertifications, to: '/admin/enterprise-audit' },
{ label: '短信审核待审', count: pendingAudits.smsAudits, to: '/admin/sms-audit' },
@@ -120,7 +127,6 @@ function AdminAuthenticatedLayout({ session }: { session: LoginSession }) {
{ label: '短信模板审核', to: '/admin/templates', icon: FileCheck2 },
{ label: '短信签名审核', to: '/admin/signatures', icon: FilePenLine },
{ label: '引流信息审核', to: '/admin/drainage-audits', icon: FilePenLine },
{ label: '风控规则', to: '/admin/risk-rules', icon: Shield },
],
},
{
@@ -174,6 +180,7 @@ function AdminAuthenticatedLayout({ session }: { session: LoginSession }) {
title: '安全控制',
icon: Shield,
items: [
{ label: '风控规则', to: '/admin/risk-rules', icon: Shield },
{ label: '企业黑名单', to: '/admin/enterprise-blacklist', icon: UserX },
{ label: '全局黑名单', to: '/admin/global-blacklist', icon: ShieldOff },
{ label: '敏感词管理', to: '/admin/sensitive-words', icon: Shield },
+22 -3
View File
@@ -59,6 +59,7 @@ type AppShellProps = {
userRole: string;
navSections: ShellNavSection[];
auditNotifications?: AuditNotificationItem[];
onSessionLockedChange?: (locked: boolean) => void;
};
export function AppShell({
@@ -70,6 +71,7 @@ export function AppShell({
userRole,
navSections,
auditNotifications = [],
onSessionLockedChange,
}: AppShellProps) {
const [collapsed, setCollapsed] = useState(false);
const [mobileNavOpen, setMobileNavOpen] = useState(false);
@@ -199,6 +201,10 @@ export function AppShell({
setMobileNavOpen(false);
}, [location.pathname]);
useEffect(() => {
onSessionLockedChange?.(locked);
}, [locked, onSessionLockedChange]);
useEffect(() => {
if (!mobileNavOpen) return;
const closeOnEscape = (event: KeyboardEvent) => {
@@ -213,8 +219,21 @@ export function AppShell({
const onActivity = () => markUserActivity();
activityEvents.forEach((eventName) => window.addEventListener(eventName, onActivity, { passive: true }));
const onLocked = () => setLocked(true);
const onUnlocked = () => { setLocked(false); markUserActivity(); };
const onLocked = () => {
updateSessionTiming(portal, { locked: true });
setLocked(true);
// Business routes must be unmounted while the server session is locked.
// This prevents background list and badge requests from repeatedly
// receiving 401 responses and guarantees a fresh read after unlocking.
setRoutesSuspended(true);
};
const onUnlocked = () => {
updateSessionTiming(portal, { locked: false });
setLocked(false);
setRoutesSuspended(false);
lockRequested.current = false;
markUserActivity();
};
const onLogout = () => { clearSession(portal); navigate(loginPath, { replace: true }); };
const lockedEvent = sessionEvent(portal, 'locked');
const unlockedEvent = sessionEvent(portal, 'unlocked');
@@ -261,8 +280,8 @@ export function AppShell({
const remaining = session.idleTimeoutSeconds * 1000 - (now - getLastUserActivityAt());
if (remaining <= 0 && !lockRequested.current) {
lockRequested.current = true;
setLocked(true);
setIdleWarningSeconds(null);
dispatchSessionEvent(portal, 'locked', { reason: 'client_idle_timer' });
void portalSessionApi.lock(portal).catch(() => undefined);
} else if (remaining <= 5 * 60 * 1000) {
setIdleWarningSeconds(Math.ceil(remaining / 1000));