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
+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));