'use client'; import React, { useState, useEffect, useRef, useCallback } from 'react'; import { OperationsRole, OpsAction, OpsActionType, OpsActionState, OpsActionCapabilities, OpsActionsResponse, } from './types'; interface OperationsActionsProps { role: OperationsRole; onActionCompleted?: () => void; } const ACTION_CONFIGS: Record< OpsActionType, { title: string; description: string; icon: string; destructive: boolean; requiredConfirmation?: string; } > = { deploy_website: { title: 'Deploy Website', description: 'Pulls latest master commit, builds Next.js production bundle, verifies health, and updates current symlink with automatic rollback.', icon: 'rocket_launch', destructive: false, }, restart_website: { title: 'Restart Website', description: 'Gracefully restarts the motorsignal-website systemd service and verifies /api/version ingress readiness.', icon: 'restart_alt', destructive: false, }, restart_api: { title: 'Restart API', description: 'Initiates detached systemd restart of motorsignal-api. In-flight requests may terminate. Type confirmation below.', icon: 'sync_problem', destructive: true, requiredConfirmation: 'RESTART API', }, reboot_server: { title: 'Reboot Host Server', description: 'Schedules full host server reboot. All services, website, and API traffic will be temporarily unavailable. Type confirmation below.', icon: 'power_settings_new', destructive: true, requiredConfirmation: 'REBOOT MOTORSIGNAL SERVER', }, }; const ACTIVE_STATES: OpsActionState[] = [ 'queued', 'preflight', 'running', 'verifying', 'rollback_running', ]; export function OperationsActions({ role, onActionCompleted }: OperationsActionsProps) { const [capabilities, setCapabilities] = useState(null); const [actions, setActions] = useState([]); const [activeAction, setActiveAction] = useState(null); const [loading, setLoading] = useState(true); const [historyError, setHistoryError] = useState(null); // Modal State const [modalActionType, setModalActionType] = useState(null); const [confirmationInput, setConfirmationInput] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); const [submitError, setSubmitError] = useState(null); const pollTimeoutRef = useRef(null); const isOwner = role === 'owner'; // Fetch capabilities & recent action history const fetchActions = useCallback(async () => { try { const resp = await fetch('/api/ops/actions?limit=10', { headers: { accept: 'application/json' }, cache: 'no-store', }); if (resp.ok) { const data: OpsActionsResponse = await resp.json(); setActions(data.actions || []); setHistoryError(null); // Check if there is an active action const active = (data.actions || []).find((a) => ACTIVE_STATES.includes(a.state) ); setActiveAction(active || null); } else { setHistoryError('Unable to load operations action history.'); } } catch { setHistoryError('Network error connecting to operations actions platform.'); } finally { setLoading(false); } }, []); const fetchCapabilities = useCallback(async () => { try { const resp = await fetch('/api/ops/actions/capabilities', { headers: { accept: 'application/json' }, cache: 'no-store', }); if (resp.ok) { const data: OpsActionCapabilities = await resp.json(); setCapabilities(data); } } catch { // Non-blocking } }, []); useEffect(() => { void fetchCapabilities(); void fetchActions(); }, [fetchCapabilities, fetchActions]); // Polling when an action is active useEffect(() => { let isCancelled = false; if (activeAction) { const poll = async () => { try { const resp = await fetch(`/api/ops/actions/${encodeURIComponent(activeAction.id)}`, { headers: { accept: 'application/json' }, cache: 'no-store', }); if (resp.ok && !isCancelled) { const updated: OpsAction = await resp.json(); if (!ACTIVE_STATES.includes(updated.state)) { setActiveAction(null); void fetchActions(); if (onActionCompleted) onActionCompleted(); return; } else { setActiveAction(updated); } } } catch { // Non-blocking error } if (!isCancelled) { pollTimeoutRef.current = setTimeout(() => { void poll(); }, 2500); } }; pollTimeoutRef.current = setTimeout(() => { void poll(); }, 2500); } return () => { isCancelled = true; if (pollTimeoutRef.current) { clearTimeout(pollTimeoutRef.current); pollTimeoutRef.current = null; } }; }, [activeAction, fetchActions, onActionCompleted]); const handleOpenModal = (actionType: OpsActionType) => { if (!isOwner || activeAction) return; setModalActionType(actionType); setConfirmationInput(''); setSubmitError(null); }; const handleCloseModal = () => { setModalActionType(null); setConfirmationInput(''); setSubmitError(null); }; const handleSubmitAction = async (e: React.FormEvent) => { e.preventDefault(); if (!modalActionType || !isOwner || isSubmitting) return; const config = ACTION_CONFIGS[modalActionType]; if ( config.requiredConfirmation && confirmationInput.trim() !== config.requiredConfirmation ) { setSubmitError(`Please type exact confirmation text: "${config.requiredConfirmation}"`); return; } setIsSubmitting(true); setSubmitError(null); const idempotencyKey = `ops-action-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; try { const resp = await fetch('/api/ops/actions', { method: 'POST', headers: { 'content-type': 'application/json', accept: 'application/json', }, body: JSON.stringify({ action_type: modalActionType, confirmation: confirmationInput.trim() || undefined, idempotency_key: idempotencyKey, }), }); const data = await resp.json().catch(() => null); if (resp.ok) { setActiveAction(data); handleCloseModal(); void fetchActions(); } else if (resp.status === 409) { setSubmitError(data?.detail || 'Another operations action is currently active.'); } else if (resp.status === 403) { setSubmitError(data?.detail || 'Owner role authority required.'); } else { setSubmitError(data?.detail || 'Failed to trigger operations action.'); } } catch { setSubmitError('Network failure submitting action request.'); } finally { setIsSubmitting(false); } }; const getStateBadge = (state: OpsActionState) => { switch (state) { case 'succeeded': return ( check_circle Succeeded ); case 'rolled_back': return ( undo Rolled Back ); case 'failed': case 'rollback_failed': return ( error {state === 'rollback_failed' ? 'Rollback Failed' : 'Failed'} ); case 'queued': case 'preflight': case 'running': case 'verifying': case 'rollback_running': return ( progress_activity {state.replace('_', ' ').toUpperCase()} ); default: return ( {state} ); } }; return (
{/* Active Action Banner */} {activeAction && (
progress_activity

Action in Progress: {ACTION_CONFIGS[activeAction.action_type]?.title || activeAction.action_type} {getStateBadge(activeAction.state)}

{activeAction.status_message || 'Executing action across system boundary...'}

ID: {activeAction.id.substring(0, 8)}...
)} {/* Role Notice for Operators */} {!isOwner && (
lock Owner role required to execute operational actions. Operator accounts have read-only visibility into action status and history.
)} {/* Execution Boundary / Helper Status Notice */} {capabilities && !capabilities.helper_configured && (
warning Action execution boundary helper is unconfigured or not executable on this host. Mutating actions will be rejected by backend policy.
)} {/* Action Trigger Cards */}
{(Object.keys(ACTION_CONFIGS) as OpsActionType[]).map((actionKey) => { const config = ACTION_CONFIGS[actionKey]; const isDisabled = !isOwner || !!activeAction; return (
{config.icon}

{config.title}

{config.destructive && ( Guarded )}

{config.description}

{actionKey}
); })}
{/* Action History Section */}

Operations Action History

Audited system actions and deployments executed across privileged execution boundary.

{historyError && (
{historyError}
)} {actions.length === 0 && !loading && (
No operations actions have been executed yet.
)} {actions.length > 0 && (
{actions.map((act) => ( ))}
Timestamp Action Actor Status Details Revision
{new Date(act.request_timestamp).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', })} {ACTION_CONFIGS[act.action_type]?.title || act.action_type} {act.actor_identity} {getStateBadge(act.state)} {act.status_message || (act.failure_code ? `Error: ${act.failure_code}` : '—')} {act.deployed_revision ? act.deployed_revision.substring(0, 8) : '—'}
)}
{/* Confirmation Modal */} {modalActionType && (
{ACTION_CONFIGS[modalActionType].icon}

Confirm {ACTION_CONFIGS[modalActionType].title}

{submitError && (
{submitError}
)}

{ACTION_CONFIGS[modalActionType].description}

{ACTION_CONFIGS[modalActionType].requiredConfirmation && (
setConfirmationInput(e.target.value)} placeholder={ACTION_CONFIGS[modalActionType].requiredConfirmation} className="w-full px-3 py-2 bg-surface-container-lowest border border-outline-variant/40 rounded-lg text-sm font-mono text-on-surface focus:outline-none focus:ring-2 focus:ring-rose-500/50" />
)}
)}
); }