sdlc-hub/web/src/shell/ModuleStatusBlock.tsx
2026-10-24 15:30:12 +04:00

78 lines
4.4 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 { useProjects } from "../api/queries.ts";
import type { Project } from "../api/types.ts";
import { MODULES } from "../modules/index.ts";
import type { HubModule } from "../modules/types.ts";
import { fmtStamp } from "../ui/format.ts";
import { IconButton, IconRefresh } from "../ui/index.ts";
import { useModuleActivity } from "./activity/activity.ts";
import { agentText } from "./activity/agentText.ts";
import { eventText } from "./activity/events.ts";
type WithStatus = HubModule & Required<Pick<HubModule, "status">>;
const WITH_STATUS = MODULES.filter((m): m is WithStatus => !!m.status);
// строка модуля: название, агент (точка + текст), значок обновления, свежесть, событие пакета.
// Живой регион role="status" стоит в строке постоянно и меняет только текст (T10.1 #94): вставка вместе с текстом не озвучивается
function ModuleRow({ m, project, variant }: { m: WithStatus; project: Project; variant: "expanded" | "collapsed" }) {
const act = useModuleActivity(m.name);
const { status } = m;
const fresh = status.freshness(project);
const label = status.refresh?.label;
const scope = status.scope === "all" ? "все проекты" : "проект";
const action = label ? `${label} — ${status.scope === "all" ? scope : `${scope} ${project.label}`}` : null;
const a = act.agent;
const av = a ? agentText(a) : null;
const agentLabel = av?.text ?? null, agentHint = av?.hint;
const freshText = fresh.error ? `синхронизация не удалась: ${fresh.error}`
: fresh.at ? `данные на ${fmtStamp(fresh.at)} · ${scope}` : "обновления ещё не было";
const ev = act.event ? eventText(act.event, label ?? "") : null;
const online = !!av?.online;
const collapsed = variant === "collapsed";
const eventEl = <span className={`data-status__event${ev ? ` data-status__event--${ev.tone}` : ""}${collapsed ? " visually-hidden" : ""}`}
role="status" title={ev?.text}>{ev?.text}</span>;
const button = action && (
<IconButton label={action} title={collapsed ? [m.title, agentLabel, freshText, ev?.text].filter(Boolean).join(" · ") : action}
disabled={act.busy} aria-busy={act.busy ? "true" : undefined}
icon={act.busy ? <span className="ui-spinner" aria-hidden="true" /> : <IconRefresh />} onClick={() => void act.start()} />);
if (collapsed) {
return (
<div className="data-status data-status--compact" data-module={m.name}>
{button}
{a && <span className={"data-status__dot" + (online ? " is-online" : "")} aria-hidden="true" />}
{eventEl}
</div>
);
}
return (
<div className="data-status" data-module={m.name}>
<div className="data-status__head">
<strong>{m.title}</strong>
{agentLabel && (
<span className="data-status__agent" title={agentHint}>
<span className={"data-status__dot" + (online ? " is-online" : "")} aria-hidden="true" />{agentLabel}
</span>)}
{button}
</div>
<span className={"data-status__sync" + (fresh.error ? " data-status__sync--error" : "")}>{freshText}</span>
{eventEl}
</div>
);
}
// блок состояния модулей панели: по строке на модуль, у которого есть данные для проекта (у вики без корня строки нет)
export function ModuleStatusBlock({ project, variant }: { project: string | null; variant: "expanded" | "collapsed" }) {
const p = useProjects().data?.find((x) => x.label === project);
if (!p) return null;
return <>{WITH_STATUS.filter((m) => m.status.freshness(p).available).map((m) => <ModuleRow key={m.name} m={m} project={p} variant={variant} />)}</>;
}
// узкий экран: события пакетов модулей — строкой под верхней строкой; контейнер role="status" в DOM всегда (T10.1 #94)
function ModuleEvent({ m }: { m: WithStatus }) {
const act = useModuleActivity(m.name);
const ev = act.event ? eventText(act.event, m.status.refresh?.label ?? "") : null;
return ev ? <div className={`topbar-event data-status__event--${ev.tone}`}>{ev.text}</div> : null;
}
export function ModuleEventStrip() {
return <div className="topbar-events" role="status" data-testid="topbar-events">{WITH_STATUS.map((m) => <ModuleEvent key={m.name} m={m} />)}</div>;
}