573 lines
26 KiB
TypeScript
573 lines
26 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { PauseCircle, PlayCircle, RefreshCw } from "lucide-react";
|
|
import {
|
|
ActionBlockerHint,
|
|
Button,
|
|
Card,
|
|
ConfirmDialog,
|
|
DataGrid,
|
|
DismissibleAlert,
|
|
DocumentationHelpLink,
|
|
LoadingFrame,
|
|
MetricCard,
|
|
PageScrollViewport,
|
|
PageTitle,
|
|
StatusBadge,
|
|
TableActionGroup,
|
|
adminErrorMessage,
|
|
hasAnyScope,
|
|
i18nMessage,
|
|
type ApiSettings,
|
|
type AuthInfo,
|
|
type DataGridColumn } from
|
|
"@govoplan/core-webui";
|
|
import {
|
|
fetchOpsStatus,
|
|
cancelRuntimeNodeDrain,
|
|
drainRuntimeNode,
|
|
runOpsChecks,
|
|
type OpsCheck,
|
|
type OpsDeploymentProfile,
|
|
type OpsGovernanceModule,
|
|
type OpsRecoveryOperation,
|
|
type OpsRuntimeNode,
|
|
type OpsSizingAssumption,
|
|
type OpsStatus
|
|
} from "../../api/ops";
|
|
import {
|
|
OPS_DOCUMENTATION,
|
|
OPS_I18N,
|
|
OPS_RECOVERY_DOCUMENTATION,
|
|
} from "./interfacePatterns";
|
|
|
|
export default function OpsPage({ settings, auth }: {settings: ApiSettings;auth: AuthInfo;}) {
|
|
const [status, setStatus] = useState<OpsStatus | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [runningProbes, setRunningProbes] = useState(false);
|
|
const [error, setError] = useState("");
|
|
const [drainTarget, setDrainTarget] = useState<OpsRuntimeNode | null>(null);
|
|
const [nodeActionId, setNodeActionId] = useState("");
|
|
|
|
async function load() {
|
|
setLoading(true);
|
|
setError("");
|
|
try {
|
|
setStatus(await fetchOpsStatus(settings));
|
|
} catch (err) {
|
|
setError(adminErrorMessage(err));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function runChecks() {
|
|
setRunningProbes(true);
|
|
setLoading(true);
|
|
setError("");
|
|
try {
|
|
setStatus(await runOpsChecks(settings));
|
|
} catch (err) {
|
|
setError(adminErrorMessage(err));
|
|
} finally {
|
|
setRunningProbes(false);
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function confirmDrain() {
|
|
if (!drainTarget) return;
|
|
setNodeActionId(drainTarget.node_id);
|
|
setError("");
|
|
try {
|
|
await drainRuntimeNode(settings, drainTarget.node_id);
|
|
setDrainTarget(null);
|
|
setStatus(await fetchOpsStatus(settings));
|
|
} catch (err) {
|
|
setError(adminErrorMessage(err));
|
|
} finally {
|
|
setNodeActionId("");
|
|
}
|
|
}
|
|
|
|
async function cancelDrain(node: OpsRuntimeNode) {
|
|
setNodeActionId(node.node_id);
|
|
setError("");
|
|
try {
|
|
await cancelRuntimeNodeDrain(settings, node.node_id);
|
|
setStatus(await fetchOpsStatus(settings));
|
|
} catch (err) {
|
|
setError(adminErrorMessage(err));
|
|
} finally {
|
|
setNodeActionId("");
|
|
}
|
|
}
|
|
|
|
useEffect(() => {void load();}, [settings.apiBaseUrl, settings.apiKey, settings.accessToken]);
|
|
|
|
const checks = status?.checks ?? [];
|
|
const warningCount = checks.filter((item) => item.state === "warning").length;
|
|
const errorCount = checks.filter((item) => item.state === "error").length;
|
|
const ready = status?.readiness.ready ?? false;
|
|
const canRunChecks = hasAnyScope(auth, ["ops:operations:run", "system:settings:write"]);
|
|
const runProbesDisabledReason = runningProbes
|
|
? OPS_I18N.runningProbes
|
|
: loading
|
|
? OPS_I18N.loading
|
|
: !canRunChecks
|
|
? OPS_I18N.runPermissionRequired
|
|
: undefined;
|
|
const queueDepths = status?.summary.worker_metrics.queue_depths ?? {};
|
|
const queuedTasks = Object.values(queueDepths).reduce((total, value) => total + value, 0);
|
|
const storageUsage = status?.summary.storage_metrics?.capacity_used_percent;
|
|
|
|
return (
|
|
<PageScrollViewport>
|
|
<div className="content-pad workspace-data-page">
|
|
<div className="page-heading split workspace-heading">
|
|
<div>
|
|
<PageTitle loading={loading}>i18n:govoplan-ops.ops.907a54c2</PageTitle>
|
|
<p>i18n:govoplan-ops.runtime_health_deployment_profile_worker_split_a.55340156</p>
|
|
</div>
|
|
<div className="button-row compact-actions">
|
|
<DocumentationHelpLink reference={OPS_DOCUMENTATION} />
|
|
<Button variant="primary" onClick={() => void runChecks()} disabled={Boolean(runProbesDisabledReason)} disabledReason={runProbesDisabledReason}>i18n:govoplan-ops.surface.run_probes</Button>
|
|
<Button onClick={() => void load()} disabled={loading} disabledReason={loading ? OPS_I18N.loading : undefined}><RefreshCw size={16} /> i18n:govoplan-ops.reload.cce71553</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
|
|
{status && !ready && (
|
|
<ActionBlockerHint
|
|
reason={{
|
|
summary: "i18n:govoplan-ops.readiness_blocked_summary",
|
|
details: i18nMessage("i18n:govoplan-ops.readiness_blocked_details", { value0: status.readiness.blockers.length }),
|
|
requiredAction: "i18n:govoplan-ops.readiness_blocked_action",
|
|
actor: "i18n:govoplan-ops.operations_operator",
|
|
target: "i18n:govoplan-ops.readiness_blocked_target"
|
|
}}
|
|
documentation={OPS_RECOVERY_DOCUMENTATION}
|
|
/>
|
|
)}
|
|
|
|
<LoadingFrame loading={loading} label="i18n:govoplan-ops.loading_operations_status.6890fe6e">
|
|
<div className="metric-grid">
|
|
<MetricCard label="i18n:govoplan-ops.profile.ff4fc027" value={status?.summary.active_profile ?? "-"} tone="info" detail={status?.summary.database_url ?? "i18n:govoplan-ops.no_database_url.51a2db0c"} />
|
|
<MetricCard label="i18n:govoplan-ops.readiness.1db9d6fb" value={ready ? "ready" : "not ready"} tone={ready ? "good" : "danger"} detail={status?.readiness.blockers.length ? `${status.readiness.blockers.length} blocker(s)` : "i18n:govoplan-ops.no_readiness_blockers.0df259bd"} />
|
|
<MetricCard label="i18n:govoplan-ops.modules.04e9462c" value={status?.summary.module_count ?? 0} tone="neutral" detail="i18n:govoplan-ops.enabled_in_the_runtime_registry.d2c6142d" />
|
|
<MetricCard label="i18n:govoplan-ops.permissions.842c35eb" value={status?.governance.summary.permission_count ?? 0} tone="neutral" detail="i18n:govoplan-ops.declared_governance_permissions.d08d3bf1" />
|
|
<MetricCard label="i18n:govoplan-ops.policies.e7800f56" value={status?.governance.summary.policy_count ?? 0} tone="neutral" detail="i18n:govoplan-ops.registered_policy_capabilities.112a2b64" />
|
|
<MetricCard label="i18n:govoplan-ops.workers.b6ef3acd" value={status?.summary.celery_enabled ? status.summary.worker_metrics.workers ?? 0 : "off"} tone={status?.summary.celery_enabled && !(status.summary.worker_metrics.missing_queues?.length) ? "good" : "warning"} detail={workerMetricDetail(status)} />
|
|
<MetricCard label="Queued tasks" value={queuedTasks} tone={queuedTasks ? "warning" : "good"} detail={Object.keys(queueDepths).length ? `${Object.keys(queueDepths).length} measured queue(s)` : "Queue depth unavailable"} />
|
|
<MetricCard label="Storage" value={storageUsage === undefined ? status?.summary.file_storage_backend ?? "-" : `${storageUsage}%`} tone={storageUsage !== undefined && storageUsage >= 90 ? "danger" : storageUsage !== undefined && storageUsage >= 75 ? "warning" : "neutral"} detail={storageMetricDetail(status)} />
|
|
<MetricCard label="Backup evidence" value={status?.summary.backup_state ?? "unknown"} tone={status?.summary.backup_state === "ok" ? "good" : "warning"} detail="Latest coordinated backup and restore-drill evidence" />
|
|
<MetricCard label="Failed operations" value={status?.summary.failed_operation_count ?? 0} tone={status?.summary.failed_operation_count ? "danger" : "good"} detail="Terminal failures or manual intervention" />
|
|
<MetricCard label="Unknown outcomes" value={status?.summary.outcome_unknown_count ?? 0} tone={status?.summary.outcome_unknown_count ? "danger" : "good"} detail={`${status?.summary.active_operation_count ?? 0} active recovery-ledger operation(s)`} />
|
|
<MetricCard label="i18n:govoplan-ops.redis.5eaa1f2f" value={status?.summary.redis_url ? "configured" : "-"} tone={status?.summary.celery_enabled ? "info" : "neutral"} detail={status?.summary.redis_url ?? "-"} />
|
|
<MetricCard label="i18n:govoplan-ops.warnings.1430f976" value={warningCount + errorCount} tone={errorCount ? "danger" : warningCount ? "warning" : "good"} detail="i18n:govoplan-ops.current_health_checks.7830bccf" />
|
|
<MetricCard label="Runtime nodes" value={status?.summary.runtime_node_count ?? 0} tone={status?.runtime_cluster.available ? "good" : "danger"} detail={runtimeNodeMetricDetail(status)} />
|
|
<MetricCard label="Database capacity" value={databaseCapacityValue(status)} tone={databaseCapacityTone(status)} detail="Peak pooled connections / available connections" />
|
|
<MetricCard label="Recovery" value={status?.summary.recovery_required_count ?? 0} tone={status?.summary.recovery_required_count ? "danger" : "good"} detail="Operations requiring recovery attention" />
|
|
<MetricCard label="Provider bindings" value={status?.governance.summary.configured_external_provider_count ?? 0} tone={status?.governance.summary.provider_attention_count ? "warning" : "good"} detail={`${status?.governance.summary.provider_attention_count ?? 0} requiring attention`} />
|
|
</div>
|
|
|
|
<div className="dashboard-grid">
|
|
<Card title="i18n:govoplan-ops.health_checks.201c869f">
|
|
<CheckList checks={checks} />
|
|
</Card>
|
|
|
|
<Card title="Runtime cluster">
|
|
<RuntimeNodeTable
|
|
nodes={status?.runtime_cluster.nodes ?? []}
|
|
canManage={canRunChecks}
|
|
busyNodeId={nodeActionId}
|
|
onDrain={setDrainTarget}
|
|
onCancelDrain={(node) => void cancelDrain(node)}
|
|
/>
|
|
</Card>
|
|
|
|
<Card title="Recovery evidence">
|
|
<RecoveryTable operations={status?.runtime_cluster.recovery.operations ?? []} />
|
|
</Card>
|
|
|
|
<Card title="i18n:govoplan-ops.governance_inventory.835d8e57">
|
|
<GovernanceTable modules={status?.governance.modules ?? []} />
|
|
</Card>
|
|
|
|
<Card title="i18n:govoplan-ops.deployment_profiles.b0caa179">
|
|
<ProfileList profiles={status?.deployment_profiles ?? []} />
|
|
</Card>
|
|
|
|
<Card title="i18n:govoplan-ops.sizing_assumptions.6ade9a90">
|
|
<SizingTable items={status?.sizing ?? []} />
|
|
</Card>
|
|
</div>
|
|
</LoadingFrame>
|
|
<ConfirmDialog
|
|
open={Boolean(drainTarget)}
|
|
title="i18n:govoplan-ops.surface.drain_node"
|
|
message={i18nMessage("i18n:govoplan-ops.drain_runtime_node_message", { value0: drainTarget?.node_id ?? "i18n:govoplan-ops.this_node" })}
|
|
confirmLabel="i18n:govoplan-ops.drain_node"
|
|
busy={Boolean(nodeActionId)}
|
|
onCancel={() => setDrainTarget(null)}
|
|
onConfirm={() => void confirmDrain()}
|
|
/>
|
|
</div>
|
|
</PageScrollViewport>);
|
|
|
|
}
|
|
|
|
function RuntimeNodeTable({
|
|
nodes,
|
|
canManage,
|
|
busyNodeId,
|
|
onDrain,
|
|
onCancelDrain
|
|
}: {
|
|
nodes: OpsRuntimeNode[];
|
|
canManage: boolean;
|
|
busyNodeId: string;
|
|
onDrain: (node: OpsRuntimeNode) => void;
|
|
onCancelDrain: (node: OpsRuntimeNode) => void;
|
|
}) {
|
|
const columns: DataGridColumn<OpsRuntimeNode>[] = [
|
|
{
|
|
id: "node",
|
|
header: "Node",
|
|
width: "minmax(220px, 1fr)",
|
|
minWidth: 200,
|
|
resizable: true,
|
|
sortable: true,
|
|
filterable: true,
|
|
value: (node) => `${node.node_id} ${node.role}`,
|
|
render: (node) => <div><strong>{node.node_id}</strong><span className="muted block">{node.role} · {node.software_version}</span></div>
|
|
},
|
|
{
|
|
id: "state",
|
|
header: "State",
|
|
width: 150,
|
|
sortable: true,
|
|
filterable: true,
|
|
value: (node) => node.stale ? "stale" : node.state,
|
|
render: (node) => <StatusBadge status={node.stale ? "error" : stateTone(node.state)} label={node.stale ? "stale" : node.state} />
|
|
},
|
|
{
|
|
id: "heartbeat",
|
|
header: "Last heartbeat",
|
|
width: "minmax(210px, .8fr)",
|
|
minWidth: 190,
|
|
resizable: true,
|
|
sortable: true,
|
|
value: (node) => node.last_heartbeat_at,
|
|
render: (node) => new Date(node.last_heartbeat_at).toLocaleString()
|
|
},
|
|
{
|
|
id: "queues",
|
|
header: "Queues",
|
|
width: "minmax(180px, .7fr)",
|
|
minWidth: 160,
|
|
resizable: true,
|
|
filterable: true,
|
|
value: (node) => node.queues.join(" "),
|
|
render: (node) => node.queues.join(", ") || "-"
|
|
},
|
|
{
|
|
id: "actions",
|
|
header: "",
|
|
width: 56,
|
|
value: (node) => node.state,
|
|
render: (node) => {
|
|
const busy = busyNodeId === node.node_id;
|
|
const cancelling = node.state === "draining";
|
|
const disabledReason = !canManage
|
|
? OPS_I18N.runPermissionRequired
|
|
: busy
|
|
? OPS_I18N.nodeActionActive
|
|
: node.state === "stopped"
|
|
? OPS_I18N.stoppedNode
|
|
: !cancelling && node.stale
|
|
? OPS_I18N.staleNode
|
|
: undefined;
|
|
return (
|
|
<TableActionGroup
|
|
minimumSlots={1}
|
|
label={i18nMessage("i18n:govoplan-ops.lifecycle_actions_for_value", { value0: node.node_id })}
|
|
actions={[{
|
|
id: cancelling ? "cancel-drain" : "drain",
|
|
label: cancelling
|
|
? i18nMessage("i18n:govoplan-ops.cancel_drain_for_value", { value0: node.node_id })
|
|
: i18nMessage("i18n:govoplan-ops.drain_value", { value0: node.node_id }),
|
|
icon: cancelling ? <PlayCircle size={16} /> : <PauseCircle size={16} />,
|
|
disabled: Boolean(disabledReason),
|
|
disabledReason,
|
|
onClick: () => cancelling ? onCancelDrain(node) : onDrain(node)
|
|
}]}
|
|
/>
|
|
);
|
|
}
|
|
}
|
|
];
|
|
return <DataGrid id="ops-runtime-nodes" rows={nodes} columns={columns} getRowKey={(node) => `${node.node_id}:${node.incarnation}`} emptyText="No runtime nodes reported." />;
|
|
}
|
|
|
|
function RecoveryTable({ operations }: { operations: OpsRecoveryOperation[] }) {
|
|
const columns: DataGridColumn<OpsRecoveryOperation>[] = [
|
|
{
|
|
id: "operation",
|
|
header: "Operation",
|
|
width: "minmax(240px, 1fr)",
|
|
minWidth: 220,
|
|
resizable: true,
|
|
sortable: true,
|
|
filterable: true,
|
|
value: (operation) => `${operation.module_id} ${operation.operation_type}`,
|
|
render: (operation) => <div><strong>{operation.operation_type}</strong><span className="muted block">{operation.module_id} · {operation.mode}</span></div>
|
|
},
|
|
{
|
|
id: "resource",
|
|
header: "Resource",
|
|
width: "minmax(180px, .8fr)",
|
|
minWidth: 160,
|
|
resizable: true,
|
|
filterable: true,
|
|
value: (operation) => `${operation.resource_type ?? ""} ${operation.resource_id ?? ""}`,
|
|
render: (operation) => operation.resource_type ? `${operation.resource_type}: ${operation.resource_id ?? "-"}` : "-"
|
|
},
|
|
{
|
|
id: "status",
|
|
header: "Status",
|
|
width: 170,
|
|
sortable: true,
|
|
filterable: true,
|
|
value: (operation) => operation.status,
|
|
render: (operation) => <StatusBadge status={recoveryTone(operation.status)} label={operation.status.replaceAll("_", " ")} />
|
|
},
|
|
{
|
|
id: "evidence",
|
|
header: "Evidence",
|
|
width: 150,
|
|
sortable: true,
|
|
value: (operation) => operation.checkpoint_count,
|
|
render: (operation) => `${operation.checkpoint_count} checkpoint(s)`
|
|
},
|
|
{
|
|
id: "updated",
|
|
header: "Updated",
|
|
width: "minmax(210px, .8fr)",
|
|
minWidth: 190,
|
|
resizable: true,
|
|
sortable: true,
|
|
value: (operation) => operation.updated_at,
|
|
render: (operation) => new Date(operation.updated_at).toLocaleString()
|
|
}
|
|
];
|
|
return <DataGrid id="ops-recovery-operations" rows={operations} columns={columns} getRowKey={(operation) => operation.id} emptyText="No recovery operations recorded." />;
|
|
}
|
|
|
|
function GovernanceTable({ modules }: { modules: OpsGovernanceModule[] }) {
|
|
const columns: DataGridColumn<OpsGovernanceModule>[] = [
|
|
{
|
|
id: "module",
|
|
header: "i18n:govoplan-ops.module.b8ff0289",
|
|
width: "minmax(200px, 1fr)",
|
|
minWidth: 180,
|
|
resizable: true,
|
|
sortable: true,
|
|
filterable: true,
|
|
value: (module) => `${module.name} ${module.module_id} ${module.version}`,
|
|
render: (module) => <div><strong>{module.name}</strong><span className="muted block">{module.module_id} · {module.version}</span></div>
|
|
},
|
|
{
|
|
id: "authority",
|
|
header: "i18n:govoplan-ops.authority.8802e425",
|
|
width: "minmax(190px, .8fr)",
|
|
minWidth: 170,
|
|
resizable: true,
|
|
value: (module) => `${module.permission_count} ${module.role_template_count}`,
|
|
render: (module) => `${module.permission_count} permissions · ${module.role_template_count} roles`
|
|
},
|
|
{
|
|
id: "contracts",
|
|
header: "i18n:govoplan-ops.contracts.57d80902",
|
|
width: "minmax(190px, .8fr)",
|
|
minWidth: 170,
|
|
resizable: true,
|
|
value: (module) => `${module.capability_count} ${module.policy_count}`,
|
|
render: (module) => `${module.capability_count} capabilities · ${module.policy_count} policies`
|
|
},
|
|
{
|
|
id: "architecture",
|
|
header: "Architecture",
|
|
width: "minmax(210px, .9fr)",
|
|
minWidth: 190,
|
|
resizable: true,
|
|
sortable: true,
|
|
filterable: true,
|
|
value: (module) => module.architecture
|
|
? `${module.architecture.layer} ${module.architecture.kind} ${module.architecture.maturity}`
|
|
: "undeclared",
|
|
render: (module) => module.architecture ? (
|
|
<div>
|
|
<strong>{module.architecture.maturity}</strong>
|
|
<span className="muted block">{module.architecture.layer} · {module.architecture.kind}</span>
|
|
{module.architecture.known_limits.length ? <span className="muted block">{module.architecture.known_limits.length} known limit(s)</span> : null}
|
|
</div>
|
|
) : <span className="muted">staged declaration pending</span>
|
|
},
|
|
{
|
|
id: "providers",
|
|
header: "External providers",
|
|
width: "minmax(230px, 1fr)",
|
|
minWidth: 210,
|
|
resizable: true,
|
|
filterable: true,
|
|
value: (module) => `${module.external_provider_count} ${module.external_providers.map((provider) => `${provider.id} ${provider.maturity} ${provider.authority_modes.join(" ")} ${provider.runtime_state?.health ?? "unobserved"} ${provider.runtime_state?.freshness ?? ""}`).join(" ")}`,
|
|
render: (module) => module.external_provider_count ? (
|
|
<div>
|
|
<strong>{module.external_provider_count} declared</strong>
|
|
{module.external_providers.map((provider) => (
|
|
<span className="muted block" key={provider.id}>
|
|
{provider.label}: {provider.maturity} · {provider.runtime_state ? `${provider.runtime_state.health}/${provider.runtime_state.freshness} · ${provider.runtime_state.bindings?.length ?? 0} binding(s)` : "unobserved"}
|
|
</span>
|
|
))}
|
|
</div>
|
|
) : <span className="muted">none declared</span>
|
|
},
|
|
{
|
|
id: "controls",
|
|
header: "i18n:govoplan-ops.controls.0cdb80fb",
|
|
width: "minmax(190px, .8fr)",
|
|
minWidth: 170,
|
|
resizable: true,
|
|
value: (module) => `${module.access_control_count} ${module.search_provider_count}`,
|
|
render: (module) => `${module.access_control_count} access · ${module.search_provider_count} search`
|
|
},
|
|
{
|
|
id: "evidence",
|
|
header: "i18n:govoplan-ops.evidence.7ea014de",
|
|
width: "minmax(190px, .8fr)",
|
|
minWidth: 170,
|
|
resizable: true,
|
|
value: (module) => `${module.documentation_count} ${module.documentation_provider_count} ${module.migration_managed}`,
|
|
render: (module) => (
|
|
<div>
|
|
{module.documentation_count + module.documentation_provider_count} docs
|
|
<span className="muted block">{module.migration_managed ? "migration managed" : "no module migrations"}</span>
|
|
</div>
|
|
)
|
|
}
|
|
];
|
|
return (
|
|
<DataGrid
|
|
id="ops-governance-inventory"
|
|
rows={modules}
|
|
columns={columns}
|
|
getRowKey={(module) => module.module_id}
|
|
emptyText="i18n:govoplan-ops.no_modules_reported.847f06d9"
|
|
/>
|
|
);
|
|
}
|
|
|
|
function CheckList({ checks }: {checks: OpsCheck[];}) {
|
|
if (!checks.length) return <p className="muted">i18n:govoplan-ops.no_health_checks_reported.03c067c4</p>;
|
|
return (
|
|
<dl className="detail-list">
|
|
{checks.map((check) =>
|
|
<div key={check.id}>
|
|
<dt><StatusBadge status={stateTone(check.state)} label={check.state} /></dt>
|
|
<dd><strong>{check.label}</strong><span className="muted"> · {check.detail}</span></dd>
|
|
</div>
|
|
)}
|
|
</dl>);
|
|
|
|
}
|
|
|
|
function ProfileList({ profiles }: {profiles: OpsDeploymentProfile[];}) {
|
|
if (!profiles.length) return <p className="muted">i18n:govoplan-ops.no_deployment_profiles_reported.7c3af1db</p>;
|
|
const columns: DataGridColumn<OpsDeploymentProfile>[] = [
|
|
{ id: "profile", header: "i18n:govoplan-ops.profile.ff4fc027", width: "minmax(220px, 1fr)", minWidth: 180, resizable: true, sortable: true, filterable: true, value: (profile) => `${profile.name} ${profile.id}`, render: (profile) => <div><strong>{profile.name}</strong><span className="muted block">{profile.id}</span></div> },
|
|
{ id: "status", header: "i18n:govoplan-ops.status.bae7d5be", width: 140, sortable: true, filterable: true, value: (profile) => profile.current ? "current" : "reference", render: (profile) => <StatusBadge status={profile.current ? "success" : "inactive"} label={profile.current ? "current" : "reference"} /> },
|
|
{ id: "components", header: "i18n:govoplan-ops.components.9289473e", width: "minmax(220px, 1fr)", minWidth: 180, resizable: true, filterable: true, value: (profile) => profile.components.join(" "), render: (profile) => profile.components.join(", ") },
|
|
{ id: "fit", header: "i18n:govoplan-ops.fit.dab564d8", width: "minmax(180px, .8fr)", minWidth: 160, resizable: true, filterable: true, value: (profile) => profile.fit }
|
|
];
|
|
return (
|
|
<DataGrid id="ops-deployment-profiles" rows={profiles} columns={columns} getRowKey={(profile) => profile.id} />);
|
|
|
|
}
|
|
|
|
function SizingTable({ items }: {items: OpsSizingAssumption[];}) {
|
|
if (!items.length) return <p className="muted">i18n:govoplan-ops.no_sizing_assumptions_reported.17515959</p>;
|
|
const columns: DataGridColumn<OpsSizingAssumption>[] = [
|
|
{ id: "area", header: "i18n:govoplan-ops.area.2745deba", width: "minmax(180px, .7fr)", minWidth: 160, resizable: true, sortable: true, filterable: true, value: (item) => item.area, render: (item) => <strong>{item.area}</strong> },
|
|
{ id: "baseline", header: "i18n:govoplan-ops.baseline.e6ab7982", width: "minmax(220px, 1fr)", minWidth: 180, resizable: true, filterable: true, value: (item) => item.baseline },
|
|
{ id: "trigger", header: "i18n:govoplan-ops.scale_trigger.1c85e10e", width: "minmax(220px, 1fr)", minWidth: 180, resizable: true, filterable: true, value: (item) => item.scale_trigger },
|
|
{ id: "note", header: "i18n:govoplan-ops.operator_note.1dc58f7b", width: "minmax(240px, 1.2fr)", minWidth: 200, resizable: true, filterable: true, value: (item) => item.operator_note }
|
|
];
|
|
return (
|
|
<DataGrid id="ops-sizing-assumptions" rows={items} columns={columns} getRowKey={(item) => item.area} />);
|
|
|
|
}
|
|
|
|
function stateTone(state: string): string {
|
|
if (state === "ok") return "success";
|
|
if (state === "warning") return "warning";
|
|
if (state === "error") return "error";
|
|
return "inactive";
|
|
}
|
|
|
|
function recoveryTone(state: string): string {
|
|
if (["succeeded", "recovered"].includes(state)) return "success";
|
|
if (state === "rejected") return "warning";
|
|
if (["failed", "recovery_required"].includes(state)) return "error";
|
|
if (["running", "recovering", "prepared"].includes(state)) return "warning";
|
|
return "inactive";
|
|
}
|
|
|
|
function runtimeNodeMetricDetail(status: OpsStatus | null): string {
|
|
const cluster = status?.runtime_cluster;
|
|
if (!cluster?.available) return cluster?.detail ?? "Runtime directory unavailable";
|
|
const active = cluster.active ?? { api: 0, worker: 0 };
|
|
const expected = cluster.expected ?? { api: 0, worker: 0 };
|
|
return `${active.api}/${expected.api} API · ${active.worker}/${expected.worker} workers`;
|
|
}
|
|
|
|
function databaseCapacityValue(status: OpsStatus | null): string {
|
|
const peak = status?.summary.database_connection_peak;
|
|
const available = status?.summary.database_connection_available;
|
|
return typeof peak === "number" && typeof available === "number"
|
|
? `${peak} / ${available}`
|
|
: "not declared";
|
|
}
|
|
|
|
function databaseCapacityTone(status: OpsStatus | null): "good" | "warning" | "danger" {
|
|
const peak = status?.summary.database_connection_peak;
|
|
const available = status?.summary.database_connection_available;
|
|
if (typeof peak !== "number" || typeof available !== "number") {
|
|
return status?.runtime_cluster.state_profile === "shared" ? "danger" : "warning";
|
|
}
|
|
if (peak > available) return "danger";
|
|
return peak / available >= 0.8 ? "warning" : "good";
|
|
}
|
|
|
|
function workerMetricDetail(status: OpsStatus | null): string {
|
|
if (!status?.summary.celery_enabled) return "i18n:govoplan-ops.celery_worker_setting.323d7737";
|
|
const metrics = status.summary.worker_metrics;
|
|
if (metrics.missing_queues?.length) return `Missing queues: ${metrics.missing_queues.join(", ")}`;
|
|
const queued = Object.values(metrics.queue_depths ?? {}).reduce((total, value) => total + value, 0);
|
|
return `${metrics.active_tasks ?? 0} active · ${queued} queued`;
|
|
}
|
|
|
|
function storageMetricDetail(status: OpsStatus | null): string {
|
|
const metrics = status?.summary.storage_metrics;
|
|
if (!metrics?.capacity_observable) {
|
|
return `${status?.summary.file_storage_backend ?? "Storage"} capacity is provider-managed or unavailable`;
|
|
}
|
|
return `${formatBytes(metrics.capacity_used_bytes ?? 0)} used of ${formatBytes(metrics.capacity_total_bytes ?? 0)}`;
|
|
}
|
|
|
|
function formatBytes(value: number): string {
|
|
if (!Number.isFinite(value) || value <= 0) return "0 B";
|
|
const units = ["B", "KB", "MB", "GB", "TB", "PB"];
|
|
const index = Math.min(Math.floor(Math.log(value) / Math.log(1024)), units.length - 1);
|
|
return `${(value / 1024 ** index).toFixed(index ? 1 : 0)} ${units[index]}`;
|
|
}
|