krona-front/src/features/control-center/components/ControlCenterShell.tsx

640 lines
31 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useState, useMemo } from 'react';
import { NavLink, Outlet, useLocation, useNavigate } from 'react-router-dom';
import {
LayoutDashboard,
Building2,
CreditCard,
Package,
Smartphone,
Server,
BellRing,
AlertTriangle,
FileCheck2,
RotateCcw,
CheckCircle2,
Radio,
ExternalLink,
Menu,
X,
ShieldAlert,
Sun,
Moon,
Sparkles,
ShieldCheck,
ChevronRight,
SlidersHorizontal,
ChevronLeft,
} from 'lucide-react';
import { useKccStore, useOperatorRole } from '../store/kccStore';
import { OperatorSessionSwitcher } from './OperatorSessionSwitcher';
import { InspectorDrawer } from './InspectorDrawer';
import { ConfirmOperatorActionModal } from './ConfirmOperatorAction';
import { ServerMonitorOutageBanner } from './ServerMonitorOutageBanner';
import { useThemeStore, ThemeMode } from '../../../stores/theme';
import { RU_ROLES, RU_THEMES, RU_UI } from '../locales/ru';
export const ControlCenterShell: React.FC = () => {
const location = useLocation();
const navigate = useNavigate();
const isInitialized = useKccStore((state) => state.isInitialized);
const initialize = useKccStore((state) => state.initialize);
const resetToDefaults = useKccStore((state) => state.resetToDefaults);
const serverMonitorOutage = useKccStore((state) => state.serverMonitorOutage);
const toggleServerMonitorOutage = useKccStore((state) => state.toggleServerMonitorOutage);
const incidents = useKccStore((state) => state.incidents);
const { role, operator } = useOperatorRole();
const themeMode = useThemeStore((state) => state.themeMode);
const setTheme = useThemeStore((state) => state.setTheme);
const [utcTime, setUtcTime] = useState('');
const [isResetting, setIsResetting] = useState(false);
const [showSavedFeedback, setShowSavedFeedback] = useState(false);
// Mobile / Tablet Drawer states
const [isMobileDrawerOpen, setIsMobileDrawerOpen] = useState(false);
const [isTabletSidebarCollapsed, setIsTabletSidebarCollapsed] = useState(false);
useEffect(() => {
if (!isInitialized) {
initialize();
}
}, [isInitialized, initialize]);
useEffect(() => {
const updateTime = () => {
const now = new Date();
setUtcTime(now.toISOString().replace('T', ' ').slice(0, 19) + ' UTC');
};
updateTime();
const interval = setInterval(updateTime, 1000);
return () => clearInterval(interval);
}, []);
// Close mobile drawer on route change
useEffect(() => {
setIsMobileDrawerOpen(false);
}, [location.pathname]);
const handleReset = async () => {
if (confirm('Сбросить демонстрационные данные платформы к исходному состоянию?')) {
setIsResetting(true);
await resetToDefaults();
setIsResetting(false);
setShowSavedFeedback(true);
setTimeout(() => setShowSavedFeedback(false), 3000);
}
};
// Critical events calculation
const criticalCount = useMemo(() => {
let count = 0;
if (serverMonitorOutage) count += 1;
count += incidents.filter(
(i) => i.status !== 'resolved' && (i.severity === 'P0_CRITICAL' || i.severity === 'P1_MAJOR')
).length;
return count;
}, [serverMonitorOutage, incidents]);
// Navigation Items
const primaryMobileNav = [
{ to: '/control', label: 'Обзор', icon: LayoutDashboard, end: true },
{ to: '/control/organizations', label: 'Организации', icon: Building2 },
{ to: '/control/incidents', label: 'Инциденты', icon: AlertTriangle, badge: criticalCount > 0 ? criticalCount : undefined },
{ to: '/control/infrastructure', label: 'Серверы', icon: Server },
];
const allNavItems = [
{ to: '/control', label: 'Обзор', icon: LayoutDashboard, end: true },
{ to: '/control/organizations', label: 'Организации', icon: Building2 },
{ to: '/control/billing', label: 'Подписки и платежи', icon: CreditCard },
{ to: '/control/modules', label: 'Каталог модулей', icon: Package },
{ to: '/control/installations', label: 'Установки', icon: Smartphone },
{ to: '/control/infrastructure', label: 'Серверы', icon: Server },
{ to: '/control/announcements', label: 'Оповещения', icon: BellRing },
{ to: '/control/incidents', label: 'Инциденты', icon: AlertTriangle, badge: criticalCount > 0 ? criticalCount : undefined },
{ to: '/control/audit', label: 'Журнал аудита', icon: FileCheck2 },
];
const moreNavItems = [
{ to: '/control/billing', label: 'Подписки и платежи', icon: CreditCard },
{ to: '/control/modules', label: 'Каталог модулей', icon: Package },
{ to: '/control/installations', label: 'Установки', icon: Smartphone },
{ to: '/control/announcements', label: 'Оповещения', icon: BellRing },
{ to: '/control/audit', label: 'Журнал аудита', icon: FileCheck2 },
];
// Current section title
const currentTitle = useMemo(() => {
const current = allNavItems.find((item) =>
item.end ? location.pathname === item.to : location.pathname.startsWith(item.to)
);
return current?.label || 'Control Center';
}, [location.pathname, allNavItems]);
const currentRoleLabel = RU_ROLES[role]?.shortLabel || role;
return (
<div
className="min-h-screen max-w-full overflow-x-hidden bg-krona-app text-krona-primary flex flex-col font-sans selection:bg-krona-gold selection:text-zinc-950 transition-colors duration-200"
data-testid="control-center-shell"
>
{/* ========================================================================= */}
{/* 1. DESKTOP & TABLET HEADER (Hidden on mobile < md) */}
{/* ========================================================================= */}
<header className="hidden md:flex h-14 border-b border-krona-border bg-krona-workspace/95 backdrop-blur-md px-4 items-center justify-between z-30 shrink-0 max-w-full">
<div className="flex items-center gap-3 lg:gap-4 min-w-0">
{/* Logo & Platform info */}
<div className="flex items-center gap-2.5 min-w-0">
<div className="w-8 h-8 rounded-lg bg-krona-surface border border-krona-gold/40 flex items-center justify-center text-krona-gold font-extrabold shadow-sm shrink-0">
K
</div>
<div className="min-w-0">
<div className="text-xs font-black tracking-wider text-krona-primary uppercase flex items-center gap-1.5">
<span className="shrink-0">Krona Control Center</span>
<span className="text-[9px] px-1.5 py-0.5 rounded-sm bg-status-success-soft text-status-success-text font-mono">
PROD v1.0
</span>
</div>
<div className="text-[10px] text-krona-tertiary font-mono tracking-wider">
Кабинет владельца платформы
</div>
</div>
</div>
<div className="hidden xl:flex items-center gap-2 text-[11px] text-krona-tertiary font-mono pl-3 border-l border-krona-border shrink-0">
<span>{utcTime}</span>
<span>•</span>
<span className="flex items-center gap-1 text-status-success-text">
<span className="w-1.5 h-1.5 rounded-full bg-status-success-text animate-pulse shrink-0" />
<span>{RU_UI.system.synced}</span>
</span>
</div>
</div>
{/* Right Controls: Outage simulation, Theme Switcher, Operator Switcher, Reset */}
<div className="flex items-center gap-2 lg:gap-3 shrink-0">
{/* Circuit Breaker Outage toggle */}
<button
type="button"
onClick={toggleServerMonitorOutage}
className={`min-h-[38px] px-2.5 py-1.5 rounded-lg border text-xs font-medium flex items-center gap-1.5 transition-colors ${
serverMonitorOutage
? 'border-status-danger-border bg-status-danger-soft text-status-danger-text animate-pulse font-semibold'
: 'border-krona-border bg-krona-surface hover:bg-krona-hover text-krona-secondary'
}`}
title="Имитация сбоя канала телеметрии Server Monitor (Circuit Breaker)"
data-testid="simulate-outage-toggle"
>
<Radio className="w-3.5 h-3.5 shrink-0" />
<span className="hidden lg:inline">
{serverMonitorOutage ? 'Сбой мониторинга (Активен)' : 'Имитировать сбой'}
</span>
</button>
{/* Theme Switcher in Desktop Header */}
<div className="flex items-center bg-krona-surface border border-krona-border rounded-lg p-0.5" role="radiogroup" aria-label="Выбор темы">
<button
type="button"
onClick={() => setTheme('soft-light')}
className={`p-1.5 rounded-md text-xs transition-colors ${
themeMode === 'soft-light' ? 'bg-krona-workspace text-krona-gold font-semibold shadow-xs' : 'text-krona-secondary hover:text-krona-primary'
}`}
title="Мягкая светлая тема"
data-testid="theme-toggle-soft-light"
>
<Sparkles className="w-3.5 h-3.5" />
</button>
<button
type="button"
onClick={() => setTheme('light')}
className={`p-1.5 rounded-md text-xs transition-colors ${
themeMode === 'light' ? 'bg-krona-workspace text-krona-gold font-semibold shadow-xs' : 'text-krona-secondary hover:text-krona-primary'
}`}
title="Классическая светлая тема"
data-testid="theme-toggle-light"
>
<Sun className="w-3.5 h-3.5" />
</button>
<button
type="button"
onClick={() => setTheme('dark')}
className={`p-1.5 rounded-md text-xs transition-colors ${
themeMode === 'dark' ? 'bg-krona-workspace text-krona-gold font-semibold shadow-xs' : 'text-krona-secondary hover:text-krona-primary'
}`}
title="Тёмная тема"
data-testid="theme-toggle-dark"
>
<Moon className="w-3.5 h-3.5" />
</button>
</div>
{/* Reset Demo Data */}
<button
type="button"
onClick={handleReset}
disabled={isResetting}
className="min-h-[38px] px-2.5 py-1.5 rounded-lg border border-krona-border bg-krona-surface hover:bg-krona-hover text-krona-secondary transition-colors text-xs flex items-center gap-1.5"
title="Сброс демонстрационных данных к начальным"
data-testid="reset-demo-button"
>
<RotateCcw className={`w-3.5 h-3.5 ${isResetting ? 'animate-spin text-krona-gold' : ''}`} />
<span className="hidden xl:inline">Сбросить данные</span>
</button>
{/* Operator Role Switcher */}
<OperatorSessionSwitcher />
</div>
</header>
{/* ========================================================================= */}
{/* 2. MOBILE HEADER (< md) */}
{/* ========================================================================= */}
<header
className="md:hidden h-14 border-b border-krona-border bg-krona-workspace/95 backdrop-blur-md px-3 flex items-center justify-between z-30 shrink-0 sticky top-0"
data-testid="mobile-header"
>
{/* Left: Menu toggle + Logo + Title */}
<div className="flex items-center gap-2 min-w-0">
<button
type="button"
onClick={() => setIsMobileDrawerOpen(true)}
className="min-w-[44px] min-h-[44px] flex items-center justify-center rounded-lg border border-krona-border bg-krona-surface text-krona-primary hover:bg-krona-hover transition-colors"
aria-label="Открыть меню панели оператора"
data-testid="mobile-menu-button"
>
<Menu className="w-5 h-5" />
</button>
<div className="flex items-center gap-1.5 min-w-0">
<div className="w-7 h-7 rounded-md bg-krona-surface border border-krona-gold/40 flex items-center justify-center text-krona-gold font-extrabold text-xs shrink-0">
K
</div>
<div className="min-w-0">
<div className="text-xs font-bold text-krona-primary truncate leading-tight">
{currentTitle}
</div>
<div className="text-[10px] text-krona-gold font-medium truncate">
Control Center
</div>
</div>
</div>
</div>
{/* Right: Critical Badge + Operator Profile button */}
<div className="flex items-center gap-1.5 shrink-0">
{criticalCount > 0 && (
<button
type="button"
onClick={() => navigate('/control/incidents')}
className="min-w-[44px] min-h-[44px] flex items-center justify-center rounded-lg bg-status-danger-soft border border-status-danger-border text-status-danger-text text-xs font-bold"
aria-label={`Критические события: ${criticalCount}`}
title="Есть критические события платформы"
data-testid="mobile-critical-indicator"
>
<ShieldAlert className="w-4 h-4 animate-pulse mr-1" />
<span>{criticalCount}</span>
</button>
)}
<button
type="button"
onClick={() => setIsMobileDrawerOpen(true)}
className="min-w-[44px] min-h-[44px] px-2.5 flex items-center gap-1.5 rounded-lg border border-krona-gold/30 bg-krona-gold/10 text-krona-gold-text text-xs font-semibold"
aria-label={`Профиль оператора: ${currentRoleLabel}`}
title="Профиль оператора платформы"
data-testid="mobile-operator-profile-btn"
>
<ShieldCheck className="w-4 h-4 text-krona-gold shrink-0" />
<span className="truncate max-w-[80px]">{currentRoleLabel}</span>
</button>
</div>
</header>
{/* ========================================================================= */}
{/* 3. MAIN LAYOUT (Sidebar on Desktop/Tablet + Content Body) */}
{/* ========================================================================= */}
<div className="flex-1 flex min-h-0 relative max-w-full">
{/* Desktop & Tablet Sidebar */}
<aside
className={`hidden md:flex flex-col border-r border-krona-border bg-krona-workspace transition-all duration-200 shrink-0 z-20 ${
isTabletSidebarCollapsed ? 'w-16' : 'w-56 lg:w-64'
}`}
data-testid="desktop-sidebar"
>
{/* Tablet Collapse Toggle */}
<div className="p-2 border-b border-krona-border flex items-center justify-between">
<span className={`text-[11px] font-semibold text-krona-tertiary uppercase tracking-wider px-2 ${isTabletSidebarCollapsed ? 'hidden' : 'block'}`}>
Навигация
</span>
<button
type="button"
onClick={() => setIsTabletSidebarCollapsed(!isTabletSidebarCollapsed)}
className="p-1.5 rounded-md hover:bg-krona-hover text-krona-secondary hover:text-krona-primary transition-colors ml-auto"
title={isTabletSidebarCollapsed ? 'Развернуть меню' : 'Свернуть меню'}
>
{isTabletSidebarCollapsed ? <ChevronRight className="w-4 h-4" /> : <ChevronLeft className="w-4 h-4" />}
</button>
</div>
{/* Navigation Links */}
<nav className="flex-1 p-2 space-y-1 overflow-y-auto">
{allNavItems.map((item) => {
const Icon = item.icon;
return (
<NavLink
key={item.to}
to={item.to}
end={item.end}
className={({ isActive }) =>
`flex items-center min-h-[40px] px-3 py-2 rounded-lg text-xs font-medium transition-colors ${
isActive
? 'bg-krona-gold/15 text-krona-gold font-bold border border-krona-gold/30'
: 'text-krona-secondary hover:bg-krona-hover hover:text-krona-primary'
}`
}
title={item.label}
data-testid={`nav-${item.to.replace('/control', '').replace('/', '') || 'overview'}`}
>
<Icon className="w-4 h-4 shrink-0" />
{!isTabletSidebarCollapsed && (
<span className="ml-2.5 truncate flex-1">{item.label}</span>
)}
{!isTabletSidebarCollapsed && item.badge && item.badge > 0 && (
<span className="ml-auto px-1.5 py-0.5 rounded-full text-[10px] font-mono font-bold bg-status-danger-soft text-status-danger-text border border-status-danger-border">
{item.badge}
</span>
)}
</NavLink>
);
})}
</nav>
{/* Footer Info in Sidebar */}
{!isTabletSidebarCollapsed && (
<div className="p-3 border-t border-krona-border text-[11px] text-krona-tertiary font-mono space-y-1 bg-krona-surface/30">
<div className="flex items-center justify-between">
<span>{RU_UI.system.nodeLabel}:</span>
<span className="font-semibold text-krona-secondary">srv-msk-prod-01</span>
</div>
<div className="flex items-center justify-between">
<span>{RU_UI.scope}:</span>
<span className="text-status-success-text">Боевой (Live)</span>
</div>
<NavLink
to="/"
className="mt-2 pt-2 border-t border-krona-border flex items-center gap-1.5 text-krona-gold hover:underline text-[11px]"
>
<ExternalLink className="w-3.5 h-3.5" />
<span>{RU_UI.system.exitToClient}</span>
</NavLink>
</div>
)}
</aside>
{/* Main Content Area */}
<main className="flex-1 min-w-0 max-w-full overflow-y-auto overflow-x-hidden p-3 sm:p-5 lg:p-6 pb-20 md:pb-6">
<div className="max-w-7xl mx-auto w-full">
{/* Feedback Alert if Database Reset */}
{showSavedFeedback && (
<div
className="mb-4 p-3.5 rounded-xl border border-status-success-border bg-status-success-soft text-status-success-text text-xs flex items-center gap-2 shadow-md animate-in fade-in"
role="status"
>
<CheckCircle2 className="w-4 h-4 shrink-0" />
<span>Демонстрационные данные успешно сброшены к эталонному состоянию.</span>
</div>
)}
{/* Outage Banner */}
<ServerMonitorOutageBanner />
{/* Outlet for Page Routes */}
<Outlet />
</div>
</main>
</div>
{/* ========================================================================= */}
{/* 4. MOBILE BOTTOM BAR (< md) */}
{/* ========================================================================= */}
<nav
className="md:hidden fixed bottom-0 left-0 right-0 h-16 border-t border-krona-border bg-krona-workspace/95 backdrop-blur-md px-1 flex items-center justify-around z-40"
data-testid="mobile-bottom-bar"
aria-label="Основная мобильная навигация"
>
{primaryMobileNav.map((item) => {
const Icon = item.icon;
const isActive = item.end ? location.pathname === item.to : location.pathname.startsWith(item.to);
return (
<NavLink
key={item.to}
to={item.to}
end={item.end}
className={`flex-1 min-h-[48px] min-w-[44px] flex flex-col items-center justify-center rounded-lg py-1 transition-colors relative ${
isActive ? 'text-krona-gold font-bold' : 'text-krona-secondary hover:text-krona-primary'
}`}
data-testid={`bottom-nav-${item.to.replace('/control', '').replace('/', '') || 'overview'}`}
>
<Icon className={`w-5 h-5 ${isActive ? 'stroke-[2.5]' : ''}`} />
<span className="text-[10px] mt-0.5 leading-none truncate max-w-[64px]">{item.label}</span>
{item.badge && item.badge > 0 && (
<span className="absolute top-1 right-3 w-4 h-4 rounded-full bg-status-danger-text text-white text-[9px] font-bold flex items-center justify-center animate-pulse">
{item.badge}
</span>
)}
</NavLink>
);
})}
{/* "More" Button */}
<button
type="button"
onClick={() => setIsMobileDrawerOpen(true)}
className={`flex-1 min-h-[48px] min-w-[44px] flex flex-col items-center justify-center rounded-lg py-1 transition-colors ${
isMobileDrawerOpen ? 'text-krona-gold font-bold' : 'text-krona-secondary hover:text-krona-primary'
}`}
aria-label="Ещё разделы и настройки"
data-testid="bottom-nav-more"
>
<SlidersHorizontal className="w-5 h-5" />
<span className="text-[10px] mt-0.5 leading-none">Ещё</span>
</button>
</nav>
{/* ========================================================================= */}
{/* 5. MOBILE DRAWER / BOTTOM SHEET */}
{/* ========================================================================= */}
{isMobileDrawerOpen && (
<div
className="fixed inset-0 z-50 flex flex-col justify-end bg-black/60 backdrop-blur-xs md:hidden animate-in fade-in duration-200"
role="dialog"
aria-modal="true"
aria-label="Меню оператора платформы"
onClick={() => setIsMobileDrawerOpen(false)}
>
<div
className="w-full max-h-[85vh] bg-krona-workspace border-t border-krona-border rounded-t-2xl flex flex-col overflow-hidden shadow-2xl animate-in slide-in-from-bottom duration-250"
onClick={(e) => e.stopPropagation()}
>
{/* Sheet Header */}
<div className="px-4 py-3 border-b border-krona-border flex items-center justify-between shrink-0 bg-krona-surface">
<div className="flex items-center gap-2">
<div className="w-6 h-6 rounded-md bg-krona-gold/20 text-krona-gold font-bold flex items-center justify-center text-xs">
K
</div>
<span className="font-bold text-sm text-krona-primary">Панель оператора</span>
</div>
<button
type="button"
onClick={() => setIsMobileDrawerOpen(false)}
className="min-w-[44px] min-h-[44px] flex items-center justify-center rounded-lg text-krona-secondary hover:text-krona-primary hover:bg-krona-hover"
aria-label="Закрыть меню"
data-testid="close-mobile-drawer"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Scrollable Sheet Content */}
<div className="p-4 overflow-y-auto space-y-4">
{/* Operator Role Section */}
<div className="p-3 rounded-xl bg-krona-surface border border-krona-border">
<div className="text-[11px] font-semibold text-krona-tertiary uppercase tracking-wider mb-2">
Сессия оператора
</div>
<div className="flex items-center justify-between gap-2">
<div className="min-w-0">
<div className="font-bold text-sm text-krona-primary">{operator.name}</div>
<div className="text-xs text-krona-gold font-medium">{RU_ROLES[role]?.label}</div>
</div>
<OperatorSessionSwitcher variant="compact" />
</div>
</div>
{/* Theme Switcher Section */}
<div className="p-3 rounded-xl bg-krona-surface border border-krona-border">
<div className="text-[11px] font-semibold text-krona-tertiary uppercase tracking-wider mb-2">
{RU_UI.system.themeLabel}
</div>
<div className="grid grid-cols-3 gap-2">
{(['soft-light', 'light', 'dark'] as ThemeMode[]).map((t) => (
<button
key={t}
type="button"
onClick={() => setTheme(t)}
className={`min-h-[44px] px-2 py-2 rounded-lg text-xs font-semibold flex flex-col items-center justify-center transition-colors border ${
themeMode === t
? 'border-krona-gold bg-krona-gold/15 text-krona-gold shadow-xs'
: 'border-krona-border bg-krona-workspace text-krona-secondary hover:text-krona-primary'
}`}
data-testid={`mobile-theme-${t}`}
>
{t === 'soft-light' && <Sparkles className="w-4 h-4 mb-0.5" />}
{t === 'light' && <Sun className="w-4 h-4 mb-0.5" />}
{t === 'dark' && <Moon className="w-4 h-4 mb-0.5" />}
<span className="text-[10px] text-center leading-tight">{RU_THEMES[t]}</span>
</button>
))}
</div>
</div>
{/* More Navigation Links */}
<div className="p-3 rounded-xl bg-krona-surface border border-krona-border">
<div className="text-[11px] font-semibold text-krona-tertiary uppercase tracking-wider mb-2">
Все разделы
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-1.5">
{allNavItems.map((item) => {
const Icon = item.icon;
const isActive = item.end ? location.pathname === item.to : location.pathname.startsWith(item.to);
return (
<NavLink
key={item.to}
to={item.to}
end={item.end}
className={`min-h-[44px] px-3 py-2.5 rounded-lg text-xs flex items-center justify-between border transition-colors ${
isActive
? 'border-krona-gold/40 bg-krona-gold/15 text-krona-gold font-bold'
: 'border-transparent text-krona-secondary hover:text-krona-primary hover:bg-krona-hover'
}`}
data-testid={`drawer-nav-${item.to.replace('/control', '').replace('/', '') || 'overview'}`}
>
<div className="flex items-center gap-2.5 min-w-0">
<Icon className="w-4 h-4 shrink-0" />
<span className="truncate">{item.label}</span>
</div>
{item.badge && item.badge > 0 && (
<span className="px-2 py-0.5 rounded-full text-[10px] font-bold bg-status-danger-soft text-status-danger-text border border-status-danger-border">
{item.badge}
</span>
)}
</NavLink>
);
})}
</div>
</div>
{/* System Actions */}
<div className="p-3 rounded-xl bg-krona-surface border border-krona-border space-y-2">
<div className="text-[11px] font-semibold text-krona-tertiary uppercase tracking-wider">
Сервисные действия
</div>
<button
type="button"
onClick={toggleServerMonitorOutage}
className={`w-full min-h-[44px] px-3 py-2 rounded-lg text-xs font-semibold flex items-center justify-between border transition-colors ${
serverMonitorOutage
? 'border-status-danger-border bg-status-danger-soft text-status-danger-text'
: 'border-krona-border bg-krona-workspace text-krona-secondary hover:text-krona-primary'
}`}
data-testid="drawer-toggle-outage"
>
<div className="flex items-center gap-2">
<Radio className="w-4 h-4" />
<span>Сбой мониторинга (Circuit Breaker)</span>
</div>
<span className="text-[11px] font-mono">
{serverMonitorOutage ? 'ВКЛ' : 'ВЫКЛ'}
</span>
</button>
<button
type="button"
onClick={handleReset}
disabled={isResetting}
className="w-full min-h-[44px] px-3 py-2 rounded-lg text-xs font-medium border border-krona-border bg-krona-workspace text-krona-secondary hover:text-krona-primary flex items-center justify-center gap-2 transition-colors"
data-testid="drawer-reset-data"
>
<RotateCcw className={`w-4 h-4 ${isResetting ? 'animate-spin text-krona-gold' : ''}`} />
<span>Сбросить демоданные к эталону</span>
</button>
<NavLink
to="/"
className="w-full min-h-[44px] px-3 py-2 rounded-lg text-xs font-medium border border-krona-border bg-krona-workspace text-krona-gold hover:bg-krona-hover flex items-center justify-center gap-2 transition-colors"
>
<ExternalLink className="w-4 h-4" />
<span>Перейти в Krona Business OS</span>
</NavLink>
</div>
{/* Node info */}
<div className="text-center text-[11px] text-krona-tertiary font-mono pt-1 pb-2">
srv-msk-prod-01 • Данные синхронизированы (IndexedDB)
</div>
</div>
</div>
</div>
)}
{/* Global Contextual Modals */}
<InspectorDrawer />
<ConfirmOperatorActionModal />
</div>
);
};