feat(data): add read-only artifact health monitor

This commit is contained in:
DCCONSTRUCTIONS
2026-07-29 01:54:41 +03:00
parent 79eb4b46f7
commit 37b8930527
10 changed files with 1110 additions and 3 deletions
@@ -0,0 +1,73 @@
export interface ArtifactScope {
scope_id: string;
file_count: number;
logical_bytes: number;
modified_at_utc: string | null;
}
export interface ArtifactHealth {
schema_version: "missioncore.artifact-health/v1";
generated_at_utc: string;
overall_state: "live" | "attention" | "unavailable";
inventory: {
state: "live" | "degraded" | "unavailable";
scanned_at_utc: string;
scan_duration_ms: number;
file_count: number;
logical_bytes: number;
unreadable_entries: number;
files_delta_since_previous: number | null;
bytes_delta_since_previous: number | null;
scopes: ArtifactScope[];
};
exact_audit: {
state: "exact-current" | "exact-stale" | "unavailable";
result_id: string | null;
audited_at_utc: string | null;
root_count: number;
file_count: number;
logical_bytes: number;
unique_content_bytes: number;
duplicate_bytes: number;
duplicate_fraction: number;
amplification_ratio: number;
duplicate_content_groups: number;
changed_roots: string[];
limitations: string[];
};
content_references: {
state: "not-indexed";
storage_model: "materialized-copies";
storage_migration_authorized: false;
next_gate: string;
};
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
export async function fetchArtifactHealth(
signal?: AbortSignal,
): Promise<ArtifactHealth> {
const response = await fetch("/api/v1/data/artifact-health", {
method: "GET",
headers: { Accept: "application/json" },
signal,
});
if (!response.ok) {
throw new Error(`API здоровья данных вернул HTTP ${response.status}.`);
}
const document: unknown = await response.json();
if (
!isRecord(document)
|| document.schema_version !== "missioncore.artifact-health/v1"
|| !isRecord(document.inventory)
|| !Array.isArray(document.inventory.scopes)
|| !isRecord(document.exact_audit)
|| !isRecord(document.content_references)
) {
throw new Error("Ответ здоровья данных не соответствует контракту.");
}
return document as unknown as ArtifactHealth;
}
+12
View File
@@ -21,6 +21,7 @@ export type WorkspaceKind =
| "compute-modules"
| "network-monitor"
| "datasets"
| "artifact-health"
| "lab-archive";
export type CapabilityStatus = "active" | "ready" | "contract" | "later";
@@ -495,6 +496,17 @@ export const workspaces: WorkspaceDefinition[] = [
kind: "datasets",
groups: [],
},
{
id: "artifact-health",
root: "data",
label: "Здоровье данных",
title: "Здоровье данных",
eyebrow: "ДАННЫЕ / АРТЕФАКТЫ",
description: "Живой объём runtime, рост и точный статус дублирования контента.",
icon: "database",
kind: "artifact-health",
groups: [],
},
{
id: "streams",
root: "data",
+1
View File
@@ -12,3 +12,4 @@
@import "./styles/observation.css";
@import "./styles/environment-settings.css";
@import "./styles/system-telemetry.css";
@import "./styles/artifact-health.css";
@@ -0,0 +1,237 @@
.artifact-health-workspace {
display: grid;
min-width: 0;
gap: 1rem;
padding-bottom: 1rem;
}
.artifact-health-lead,
.artifact-scope-panel > header,
.artifact-health-details header {
display: flex;
min-width: 0;
align-items: flex-start;
justify-content: space-between;
gap: 1.2rem;
}
.artifact-health-lead {
padding: 0.25rem 0 0.35rem;
}
.artifact-health-lead h2,
.artifact-health-lead p,
.artifact-scope-panel h3,
.artifact-scope-panel p,
.artifact-health-details h3,
.artifact-health-details p,
.artifact-health-state h3,
.artifact-health-state p {
margin: 0;
}
.artifact-health-lead h2 {
margin-top: 0.4rem;
color: var(--nodedc-text-primary);
font-size: 1.42rem;
letter-spacing: -0.035em;
}
.artifact-health-lead p,
.artifact-scope-panel p,
.artifact-health-details p,
.artifact-health-state p {
color: var(--nodedc-text-muted);
font-size: 0.66rem;
line-height: 1.5;
}
.artifact-health-lead p {
max-width: 48rem;
margin-top: 0.42rem;
}
.artifact-health-state {
display: grid;
justify-items: start;
gap: 0.65rem;
}
.artifact-health-state h3 {
color: var(--nodedc-text-primary);
font-size: 1rem;
}
.artifact-health-notice {
display: flex;
align-items: center;
gap: 0.7rem;
color: var(--nodedc-text-secondary);
font-size: 0.64rem;
}
.artifact-health-metrics {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.65rem;
}
.artifact-health-metrics > div {
display: grid;
min-width: 0;
gap: 0.28rem;
}
.artifact-health-metrics span,
.artifact-health-metrics small,
.artifact-scope-list span,
.artifact-health-details dt {
color: var(--nodedc-text-muted);
font-size: 0.6rem;
}
.artifact-health-metrics strong {
color: var(--nodedc-text-primary);
font-size: 1.18rem;
font-weight: 720;
letter-spacing: -0.035em;
}
.artifact-scope-panel,
.artifact-health-details > div {
display: grid;
min-width: 0;
gap: 1rem;
}
.artifact-scope-panel h3,
.artifact-health-details h3 {
margin-top: 0.36rem;
color: var(--nodedc-text-primary);
font-size: 1rem;
letter-spacing: -0.025em;
}
.artifact-scope-panel p {
margin-top: 0.36rem;
}
.artifact-scope-list {
display: grid;
gap: 0.22rem;
margin: 0;
padding: 0;
list-style: none;
}
.artifact-scope-list li {
display: grid;
grid-template-columns: minmax(10rem, 0.7fr) minmax(12rem, 1.4fr) 6rem;
align-items: center;
gap: 0.8rem;
min-width: 0;
padding: 0.58rem 0.66rem;
border-radius: var(--nodedc-radius-control);
background: var(--station-panel-soft);
}
.artifact-scope-list li > div:first-child {
display: grid;
min-width: 0;
gap: 0.16rem;
}
.artifact-scope-list strong {
overflow: hidden;
color: var(--nodedc-text-primary);
font-size: 0.66rem;
font-weight: 680;
text-overflow: ellipsis;
white-space: nowrap;
}
.artifact-scope-list li > strong {
text-align: right;
}
.artifact-scope-list__measure {
overflow: hidden;
height: 0.34rem;
border-radius: 999px;
background: var(--station-panel-deep);
}
.artifact-scope-list__measure span {
display: block;
min-width: 0.2rem;
height: 100%;
border-radius: inherit;
background: rgb(var(--nodedc-accent-rgb) / 0.78);
}
.artifact-health-details {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.65rem;
}
.artifact-health-details dl {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.45rem;
margin: 0;
}
.artifact-health-details dl > div {
display: grid;
min-width: 0;
gap: 0.18rem;
padding: 0.66rem;
border-radius: var(--nodedc-radius-control);
background: var(--station-panel-soft);
}
.artifact-health-details dd {
overflow: hidden;
margin: 0;
color: var(--nodedc-text-primary);
font-size: 0.66rem;
font-weight: 680;
text-overflow: ellipsis;
white-space: nowrap;
}
@media (max-width: 980px) {
.artifact-health-metrics {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.artifact-health-details {
grid-template-columns: 1fr;
}
}
@media (max-width: 680px) {
.artifact-health-lead,
.artifact-scope-panel > header,
.artifact-health-details header {
display: grid;
}
.artifact-health-metrics {
grid-template-columns: 1fr;
}
.artifact-scope-list li {
grid-template-columns: minmax(0, 1fr) 5.2rem;
}
.artifact-scope-list__measure {
grid-column: 1 / -1;
grid-row: 2;
}
.artifact-health-details dl {
grid-template-columns: 1fr;
}
}
@@ -11,7 +11,6 @@ import {
Icon,
StatusBadge,
} from "@nodedc/ui-react";
import {
ObservationMedia,
ObservationSourcePicker,
@@ -44,11 +43,11 @@ import { finiteMetric, formatNumber, pipelineLatency, sourceModeLabel } from "..
import type { SceneSettings } from "../sceneSettings";
import type { WorkspaceRendererProps } from "./contracts";
import { DatasetGatewayWorkspace } from "./DatasetGatewayWorkspace";
import { ArtifactHealthWorkspace } from "./data/ArtifactHealthWorkspace";
import { ContourHealthWorkspace } from "./ContourHealthWorkspace";
import { LaboratoryArchiveWorkspace } from "./laboratory/LaboratoryArchiveWorkspace";
import { ComputeModulesWorkspace } from "./system/ComputeModulesWorkspace";
import { NetworkWorkspace } from "./system/NetworkWorkspace";
function statusTone(status: CapabilityStatus): "success" | "accent" | "warning" | "neutral" {
if (status === "active") return "success";
if (status === "ready") return "accent";
@@ -1156,7 +1155,6 @@ function RecordingsWorkspace(props: WorkspaceRendererProps) {
);
}
export function WorkspaceRenderer(props: WorkspaceRendererProps) {
switch (props.definition.kind) {
case "spatial":
@@ -1185,6 +1183,8 @@ export function WorkspaceRenderer(props: WorkspaceRendererProps) {
return <NetworkWorkspace />;
case "datasets":
return <DatasetGatewayWorkspace />;
case "artifact-health":
return <ArtifactHealthWorkspace />;
case "lab-archive":
return (
<LaboratoryArchiveWorkspace
@@ -0,0 +1,264 @@
import { useEffect, useState } from "react";
import {
Button,
GlassSurface,
StatusBadge,
} from "@nodedc/ui-react";
import {
fetchArtifactHealth,
type ArtifactHealth,
} from "../../core/data/artifactHealth";
const POLL_MILLISECONDS = 30_000;
function formatBytes(value: number | null): string {
if (value === null || !Number.isFinite(value)) return "—";
const absolute = Math.abs(value);
const units = ["B", "KB", "MB", "GB", "TB"];
let scaled = absolute;
let unit = units[0];
for (const candidate of units) {
unit = candidate;
if (scaled < 1_000 || candidate === units.at(-1)) break;
scaled /= 1_000;
}
const formatted = new Intl.NumberFormat("ru-RU", {
maximumFractionDigits: scaled < 10 ? 2 : 1,
}).format(scaled);
return `${value < 0 ? "" : ""}${formatted} ${unit}`;
}
function formatInteger(value: number): string {
return new Intl.NumberFormat("ru-RU").format(value);
}
function formatPercent(value: number): string {
return new Intl.NumberFormat("ru-RU", {
maximumFractionDigits: 1,
style: "percent",
}).format(value);
}
function formatTimestamp(value: string | null): string {
if (!value) return "—";
const timestamp = new Date(value);
if (!Number.isFinite(timestamp.valueOf())) return "—";
return new Intl.DateTimeFormat("ru-RU", {
dateStyle: "short",
timeStyle: "medium",
}).format(timestamp);
}
function deltaLabel(value: number | null, unit: "files" | "bytes"): string {
if (value === null) return "Базовый замер";
if (value === 0) return "Без изменений";
const prefix = value > 0 ? "+" : "";
const absolute = Math.abs(value);
return unit === "files"
? `${prefix}${formatInteger(absolute)} файлов`
: `${prefix}${formatBytes(absolute)}`;
}
function errorLabel(error: unknown): string {
return error instanceof Error && error.message.trim()
? error.message
: "Срез здоровья данных недоступен.";
}
export function ArtifactHealthWorkspace() {
const [health, setHealth] = useState<ArtifactHealth | null>(null);
const [error, setError] = useState<string | null>(null);
const [generation, setGeneration] = useState(0);
useEffect(() => {
const controller = new AbortController();
let timer: number | null = null;
const poll = () => {
void fetchArtifactHealth(controller.signal)
.then((document) => {
if (controller.signal.aborted) return;
setHealth(document);
setError(null);
})
.catch((loadError: unknown) => {
if (!controller.signal.aborted) setError(errorLabel(loadError));
})
.finally(() => {
if (!controller.signal.aborted) {
timer = window.setTimeout(poll, POLL_MILLISECONDS);
}
});
};
poll();
return () => {
controller.abort();
if (timer !== null) window.clearTimeout(timer);
};
}, [generation]);
if (!health) {
return (
<div className="artifact-health-workspace">
<section className="artifact-health-lead">
<div>
<span className="section-eyebrow">ДАННЫЕ / ЗДОРОВЬЕ АРТЕФАКТОВ</span>
<h2>Живой контур хранения</h2>
<p>
Метаданные локальных артефактов и последний точный content-аудит
без изменения или автоматического удаления данных.
</p>
</div>
</section>
<GlassSurface className="artifact-health-state" padding="lg">
<StatusBadge tone={error ? "danger" : "accent"}>
{error ? "Срез недоступен" : "Сканируем метаданные"}
</StatusBadge>
<h3>{error ?? "Собираем файловый инвентарь"}</h3>
<p>Содержимое больших файлов не читается и повторно не хешируется.</p>
{error ? (
<Button
size="compact"
variant="secondary"
onClick={() => setGeneration((value) => value + 1)}
>
Повторить
</Button>
) : null}
</GlassSurface>
</div>
);
}
const largestScope = Math.max(
...health.inventory.scopes.map((scope) => scope.logical_bytes),
1,
);
const exactCurrent = health.exact_audit.state === "exact-current";
return (
<div className="artifact-health-workspace">
<section className="artifact-health-lead">
<div>
<span className="section-eyebrow">ДАННЫЕ / ЗДОРОВЬЕ АРТЕФАКТОВ</span>
<h2>Живой контур хранения</h2>
<p>
Текущий объём runtime, рост между замерами и доказанное дублирование.
Контур остаётся только наблюдающим.
</p>
</div>
<StatusBadge tone={health.overall_state === "live" ? "success" : "warning"}>
{health.overall_state === "live" ? "Стабильно" : "Требует оптимизации"}
</StatusBadge>
</section>
{error ? (
<GlassSurface className="artifact-health-notice" padding="md" tone="soft">
<StatusBadge tone="warning">{error}</StatusBadge>
<span>Показан последний успешный срез.</span>
</GlassSurface>
) : null}
<section className="artifact-health-metrics" aria-label="Сводка хранения">
<GlassSurface padding="md">
<span>Runtime всего</span>
<strong>{formatBytes(health.inventory.logical_bytes)}</strong>
<small>{deltaLabel(health.inventory.bytes_delta_since_previous, "bytes")}</small>
</GlassSurface>
<GlassSurface padding="md">
<span>Файлов</span>
<strong>{formatInteger(health.inventory.file_count)}</strong>
<small>{deltaLabel(health.inventory.files_delta_since_previous, "files")}</small>
</GlassSurface>
<GlassSurface padding="md">
<span>Доказанные дубли</span>
<strong>{formatBytes(health.exact_audit.duplicate_bytes)}</strong>
<small>{formatPercent(health.exact_audit.duplicate_fraction)} audit-выборки</small>
</GlassSurface>
<GlassSurface padding="md">
<span>Content-групп</span>
<strong>{formatInteger(health.exact_audit.duplicate_content_groups)}</strong>
<small>Коэффициент {health.exact_audit.amplification_ratio.toFixed(2)}×</small>
</GlassSurface>
</section>
<GlassSurface className="artifact-scope-panel" padding="lg">
<header>
<div>
<span className="section-eyebrow">LIVE INVENTORY</span>
<h3>Распределение runtime</h3>
<p>
Метаданные обновлены {formatTimestamp(health.inventory.scanned_at_utc)}
{" · "}{Math.round(health.inventory.scan_duration_ms)} мс
</p>
</div>
<StatusBadge tone={health.inventory.state === "live" ? "success" : "warning"}>
{health.inventory.state === "live" ? "Live" : "Частичный срез"}
</StatusBadge>
</header>
<ol className="artifact-scope-list">
{health.inventory.scopes.slice(0, 8).map((scope) => (
<li key={scope.scope_id}>
<div>
<strong>{scope.scope_id}</strong>
<span>{formatInteger(scope.file_count)} файлов</span>
</div>
<div className="artifact-scope-list__measure">
<span
style={{ width: `${scope.logical_bytes / largestScope * 100}%` }}
aria-hidden="true"
/>
</div>
<strong>{formatBytes(scope.logical_bytes)}</strong>
</li>
))}
</ol>
</GlassSurface>
<section className="artifact-health-details">
<GlassSurface padding="lg">
<header>
<div>
<span className="section-eyebrow">E44 / EXACT SHA-256 AUDIT</span>
<h3>Доказанное дублирование</h3>
</div>
<StatusBadge tone={exactCurrent ? "success" : "warning"}>
{exactCurrent ? "Точный срез актуален" : "Нужен повторный аудит"}
</StatusBadge>
</header>
<dl>
<div><dt>Охвачено корней</dt><dd>{health.exact_audit.root_count}</dd></div>
<div><dt>Логический объём</dt><dd>{formatBytes(health.exact_audit.logical_bytes)}</dd></div>
<div><dt>Уникальный content</dt><dd>{formatBytes(health.exact_audit.unique_content_bytes)}</dd></div>
<div><dt>Дата аудита</dt><dd>{formatTimestamp(health.exact_audit.audited_at_utc)}</dd></div>
</dl>
{health.exact_audit.changed_roots.length ? (
<p>Изменены корни: {health.exact_audit.changed_roots.join(", ")}.</p>
) : (
<p>Все 14 измеренных корней совпадают с точным аудитом по числу файлов и байтам.</p>
)}
</GlassSurface>
<GlassSurface padding="lg">
<header>
<div>
<span className="section-eyebrow">CONTENT REFERENCES</span>
<h3>Материализованные копии</h3>
</div>
<StatusBadge tone="warning">Не индексировано</StatusBadge>
</header>
<p>
Content-addressed ссылки ещё не включены. Миграция хранения не
авторизована: сначала выбираются классы дублей с максимальным
измеренным эффектом.
</p>
<dl>
<div><dt>Режим</dt><dd>Наблюдение</dd></div>
<div><dt>Удаление</dt><dd>Запрещено</dd></div>
<div><dt>Автомиграция</dt><dd>Запрещена</dd></div>
<div><dt>Следующий gate</dt><dd>Content refs / chunking</dd></div>
</dl>
</GlassSurface>
</section>
</div>
);
}
@@ -0,0 +1,41 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { test } from "node:test";
const sourceRoot = new URL("../src/", import.meta.url);
async function read(relativePath) {
return readFile(new URL(relativePath, sourceRoot), "utf8");
}
test("artifact health remains a bounded read-only data feature", async () => {
const [
productModel,
workspaceHub,
core,
workspace,
featureStyles,
styles,
] = await Promise.all([
read("productModel.ts"),
read("workspaces/Workspaces.tsx"),
read("core/data/artifactHealth.ts"),
read("workspaces/data/ArtifactHealthWorkspace.tsx"),
read("styles/artifact-health.css"),
read("styles.css"),
]);
assert.match(productModel, /kind: "artifact-health"/);
assert.match(workspaceHub, /<ArtifactHealthWorkspace \/>/);
assert.match(core, /\/api\/v1\/data\/artifact-health/);
assert.doesNotMatch(core, /@nodedc\/ui-react/);
assert.match(workspace, /fetchArtifactHealth/);
assert.match(workspace, /POLL_MILLISECONDS\s*=\s*30_000/);
assert.doesNotMatch(workspace, /delete|deduplicat|migration.*fetch/i);
assert.doesNotMatch(
featureStyles,
/\bborder:\s*1px\s+solid/,
"Artifact health must not add decorative perimeter outlines",
);
assert.match(styles, /artifact-health\.css/);
});