import { create } from 'zustand'; import type { OperatorRole, OperatorProfile, OrganizationProjection, ModuleDefinition, Subscription, Installation, ModuleInstance, ServerNode, Incident, Announcement, Rollout, OperatorAuditEvent, PlatformStatus, RolloutStage, } from '../contracts'; import { OPERATOR_ROLE_DEFINITIONS } from '../contracts'; import { initKccStorage, persistItem, computeSha256 } from './db'; export interface InspectorState { type: 'organization' | 'server' | 'incident' | 'module' | 'installation'; id: string; } export interface ConfirmActionModalConfig { title: string; actionName: string; impactSummary: string; targetType: OperatorAuditEvent['target']['type']; targetId: string; targetName: string; onConfirm: (reason: string) => Promise | void; } interface KccStoreState { // Session & RBAC activeRole: OperatorRole; activeOperator: OperatorProfile; isAccessGatePassed: boolean; // Data isLoading: boolean; isInitialized: boolean; serverMonitorOutage: boolean; // Mock-состояние «Server Monitor недоступен» organizations: OrganizationProjection[]; modules: ModuleDefinition[]; subscriptions: Subscription[]; installations: Installation[]; instances: ModuleInstance[]; servers: ServerNode[]; incidents: Incident[]; announcements: Announcement[]; rollouts: Rollout[]; auditLog: OperatorAuditEvent[]; // Inspector & Action Modal inspectorTarget: InspectorState | null; pendingConfirmModal: ConfirmActionModalConfig | null; // Actions initialize: (forceReset?: boolean) => Promise; setActiveRole: (role: OperatorRole) => void; setAccessGatePassed: (passed: boolean) => void; openInspector: (type: InspectorState['type'], id: string) => void; closeInspector: () => void; setServerMonitorOutage: (outage: boolean) => void; toggleServerMonitorOutage: () => void; openConfirmModal: (config: ConfirmActionModalConfig) => void; closeConfirmModal: () => void; // Mutations with audit recording recordAuditEvent: (params: { action: string; target: OperatorAuditEvent['target']; reason: string; impactSummary: string; }) => Promise; updateOrganizationStatus: (tenantId: string, status: PlatformStatus, reason: string) => Promise; grantModuleTrial: ( tenantId: string, moduleId: string, durationDays: number, reason: string ) => Promise; grantGracePeriod: (subscriptionId: string, days: number, reason: string) => Promise; renewSubscription: ( subscriptionId: string, newPeriod: 'month' | 'year', activeModules: string[], reason: string ) => Promise; createIncidentFromAlert: ( serverId: string, summary: string, severity: Incident['severity'], reason: string ) => Promise; resolveIncident: (incidentId: string, reason: string) => Promise; createAnnouncement: ( announcement: Omit, reason: string ) => Promise; triggerRolloutStage: (rolloutId: string, stage: RolloutStage, percentage: number, reason: string) => Promise; createRollout: ( moduleId: string, version: string, stage: RolloutStage, pilotTenantIds: string[], percentage: number, notes: string, reason: string ) => Promise; requestServerDiagnostics: (serverId: string, reason: string) => Promise; scheduleServerMaintenance: (serverId: string, reason: string) => Promise; resetToDefaults: () => Promise; } const DEFAULT_OPERATORS: Record = { platform_owner: { id: 'op-001', name: 'Михаил Романов', role: 'platform_owner', email: 'm.romanov@krona.systems', title: 'Владелец платформы & Архитектор', }, platform_admin: { id: 'op-002', name: 'Алексей Смирнов', role: 'platform_admin', email: 'a.smirnov@krona.systems', title: 'Главный системный администратор', }, support_operator: { id: 'op-003', name: 'Елена Соколова', role: 'support_operator', email: 'e.sokolova@krona.systems', title: 'Ведущий инженер поддержки L2', }, billing_manager: { id: 'op-004', name: 'Анна Кузнецова', role: 'billing_manager', email: 'a.kuznetsova@krona.systems', title: 'Руководитель биллинговых операций', }, security_auditor: { id: 'op-005', name: 'Сергей Белов', role: 'security_auditor', email: 's.belov@krona.systems', title: 'Офицер информационной безопасности', }, release_manager: { id: 'op-006', name: 'Мария Лебедева', role: 'release_manager', email: 'm.lebedeva@krona.systems', title: 'Руководитель релизного конвейера', }, }; export const useKccStore = create((set, get) => ({ activeRole: 'platform_owner', activeOperator: DEFAULT_OPERATORS.platform_owner, isAccessGatePassed: typeof window !== 'undefined' ? localStorage.getItem('kcc_access_gate') === '1' : false, isLoading: false, isInitialized: false, serverMonitorOutage: false, organizations: [], modules: [], subscriptions: [], installations: [], instances: [], servers: [], incidents: [], announcements: [], rollouts: [], auditLog: [], inspectorTarget: null, pendingConfirmModal: null, initialize: async (forceReset = false) => { set({ isLoading: true }); try { const data = await initKccStorage(forceReset); set({ ...data, isInitialized: true, isLoading: false, }); } catch (err) { console.error('[KCC DB] Failed to init indexedDB:', err); set({ isLoading: false }); } }, setActiveRole: (role) => { set({ activeRole: role, activeOperator: DEFAULT_OPERATORS[role], }); }, setAccessGatePassed: (passed) => { if (typeof window !== 'undefined') { if (passed) localStorage.setItem('kcc_access_gate', '1'); else localStorage.removeItem('kcc_access_gate'); } set({ isAccessGatePassed: passed }); }, openInspector: (type, id) => set({ inspectorTarget: { type, id } }), closeInspector: () => set({ inspectorTarget: null }), setServerMonitorOutage: (outage) => set({ serverMonitorOutage: outage }), toggleServerMonitorOutage: () => set((state) => ({ serverMonitorOutage: !state.serverMonitorOutage })), openConfirmModal: (config) => set({ pendingConfirmModal: config }), closeConfirmModal: () => set({ pendingConfirmModal: null }), recordAuditEvent: async ({ action, target, reason, impactSummary }) => { const { activeOperator, auditLog } = get(); const prevEvent = auditLog[0]; const previousHash = prevEvent ? prevEvent.hash : 'GENESIS_HASH_00000000000000000000000000000000000000000000000000000000000000'; const eventId = `evt-${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 6)}`; const occurredAt = new Date().toISOString(); const correlationId = `corr-${Date.now().toString(36)}`; const hashPayload = `${previousHash}|${occurredAt}|${activeOperator.id}|${activeOperator.role}|${action}|${target.type}:${target.id}|${reason}|${correlationId}`; const hash = await computeSha256(hashPayload); const event: OperatorAuditEvent = { eventId, occurredAt, operatorId: activeOperator.id, operatorName: activeOperator.name, operatorRole: activeOperator.role, action, target, reason, impactSummary, correlationId, previousHash, hash, }; await persistItem('audit_events', event); set((state) => ({ auditLog: [event, ...state.auditLog] })); return event; }, updateOrganizationStatus: async (tenantId, status, reason) => { const { organizations, recordAuditEvent } = get(); const org = organizations.find((o) => o.tenantId === tenantId); if (!org) return; const oldStatus = org.platformStatus; const updated: OrganizationProjection = { ...org, platformStatus: status, projectedAt: new Date().toISOString(), }; await persistItem('organizations', updated); set((state) => ({ organizations: state.organizations.map((o) => (o.tenantId === tenantId ? updated : o)), })); await recordAuditEvent({ action: 'organization.status_changed', target: { type: 'organization', id: tenantId, displayName: org.displayName, tenantId, }, reason, impactSummary: `Статус организации изменен с «${oldStatus}» на «${status}»`, }); }, grantModuleTrial: async (tenantId, moduleId, durationDays, reason) => { const { organizations, modules, recordAuditEvent } = get(); const org = organizations.find((o) => o.tenantId === tenantId); const mod = modules.find((m) => m.moduleId === moduleId); if (!org || !mod) return; const trialUntil = new Date(Date.now() + durationDays * 24 * 3600 * 1000).toISOString(); const updated: OrganizationProjection = { ...org, installedModulesCount: org.installedModulesCount + 1, tariffPlan: `${org.tariffPlan} (+Trial ${mod.name})`, projectedAt: new Date().toISOString(), }; await persistItem('organizations', updated); set((state) => ({ organizations: state.organizations.map((o) => (o.tenantId === tenantId ? updated : o)), })); await recordAuditEvent({ action: 'entitlement.trial_granted', target: { type: 'organization', id: tenantId, displayName: org.displayName, tenantId, }, reason, impactSummary: `Выдан пробный доступ к модулю «${mod.name}» на ${durationDays} дней (до ${trialUntil.slice(0, 10)})`, }); }, grantGracePeriod: async (subscriptionId, days, reason) => { const { subscriptions, recordAuditEvent } = get(); const sub = subscriptions.find((s) => s.subscriptionId === subscriptionId); if (!sub) return; const graceUntil = new Date(Date.now() + days * 24 * 3600 * 1000).toISOString(); const updated: Subscription = { ...sub, status: 'grace', graceUntil, history: [ { eventAt: new Date().toISOString(), type: 'grace_granted', description: `Предоставлен льготный период на ${days} дней`, actor: get().activeOperator.name, }, ...sub.history, ], }; await persistItem('subscriptions', updated); set((state) => ({ subscriptions: state.subscriptions.map((s) => (s.subscriptionId === subscriptionId ? updated : s)), })); await recordAuditEvent({ action: 'subscription.grace_period_granted', target: { type: 'subscription', id: subscriptionId, displayName: sub.tenantName, tenantId: sub.tenantId, }, reason, impactSummary: `Установлен Grace Period на ${days} дней (до ${graceUntil.slice(0, 10)})`, }); }, renewSubscription: async (subscriptionId, newPeriod, activeModules, reason) => { const { subscriptions, recordAuditEvent } = get(); const sub = subscriptions.find((s) => s.subscriptionId === subscriptionId); if (!sub) return; const currentEnd = new Date(sub.validUntil).getTime(); const addMs = (newPeriod === 'year' ? 365 : 30) * 24 * 3600 * 1000; const newUntil = new Date(Math.max(Date.now(), currentEnd) + addMs).toISOString(); const updated: Subscription = { ...sub, period: newPeriod, activeModules, status: 'normal', validUntil: newUntil, renewsAt: newUntil, graceUntil: null, history: [ { eventAt: new Date().toISOString(), type: 'renewed', description: `Продление тарифа на ${newPeriod === 'year' ? '1 год' : '1 месяц'}`, actor: get().activeOperator.name, }, ...sub.history, ], }; await persistItem('subscriptions', updated); set((state) => ({ subscriptions: state.subscriptions.map((s) => (s.subscriptionId === subscriptionId ? updated : s)), })); await recordAuditEvent({ action: 'subscription.renewed', target: { type: 'subscription', id: subscriptionId, displayName: sub.tenantName, tenantId: sub.tenantId, }, reason, impactSummary: `Продление подписки до ${newUntil.slice(0, 10)}, активных модулей: ${activeModules.length}`, }); }, createIncidentFromAlert: async (serverId, summary, severity, reason) => { const { servers, instances, recordAuditEvent, incidents } = get(); const srv = servers.find((s) => s.serverId === serverId); const affectedTenantIds = instances .filter((i) => i.serverId === serverId) .map((i) => i.tenantId); const affectedModuleIds = Array.from( new Set(instances.filter((i) => i.serverId === serverId).map((i) => i.moduleId)) ); const incNumber = `INC-${1050 + incidents.length}`; const incidentId = `inc-${Date.now().toString(36)}`; const openedAt = new Date().toISOString(); const newIncident: Incident = { incidentId, incidentNumber: incNumber, severity, status: 'open', summary, description: `Инцидент открыт оператором из телеметрического оповещения узла ${srv ? srv.name : serverId}.`, affectedTenantIds, affectedServerIds: [serverId], affectedModuleIds, ownerName: get().activeOperator.name, openedAt, timeline: [ { timestamp: openedAt, author: get().activeOperator.name, message: `Инцидент зарегистрирован в системе. Причина: ${reason}`, }, ], }; await persistItem('incidents', newIncident); set((state) => ({ incidents: [newIncident, ...state.incidents] })); await recordAuditEvent({ action: 'incident.created', target: { type: 'incident', id: incidentId, displayName: `${incNumber} (${summary})`, }, reason, impactSummary: `Создан инцидент ${incNumber} со степенью критичности ${severity}`, }); }, resolveIncident: async (incidentId, reason) => { const { incidents, recordAuditEvent, activeOperator } = get(); const inc = incidents.find((i) => i.incidentId === incidentId); if (!inc) return; const resolvedAt = new Date().toISOString(); const updated: Incident = { ...inc, status: 'resolved', resolvedAt, timeline: [ ...inc.timeline, { timestamp: resolvedAt, author: activeOperator.name, message: `Инцидент переведен в статус resolved. Решение: ${reason}`, }, ], }; await persistItem('incidents', updated); set((state) => ({ incidents: state.incidents.map((i) => (i.incidentId === incidentId ? updated : i)), })); await recordAuditEvent({ action: 'incident.resolved', target: { type: 'incident', id: incidentId, displayName: inc.incidentNumber, }, reason, impactSummary: `Инцидент ${inc.incidentNumber} успешно разрешен`, }); }, createAnnouncement: async (announcementData, reason) => { const { recordAuditEvent, activeOperator, organizations } = get(); const announcementId = `anc-${Date.now().toString(36)}`; const createdAt = new Date().toISOString(); let targetCount = organizations.length; if (announcementData.audience.tenantIds && announcementData.audience.tenantIds.length > 0) { targetCount = announcementData.audience.tenantIds.length; } const newAnnouncement: Announcement = { ...announcementData, announcementId, createdAt, author: activeOperator.name, stats: { totalTargeted: targetCount, queued: targetCount, delivered: 0, failed: 0, }, }; await persistItem('announcements', newAnnouncement); set((state) => ({ announcements: [newAnnouncement, ...state.announcements] })); await recordAuditEvent({ action: 'announcement.created', target: { type: 'announcement', id: announcementId, displayName: announcementData.title, }, reason, impactSummary: `Создано оповещение «${announcementData.title}» для ${targetCount} организаций`, }); }, triggerRolloutStage: async (rolloutId, stage, percentage, reason) => { const { rollouts, recordAuditEvent } = get(); const rol = rollouts.find((r) => r.rolloutId === rolloutId); if (!rol) return; const updated: Rollout = { ...rol, stage, percentage, updatedAt: new Date().toISOString(), }; await persistItem('rollouts', updated); set((state) => ({ rollouts: state.rollouts.map((r) => (r.rolloutId === rolloutId ? updated : r)), })); await recordAuditEvent({ action: `rollout.stage_${stage}`, target: { type: 'rollout', id: rolloutId, displayName: `${rol.moduleName} ${rol.targetVersion}`, }, reason, impactSummary: `Раскатка переведена в стадию «${stage}» (${percentage}%)`, }); }, createRollout: async (moduleId, version, stage, pilotTenantIds, percentage, notes, reason) => { const { modules, recordAuditEvent } = get(); const mod = modules.find((m) => m.moduleId === moduleId); const rolloutId = `rol-${Date.now().toString(36)}`; const now = new Date().toISOString(); const newRollout: Rollout = { rolloutId, moduleId, moduleName: mod ? mod.name : moduleId, targetVersion: version, stage, pilotTenantIds, percentage, startedAt: now, updatedAt: now, notes, }; await persistItem('rollouts', newRollout); set((state) => ({ rollouts: [newRollout, ...state.rollouts] })); await recordAuditEvent({ action: 'rollout.created', target: { type: 'rollout', id: rolloutId, displayName: `${newRollout.moduleName} ${version}`, }, reason, impactSummary: `Инициирован конвейер раскатки ${newRollout.moduleName} ${version} (стадия ${stage})`, }); }, requestServerDiagnostics: async (serverId, reason) => { const { servers, recordAuditEvent } = get(); const srv = servers.find((s) => s.serverId === serverId); if (!srv) return; await recordAuditEvent({ action: 'infrastructure.diagnostics_requested', target: { type: 'infrastructure', id: serverId, displayName: srv.name, }, reason, impactSummary: `Запрошен полный дамп телеметрии и логов с узла ${srv.name}`, }); }, scheduleServerMaintenance: async (serverId, reason) => { const { servers, recordAuditEvent } = get(); const srv = servers.find((s) => s.serverId === serverId); if (!srv) return; await recordAuditEvent({ action: 'infrastructure.maintenance_scheduled', target: { type: 'infrastructure', id: serverId, displayName: srv.name, }, reason, impactSummary: `Запланировано регламентное техническое обслуживание для узла ${srv.name}`, }); }, resetToDefaults: async () => { await get().initialize(true); }, })); export function useOperatorRole() { const activeRole = useKccStore((state) => state.activeRole); const activeOperator = useKccStore((state) => state.activeOperator); const roleDef = OPERATOR_ROLE_DEFINITIONS[activeRole]; return { role: activeRole, operator: activeOperator, definition: roleDef, permissions: roleDef.permissions, }; }