diff --git a/console/src/components/dialogs.tsx b/console/src/components/dialogs.tsx
new file mode 100644
index 0000000..65c79c6
--- /dev/null
+++ b/console/src/components/dialogs.tsx
@@ -0,0 +1,379 @@
+import {
+ ArrowRight,
+ Clock3,
+ CloudUpload,
+ FileCode2,
+ Package,
+ Play,
+ RadioTower,
+ RefreshCw,
+ ShieldCheck,
+ SquareTerminal,
+ X,
+} from "lucide-react";
+import { FormEvent, ReactNode, useState } from "react";
+import { InvokeResult, RegisterComponentInput } from "../api";
+import {
+ FormattedInvocationOutput,
+ Service,
+ formatInvocationOutput,
+ invocationInputSize,
+ parseInvocationInput,
+} from "../console-model";
+
+function DialogFrame({
+ title,
+ description,
+ icon,
+ onClose,
+ closeDisabled = false,
+ children,
+}: {
+ title: string;
+ description: string;
+ icon: ReactNode;
+ onClose: () => void;
+ closeDisabled?: boolean;
+ children: ReactNode;
+}) {
+ return (
+
+
+
+ );
+}
+
+export function ActivateDeploymentDialog({
+ service,
+ currentRevision,
+ submitting,
+ onClose,
+ onConfirm,
+}: {
+ service: Service;
+ currentRevision: string | null;
+ submitting: boolean;
+ onClose: () => void;
+ onConfirm: () => void;
+}) {
+ return (
+ }
+ onClose={onClose}
+ closeDisabled={submitting}
+ >
+
+
+
+ 当前版本
+ {currentRevision ?? "尚未部署"}
+
+
+
+ 目标版本
+ {service.revision}
+
+
+
+ Wasmeld 将先确认 {service.id}@{service.revision} 的 Actor 可用,再更新对外路由。
+
+
+
+
+
+
+
+ );
+}
+
+export function DeployDialog({
+ onClose,
+ onRegister,
+}: {
+ onClose: () => void;
+ onRegister: (input: RegisterComponentInput) => Promise;
+}) {
+ const [file, setFile] = useState(null);
+ const [submitting, setSubmitting] = useState(false);
+ const [error, setError] = useState("");
+ const fileName = file?.name ?? "";
+
+ async function submit(event: FormEvent) {
+ event.preventDefault();
+ if (!file) return;
+ if (!file.name.toLowerCase().endsWith(".wasmpkg")) {
+ setError("请选择 .wasmpkg 组件包");
+ return;
+ }
+ setSubmitting(true);
+ setError("");
+ try {
+ await onRegister({ file });
+ } catch (submitError) {
+ setError(submitError instanceof Error ? submitError.message : "组件注册失败");
+ setSubmitting(false);
+ }
+ }
+
+ return (
+ }
+ onClose={onClose}
+ >
+
+
+ );
+}
+
+export function WitPackageDialog({
+ onClose,
+ onPublish,
+}: {
+ onClose: () => void;
+ onPublish: (file: File) => Promise;
+}) {
+ const [file, setFile] = useState(null);
+ const [submitting, setSubmitting] = useState(false);
+ const [error, setError] = useState("");
+ const fileName = file?.name ?? "";
+
+ async function submit(event: FormEvent) {
+ event.preventDefault();
+ if (!file) return;
+ if (!file.name.toLowerCase().endsWith(".wasm")) {
+ setError("请选择由 wasmeld wit build 生成的 .wasm 文件");
+ return;
+ }
+ if (file.size > 4 * 1024 * 1024) {
+ setError("WIT 包不能超过 4 MB");
+ return;
+ }
+ setSubmitting(true);
+ setError("");
+ try {
+ await onPublish(file);
+ } catch (submitError) {
+ setError(submitError instanceof Error ? submitError.message : "WIT 包发布失败");
+ setSubmitting(false);
+ }
+ }
+
+ return (
+ }
+ onClose={onClose}
+ >
+
+
+ );
+}
+
+export function InvokeDialog({
+ service,
+ onClose,
+ onInvoke,
+}: {
+ service: Service;
+ onClose: () => void;
+ onInvoke: (service: Service, input: Uint8Array) => Promise;
+}) {
+ const [input, setInput] = useState(service.id === "echo" ? "hello wasm" : "");
+ const [format, setFormat] = useState<"utf8" | "hex">("utf8");
+ const [output, setOutput] = useState("");
+ const [outputBytes, setOutputBytes] = useState(null);
+ const [outputFormat, setOutputFormat] = useState(null);
+ const [running, setRunning] = useState(false);
+ const [failed, setFailed] = useState(false);
+ const [latency, setLatency] = useState(null);
+
+ async function invoke() {
+ setRunning(true);
+ setOutput("");
+ setOutputBytes(null);
+ setOutputFormat(null);
+ setFailed(false);
+ setLatency(null);
+ try {
+ const result = await onInvoke(service, parseInvocationInput(input, format));
+ const formatted = formatInvocationOutput(service, result.output, format);
+ setOutput(formatted.text);
+ setOutputBytes(result.outputBytes);
+ setOutputFormat(formatted);
+ setLatency(result.latencyMs);
+ } catch (invokeError) {
+ setFailed(true);
+ setOutput(invokeError instanceof Error ? invokeError.message : "组件调用失败");
+ } finally {
+ setRunning(false);
+ }
+ }
+
+ return (
+ }
+ onClose={onClose}
+ >
+
+
+
+
+
+
+
{invocationInputSize(input, format)} B
+
+
+
+
+
+
输出
+ {output && latency !== null && outputBytes !== null && outputFormat && (
+
+
+ {outputBytes} B · {outputFormat.format}
+ {outputFormat.automatic ? " · 自动" : ""}
+
+
+
+ {latency} ms
+
+
+ )}
+
+
{running ? "invoking..." : output || "等待调用"}
+
+
+
+
+
+
+
+ );
+}
diff --git a/console/src/components/views.tsx b/console/src/components/views.tsx
new file mode 100644
index 0000000..5609ef7
--- /dev/null
+++ b/console/src/components/views.tsx
@@ -0,0 +1,986 @@
+import {
+ Activity,
+ AlertTriangle,
+ Archive,
+ ArrowRight,
+ Box,
+ Braces,
+ Check,
+ CircleStop,
+ CloudUpload,
+ Cpu,
+ Download,
+ FileCode2,
+ Package,
+ Play,
+ RadioTower,
+ RefreshCw,
+ RotateCcw,
+ Search,
+ Server,
+ SquareTerminal,
+ X,
+ Zap,
+} from "lucide-react";
+import { ReactNode, useState } from "react";
+import {
+ BackendCapability,
+ BackendRuntime,
+ BackendWitPackage,
+ witPackageDownloadUrl,
+} from "../api";
+import {
+ EventTone,
+ RuntimeEvent,
+ Service,
+ ServiceStatus,
+ STATUS_META,
+ formatDuration,
+ serviceKey,
+} from "../console-model";
+
+function PageHeading({
+ eyebrow,
+ title,
+ description,
+ actions,
+}: {
+ eyebrow: string;
+ title: string;
+ description: string;
+ actions?: ReactNode;
+}) {
+ return (
+
+
+
{eyebrow}
+
{title}
+
{description}
+
+ {actions &&
{actions}
}
+
+ );
+}
+
+export function Overview({
+ services,
+ filteredServices,
+ events,
+ running,
+ faulted,
+ totalCalls,
+ deploymentCount,
+ runtime,
+ selectedService,
+ onSelect,
+ onDeploy,
+ onInvoke,
+ onStatus,
+ onActivate,
+ onViewAll,
+ onRefresh,
+ onRuntimeAction,
+ runtimeAction,
+ deploymentAction,
+}: {
+ services: Service[];
+ filteredServices: Service[];
+ events: RuntimeEvent[];
+ running: number;
+ faulted: number;
+ totalCalls: number;
+ deploymentCount: number;
+ runtime: BackendRuntime | null;
+ selectedService: Service | null;
+ onSelect: (service: Service) => void;
+ onDeploy: () => void;
+ onInvoke: (service: Service) => void;
+ onStatus: (service: Service, status: ServiceStatus) => void;
+ onActivate: (service: Service) => void;
+ onViewAll: () => void;
+ onRefresh: () => void;
+ onRuntimeAction: (action: "start" | "stop" | "restart") => void;
+ runtimeAction: "start" | "stop" | "restart" | null;
+ deploymentAction: string | null;
+}) {
+ const errorCount = services.reduce((sum, service) => sum + service.errors, 0);
+ const latencies = services
+ .map((service) => service.latencyMs)
+ .filter((latency): latency is number => latency !== null);
+ const latestLatency = latencies.length ? Math.max(...latencies) : null;
+
+ return (
+ <>
+
+
+
+ >
+ }
+ />
+
+
+ }
+ label="服务版本"
+ value={services.length.toString()}
+ note={`${deploymentCount} 个对外服务`}
+ tone="cyan"
+ />
+ }
+ label="运行实例"
+ value={running.toString()}
+ note={`${services.length - running} 个未运行`}
+ tone="green"
+ />
+ }
+ label="累计调用"
+ value={totalCalls.toLocaleString("zh-CN")}
+ note="持久化统计"
+ tone="amber"
+ />
+ }
+ label="异常"
+ value={(faulted + errorCount).toString()}
+ note={faulted + errorCount ? "需要检查" : "无异常"}
+ tone="coral"
+ />
+
+
+
+
+
+
+
+
+ Wasmeld Runtime
+
+ {runtime?.status === "running"
+ ? "Engine 已启用 fuel 与 epoch interruption"
+ : "Engine 与所有 Actor 已释放"}
+
+
+
+ {runtime?.status === "stopped" ? (
+
+ ) : (
+ <>
+
+
+ >
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
服务状态
+
已注册版本及当前 Actor 状态
+
+
+
+
+
+
+
+
+
+ {selectedService && (
+
+
+
+
+
+
+ 当前选中
+
+ {selectedService.id}
+ @{selectedService.revision}
+
+
+
+
+
+
+
+
+ {!selectedService.active && (
+
+ )}
+
+
+
+ )}
+ >
+ );
+}
+
+function Metric({
+ icon,
+ label,
+ value,
+ note,
+ tone,
+}: {
+ icon: ReactNode;
+ label: string;
+ value: string;
+ note: string;
+ tone: string;
+}) {
+ return (
+
+ {icon}
+
+ {label}
+ {value}
+
+ {note}
+
+ );
+}
+
+function RuntimeStat({ label, value, detail }: { label: string; value: string; detail: string }) {
+ return (
+
+ {label}
+ {value}
+ {detail}
+
+ );
+}
+
+function DetailValue({ label, value }: { label: string; value: string }) {
+ return (
+
+ {label}
+ {value}
+
+ );
+}
+
+function StatusBadge({ status }: { status: ServiceStatus }) {
+ const meta = STATUS_META[status];
+ return (
+
+
+ {meta.label}
+
+ );
+}
+
+function CapabilitySummary({ capabilities }: { capabilities: BackendCapability[] }) {
+ if (capabilities.length === 0) {
+ return 无;
+ }
+
+ const [first, ...remaining] = capabilities;
+ return (
+ capability.interface).join("\n")}
+ >
+
+
+ {first.package}/{first.name} @{first.version}
+
+ {remaining.length > 0 && +{remaining.length}}
+
+ );
+}
+
+function ServiceTable({
+ services,
+ selectedKey,
+ onSelect,
+ onInvoke,
+ onStatus,
+ onActivate,
+ canActivate,
+ deploymentAction,
+ compact = false,
+}: {
+ services: Service[];
+ selectedKey?: string;
+ onSelect: (service: Service) => void;
+ onInvoke: (service: Service) => void;
+ onStatus: (service: Service, status: ServiceStatus) => void;
+ onActivate: (service: Service) => void;
+ canActivate: boolean;
+ deploymentAction: string | null;
+ compact?: boolean;
+}) {
+ if (services.length === 0) {
+ return (
+
+
+ 没有匹配的服务
+ 调整搜索关键词或状态筛选。
+
+ );
+ }
+
+ return (
+
+
+
+
+ | 服务 |
+ 状态 |
+ {!compact && 制品 | }
+ {!compact && Host 能力 | }
+ 调用 |
+ 延迟 |
+
+ 操作
+ |
+
+
+
+ {services.map((service) => (
+
+ |
+
+ |
+
+
+ |
+ {!compact && {service.artifact} | }
+ {!compact && (
+
+
+ |
+ )}
+ {service.calls.toLocaleString("zh-CN")} |
+
+ {service.latencyMs ? `${service.latencyMs} ms` : "—"}
+ |
+
+
+
+ {service.status === "running" ? (
+ <>
+
+
+
+ >
+ ) : (
+
+ )}
+
+ |
+
+ ))}
+
+
+
+ );
+}
+
+function EventList({ events }: { events: RuntimeEvent[] }) {
+ const icons: Record = {
+ success: ,
+ warning: ,
+ danger: ,
+ neutral: ,
+ };
+
+ return (
+
+ {events.map((event) => (
+
+
{icons[event.tone]}
+
+ {event.title}
+ {event.detail}
+
+
+
+ ))}
+
+ );
+}
+
+export function ServicesView({
+ services,
+ statusFilter,
+ onFilter,
+ onDeploy,
+ onInvoke,
+ onSelect,
+ onStatus,
+ onActivate,
+ canActivate,
+ deploymentAction,
+}: {
+ services: Service[];
+ statusFilter: "all" | ServiceStatus;
+ onFilter: (status: "all" | ServiceStatus) => void;
+ onDeploy: () => void;
+ onInvoke: (service: Service) => void;
+ onSelect: (service: Service) => void;
+ onStatus: (service: Service, status: ServiceStatus) => void;
+ onActivate: (service: Service) => void;
+ canActivate: boolean;
+ deploymentAction: string | null;
+}) {
+ return (
+ <>
+
+
+ 注册组件
+
+ }
+ />
+
+
+
+ {[
+ ["all", "全部"],
+ ["running", "运行中"],
+ ["stopped", "已停止"],
+ ["faulted", "故障"],
+ ].map(([value, label]) => (
+
+ ))}
+
+
{services.length} 个版本
+
+
+
+ >
+ );
+}
+
+export function WitPackagesView({
+ apiBase,
+ packages,
+ onPublish,
+}: {
+ apiBase: string;
+ packages: BackendWitPackage[];
+ onPublish: () => void;
+}) {
+ function downloadPackage(packageMetadata: BackendWitPackage) {
+ const anchor = document.createElement("a");
+ anchor.href = witPackageDownloadUrl(apiBase, packageMetadata);
+ anchor.download = `${packageMetadata.name.replace(":", "-")}-${packageMetadata.version}.wasm`;
+ document.body.append(anchor);
+ anchor.click();
+ anchor.remove();
+ }
+
+ return (
+ <>
+
+
+ 发布 WIT 包
+
+ }
+ />
+
+
+ 发布后不可覆盖;包名、版本与依赖由制品自动解析。
+ {packages.length} 个包版本
+
+ {packages.length === 0 ? (
+
+
+ 没有匹配的 WIT 包
+ 发布二进制 WIT Package,或调整搜索关键词。
+
+ ) : (
+
+
+
+
+ | Package |
+ 依赖 |
+ SHA-256 |
+
+ 操作
+ |
+
+
+
+ {packages.map((packageMetadata) => {
+ const dependencies = packageMetadata.dependencies
+ .map((dependency) => `${dependency.name}@${dependency.version}`)
+ .join(", ");
+ return (
+
+ |
+
+
+
+
+
+ {packageMetadata.name}
+ {packageMetadata.version}
+
+
+ |
+
+ {dependencies || "无直接依赖"}
+ |
+
+ {packageMetadata.sha256}
+ |
+
+
+
+
+ |
+
+ );
+ })}
+
+
+
+ )}
+
+ >
+ );
+}
+
+export function InstancesView({
+ services,
+ onInvoke,
+ onStatus,
+ onRefresh,
+}: {
+ services: Service[];
+ onInvoke: (service: Service) => void;
+ onStatus: (service: Service, status: ServiceStatus) => void;
+ onRefresh: () => void;
+}) {
+ const instances = services.filter(
+ (service) => service.status === "running" || service.status === "faulted",
+ );
+
+ return (
+ <>
+
+
+
+ }
+ />
+
+ {instances.length === 0 ? (
+
+ ) : (
+ instances.map((service) => (
+
+
+
+
+
+
+
+
+ {service.id}@{service.revision}
+
+ actor/{service.id}-01
+
+
+
+
+
+
+
+ {service.updatedAt}
+
+
+
+
+
+
+
+ ))
+ )}
+
+ >
+ );
+}
+
+export function ActivityView({
+ events,
+ services,
+}: {
+ events: RuntimeEvent[];
+ services: Service[];
+}) {
+ const calls = services.reduce((sum, service) => sum + service.calls, 0);
+ const errors = services.reduce((sum, service) => sum + service.errors, 0);
+ const latencies = services
+ .map((service) => service.latencyMs)
+ .filter((latency): latency is number => latency !== null);
+ const averageLatency = latencies.length
+ ? latencies.reduce((sum, latency) => sum + latency, 0) / latencies.length
+ : null;
+
+ function exportEvents() {
+ const blob = new Blob([JSON.stringify(events, null, 2)], {
+ type: "application/json",
+ });
+ const url = URL.createObjectURL(blob);
+ const anchor = document.createElement("a");
+ anchor.href = url;
+ anchor.download = "wasmeld-events.json";
+ anchor.click();
+ URL.revokeObjectURL(url);
+ }
+
+ return (
+ <>
+
+
+ 导出记录
+
+ }
+ />
+
+
+
+ 时间范围
+ 当前进程
+
+
+ 成功率
+ {calls ? `${(((calls - errors) / calls) * 100).toFixed(2)}%` : "—"}
+
+
+ 平均延迟
+ {averageLatency === null ? "—" : `${averageLatency.toFixed(1)} ms`}
+
+
+ 记录数
+ {events.length}
+
+
+
+
+ >
+ );
+}
+
+export function SettingsView({
+ endpoint,
+ onSaveEndpoint,
+}: {
+ endpoint: string;
+ onSaveEndpoint: (value: string) => void;
+}) {
+ const [value, setValue] = useState(endpoint);
+
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/console/src/console-model.ts b/console/src/console-model.ts
new file mode 100644
index 0000000..9436685
--- /dev/null
+++ b/console/src/console-model.ts
@@ -0,0 +1,208 @@
+import { FileCode2, History, LayoutDashboard, Package, Server, Settings } from "lucide-react";
+import { BackendCapability, BackendEvent, BackendService } from "./api";
+
+export type View = "overview" | "services" | "instances" | "wit-packages" | "activity" | "settings";
+export type ServiceStatus = "running" | "stopped" | "faulted";
+export type EventTone = "success" | "warning" | "danger" | "neutral";
+
+export type Service = {
+ id: string;
+ revision: string;
+ description: string;
+ artifact: string;
+ status: ServiceStatus;
+ updatedAt: string;
+ memoryMb: number;
+ fuel: string;
+ deadlineMs: number;
+ mailbox: number;
+ maxInputKb: number;
+ capabilities: BackendCapability[];
+ calls: number;
+ errors: number;
+ latencyMs: number | null;
+ active: boolean;
+};
+
+export type RuntimeEvent = {
+ id: number;
+ time: string;
+ title: string;
+ detail: string;
+ tone: EventTone;
+};
+
+export type ConnectionState = "connecting" | "online" | "offline";
+
+export const NAV_ITEMS: Array<{
+ id: View;
+ label: string;
+ icon: typeof LayoutDashboard;
+}> = [
+ { id: "overview", label: "运行概览", icon: LayoutDashboard },
+ { id: "services", label: "服务版本", icon: Package },
+ { id: "instances", label: "运行实例", icon: Server },
+ { id: "wit-packages", label: "WIT 包", icon: FileCode2 },
+ { id: "activity", label: "调用记录", icon: History },
+ { id: "settings", label: "运行设置", icon: Settings },
+];
+
+export const STATUS_META: Record = {
+ running: { label: "运行中", className: "status-running" },
+ stopped: { label: "已停止", className: "status-stopped" },
+ faulted: { label: "故障", className: "status-faulted" },
+};
+
+export function serviceKey(service: Pick) {
+ return `${service.id}@${service.revision}`;
+}
+
+export function toService(service: BackendService, activeRevision?: string): Service {
+ return {
+ id: service.id,
+ revision: service.revision,
+ description: "Wasm Component",
+ artifact: service.artifact.split(/[\\/]/).pop() ?? service.artifact,
+ status: service.status,
+ updatedAt: new Date(service.updated_at_ms).toLocaleString("zh-CN", {
+ hour12: false,
+ }),
+ memoryMb: Math.round(service.limits.memory_bytes / 1024 / 1024),
+ fuel: compactNumber(service.limits.fuel_per_call),
+ deadlineMs: service.limits.deadline_ms,
+ mailbox: service.limits.mailbox_capacity,
+ maxInputKb: Math.round(service.limits.max_input_bytes / 1024),
+ capabilities: service.capabilities,
+ calls: service.calls,
+ errors: service.errors,
+ latencyMs: service.last_latency_ms,
+ active: service.revision === activeRevision,
+ };
+}
+
+export function toEvent(event: BackendEvent): RuntimeEvent {
+ const identity =
+ event.service_id && event.revision ? `${event.service_id}@${event.revision}` : "Wasmeld";
+ const meta: Record = {
+ registered: { label: "已注册", tone: "success" },
+ unregistered: { label: "已注销", tone: "neutral" },
+ deployed: { label: "部署切换", tone: "success" },
+ started: { label: "已启动", tone: "success" },
+ stopped: { label: "已停止", tone: "neutral" },
+ invoked: { label: "调用完成", tone: "success" },
+ failed: { label: "操作失败", tone: "danger" },
+ };
+ return {
+ id: event.id,
+ time: new Date(event.timestamp_ms).toLocaleTimeString("zh-CN", {
+ hour12: false,
+ }),
+ title: `${identity} ${meta[event.kind].label}`,
+ detail: event.message,
+ tone: meta[event.kind].tone,
+ };
+}
+
+export function compactNumber(value: number) {
+ if (value >= 1_000_000) return `${value / 1_000_000}M`;
+ if (value >= 1_000) return `${value / 1_000}K`;
+ return value.toString();
+}
+
+export function formatDuration(milliseconds: number) {
+ const totalSeconds = Math.floor(milliseconds / 1000);
+ const hours = Math.floor(totalSeconds / 3600);
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
+ const seconds = totalSeconds % 60;
+ return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m ${seconds}s`;
+}
+
+export function formatApiHost(value: string) {
+ try {
+ return new URL(value).host;
+ } catch {
+ return value;
+ }
+}
+
+export function parseInvocationInput(value: string, format: "utf8" | "hex") {
+ if (format === "utf8") return new TextEncoder().encode(value);
+ const compact = value.replace(/\s+/g, "");
+ if (compact.length % 2 !== 0 || /[^0-9a-f]/i.test(compact)) {
+ throw new Error("HEX 输入必须由成对的十六进制字符组成");
+ }
+ return Uint8Array.from(compact.match(/.{2}/g)?.map((byte) => Number.parseInt(byte, 16)) ?? []);
+}
+
+export function invocationInputSize(value: string, format: "utf8" | "hex") {
+ try {
+ return parseInvocationInput(value, format).byteLength;
+ } catch {
+ return 0;
+ }
+}
+
+export type FormattedInvocationOutput = {
+ text: string;
+ format: "UTF-8" | "HEX" | "U64 LE";
+ automatic: boolean;
+};
+
+export function formatHex(bytes: Uint8Array) {
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(" ");
+}
+
+export function decodeReadableUtf8(bytes: Uint8Array) {
+ let text: string;
+ try {
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
+ } catch {
+ return null;
+ }
+
+ for (const character of text) {
+ const codePoint = character.codePointAt(0) ?? 0;
+ const allowedWhitespace = codePoint === 0x09 || codePoint === 0x0a || codePoint === 0x0d;
+ if ((codePoint < 0x20 && !allowedWhitespace) || codePoint === 0x7f) {
+ return null;
+ }
+ }
+ return text;
+}
+
+export function formatInvocationOutput(
+ service: Service,
+ output: Uint8Array,
+ format: "utf8" | "hex",
+) {
+ if (service.id === "counter" && output.byteLength === 8) {
+ return {
+ text: new DataView(output.buffer, output.byteOffset, output.byteLength)
+ .getBigUint64(0, true)
+ .toString(),
+ format: "U64 LE",
+ automatic: false,
+ } satisfies FormattedInvocationOutput;
+ }
+ if (format === "hex") {
+ return {
+ text: formatHex(output) || "(empty)",
+ format: "HEX",
+ automatic: false,
+ } satisfies FormattedInvocationOutput;
+ }
+
+ const text = decodeReadableUtf8(output);
+ if (text !== null) {
+ return {
+ text: text || "(empty)",
+ format: "UTF-8",
+ automatic: false,
+ } satisfies FormattedInvocationOutput;
+ }
+ return {
+ text: formatHex(output),
+ format: "HEX",
+ automatic: true,
+ } satisfies FormattedInvocationOutput;
+}
diff --git a/console/src/routes/index.tsx b/console/src/routes/index.tsx
index 4be2293..976ba7e 100644
--- a/console/src/routes/index.tsx
+++ b/console/src/routes/index.tsx
@@ -1,43 +1,10 @@
import { createFileRoute } from "@tanstack/react-router";
+import { ArrowRight, Braces, CheckCircle2, Search, Server, ShieldCheck } from "lucide-react";
+import { useCallback, useEffect, useMemo, useState } from "react";
import {
- Activity,
- AlertTriangle,
- Archive,
- ArrowRight,
- Box,
- Braces,
- Check,
- CheckCircle2,
- CircleStop,
- Clock3,
- CloudUpload,
- Cpu,
- Download,
- FileCode2,
- History,
- LayoutDashboard,
- Package,
- Play,
- RadioTower,
- RefreshCw,
- RotateCcw,
- Search,
- Server,
- Settings,
- ShieldCheck,
- SquareTerminal,
- X,
- Zap,
-} from "lucide-react";
-import { FormEvent, ReactNode, useCallback, useEffect, useMemo, useState } from "react";
-import {
- BackendCapability,
- BackendEvent,
BackendDeployment,
BackendRuntime,
- BackendService,
BackendWitPackage,
- InvokeResult,
RegisterComponentInput,
activateDeployment,
changeRuntimeState,
@@ -48,210 +15,34 @@ import {
publishWitPackage,
registerComponent,
saveApiBase,
- witPackageDownloadUrl,
} from "../api";
-
-type View = "overview" | "services" | "instances" | "wit-packages" | "activity" | "settings";
-type ServiceStatus = "running" | "stopped" | "faulted";
-type EventTone = "success" | "warning" | "danger" | "neutral";
-
-type Service = {
- id: string;
- revision: string;
- description: string;
- artifact: string;
- status: ServiceStatus;
- updatedAt: string;
- memoryMb: number;
- fuel: string;
- deadlineMs: number;
- mailbox: number;
- maxInputKb: number;
- capabilities: BackendCapability[];
- calls: number;
- errors: number;
- latencyMs: number | null;
- active: boolean;
-};
-
-type RuntimeEvent = {
- id: number;
- time: string;
- title: string;
- detail: string;
- tone: EventTone;
-};
-
-type ConnectionState = "connecting" | "online" | "offline";
-
-const NAV_ITEMS: Array<{
- id: View;
- label: string;
- icon: typeof LayoutDashboard;
-}> = [
- { id: "overview", label: "运行概览", icon: LayoutDashboard },
- { id: "services", label: "服务版本", icon: Package },
- { id: "instances", label: "运行实例", icon: Server },
- { id: "wit-packages", label: "WIT 包", icon: FileCode2 },
- { id: "activity", label: "调用记录", icon: History },
- { id: "settings", label: "运行设置", icon: Settings },
-];
-
-const STATUS_META: Record = {
- running: { label: "运行中", className: "status-running" },
- stopped: { label: "已停止", className: "status-stopped" },
- faulted: { label: "故障", className: "status-faulted" },
-};
-
-function serviceKey(service: Pick) {
- return `${service.id}@${service.revision}`;
-}
-
-function toService(service: BackendService, activeRevision?: string): Service {
- return {
- id: service.id,
- revision: service.revision,
- description: "Wasm Component",
- artifact: service.artifact.split(/[\\/]/).pop() ?? service.artifact,
- status: service.status,
- updatedAt: new Date(service.updated_at_ms).toLocaleString("zh-CN", {
- hour12: false,
- }),
- memoryMb: Math.round(service.limits.memory_bytes / 1024 / 1024),
- fuel: compactNumber(service.limits.fuel_per_call),
- deadlineMs: service.limits.deadline_ms,
- mailbox: service.limits.mailbox_capacity,
- maxInputKb: Math.round(service.limits.max_input_bytes / 1024),
- capabilities: service.capabilities,
- calls: service.calls,
- errors: service.errors,
- latencyMs: service.last_latency_ms,
- active: service.revision === activeRevision,
- };
-}
-
-function toEvent(event: BackendEvent): RuntimeEvent {
- const identity =
- event.service_id && event.revision ? `${event.service_id}@${event.revision}` : "Wasmeld";
- const meta: Record = {
- registered: { label: "已注册", tone: "success" },
- unregistered: { label: "已注销", tone: "neutral" },
- deployed: { label: "部署切换", tone: "success" },
- started: { label: "已启动", tone: "success" },
- stopped: { label: "已停止", tone: "neutral" },
- invoked: { label: "调用完成", tone: "success" },
- failed: { label: "操作失败", tone: "danger" },
- };
- return {
- id: event.id,
- time: new Date(event.timestamp_ms).toLocaleTimeString("zh-CN", {
- hour12: false,
- }),
- title: `${identity} ${meta[event.kind].label}`,
- detail: event.message,
- tone: meta[event.kind].tone,
- };
-}
-
-function compactNumber(value: number) {
- if (value >= 1_000_000) return `${value / 1_000_000}M`;
- if (value >= 1_000) return `${value / 1_000}K`;
- return value.toString();
-}
-
-function formatDuration(milliseconds: number) {
- const totalSeconds = Math.floor(milliseconds / 1000);
- const hours = Math.floor(totalSeconds / 3600);
- const minutes = Math.floor((totalSeconds % 3600) / 60);
- const seconds = totalSeconds % 60;
- return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m ${seconds}s`;
-}
-
-function formatApiHost(value: string) {
- try {
- return new URL(value).host;
- } catch {
- return value;
- }
-}
-
-function parseInvocationInput(value: string, format: "utf8" | "hex") {
- if (format === "utf8") return new TextEncoder().encode(value);
- const compact = value.replace(/\s+/g, "");
- if (compact.length % 2 !== 0 || /[^0-9a-f]/i.test(compact)) {
- throw new Error("HEX 输入必须由成对的十六进制字符组成");
- }
- return Uint8Array.from(compact.match(/.{2}/g)?.map((byte) => Number.parseInt(byte, 16)) ?? []);
-}
-
-function invocationInputSize(value: string, format: "utf8" | "hex") {
- try {
- return parseInvocationInput(value, format).byteLength;
- } catch {
- return 0;
- }
-}
-
-type FormattedInvocationOutput = {
- text: string;
- format: "UTF-8" | "HEX" | "U64 LE";
- automatic: boolean;
-};
-
-function formatHex(bytes: Uint8Array) {
- return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(" ");
-}
-
-function decodeReadableUtf8(bytes: Uint8Array) {
- let text: string;
- try {
- text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
- } catch {
- return null;
- }
-
- for (const character of text) {
- const codePoint = character.codePointAt(0) ?? 0;
- const allowedWhitespace = codePoint === 0x09 || codePoint === 0x0a || codePoint === 0x0d;
- if ((codePoint < 0x20 && !allowedWhitespace) || codePoint === 0x7f) {
- return null;
- }
- }
- return text;
-}
-
-function formatInvocationOutput(service: Service, output: Uint8Array, format: "utf8" | "hex") {
- if (service.id === "counter" && output.byteLength === 8) {
- return {
- text: new DataView(output.buffer, output.byteOffset, output.byteLength)
- .getBigUint64(0, true)
- .toString(),
- format: "U64 LE",
- automatic: false,
- } satisfies FormattedInvocationOutput;
- }
- if (format === "hex") {
- return {
- text: formatHex(output) || "(empty)",
- format: "HEX",
- automatic: false,
- } satisfies FormattedInvocationOutput;
- }
-
- const text = decodeReadableUtf8(output);
- if (text !== null) {
- return {
- text: text || "(empty)",
- format: "UTF-8",
- automatic: false,
- } satisfies FormattedInvocationOutput;
- }
- return {
- text: formatHex(output),
- format: "HEX",
- automatic: true,
- } satisfies FormattedInvocationOutput;
-}
+import {
+ ConnectionState,
+ NAV_ITEMS,
+ RuntimeEvent,
+ Service,
+ ServiceStatus,
+ View,
+ formatApiHost,
+ formatDuration,
+ serviceKey,
+ toEvent,
+ toService,
+} from "../console-model";
+import {
+ ActivateDeploymentDialog,
+ DeployDialog,
+ InvokeDialog,
+ WitPackageDialog,
+} from "../components/dialogs";
+import {
+ ActivityView,
+ InstancesView,
+ Overview,
+ ServicesView,
+ SettingsView,
+ WitPackagesView,
+} from "../components/views";
export const Route = createFileRoute("/")({
component: Home,
@@ -690,1300 +481,3 @@ function Sidebar({
);
}
-
-function PageHeading({
- eyebrow,
- title,
- description,
- actions,
-}: {
- eyebrow: string;
- title: string;
- description: string;
- actions?: ReactNode;
-}) {
- return (
-
-
-
{eyebrow}
-
{title}
-
{description}
-
- {actions &&
{actions}
}
-
- );
-}
-
-function Overview({
- services,
- filteredServices,
- events,
- running,
- faulted,
- totalCalls,
- deploymentCount,
- runtime,
- selectedService,
- onSelect,
- onDeploy,
- onInvoke,
- onStatus,
- onActivate,
- onViewAll,
- onRefresh,
- onRuntimeAction,
- runtimeAction,
- deploymentAction,
-}: {
- services: Service[];
- filteredServices: Service[];
- events: RuntimeEvent[];
- running: number;
- faulted: number;
- totalCalls: number;
- deploymentCount: number;
- runtime: BackendRuntime | null;
- selectedService: Service | null;
- onSelect: (service: Service) => void;
- onDeploy: () => void;
- onInvoke: (service: Service) => void;
- onStatus: (service: Service, status: ServiceStatus) => void;
- onActivate: (service: Service) => void;
- onViewAll: () => void;
- onRefresh: () => void;
- onRuntimeAction: (action: "start" | "stop" | "restart") => void;
- runtimeAction: "start" | "stop" | "restart" | null;
- deploymentAction: string | null;
-}) {
- const errorCount = services.reduce((sum, service) => sum + service.errors, 0);
- const latencies = services
- .map((service) => service.latencyMs)
- .filter((latency): latency is number => latency !== null);
- const latestLatency = latencies.length ? Math.max(...latencies) : null;
-
- return (
- <>
-
-
-
- >
- }
- />
-
-
- }
- label="服务版本"
- value={services.length.toString()}
- note={`${deploymentCount} 个对外服务`}
- tone="cyan"
- />
- }
- label="运行实例"
- value={running.toString()}
- note={`${services.length - running} 个未运行`}
- tone="green"
- />
- }
- label="累计调用"
- value={totalCalls.toLocaleString("zh-CN")}
- note="持久化统计"
- tone="amber"
- />
- }
- label="异常"
- value={(faulted + errorCount).toString()}
- note={faulted + errorCount ? "需要检查" : "无异常"}
- tone="coral"
- />
-
-
-
-
-
-
-
-
- Wasmeld Runtime
-
- {runtime?.status === "running"
- ? "Engine 已启用 fuel 与 epoch interruption"
- : "Engine 与所有 Actor 已释放"}
-
-
-
- {runtime?.status === "stopped" ? (
-
- ) : (
- <>
-
-
- >
- )}
-
-
-
-
-
-
-
-
-
-
-
-
-
服务状态
-
已注册版本及当前 Actor 状态
-
-
-
-
-
-
-
-
-
- {selectedService && (
-
-
-
-
-
-
- 当前选中
-
- {selectedService.id}
- @{selectedService.revision}
-
-
-
-
-
-
-
-
- {!selectedService.active && (
-
- )}
-
-
-
- )}
- >
- );
-}
-
-function Metric({
- icon,
- label,
- value,
- note,
- tone,
-}: {
- icon: ReactNode;
- label: string;
- value: string;
- note: string;
- tone: string;
-}) {
- return (
-
- {icon}
-
- {label}
- {value}
-
- {note}
-
- );
-}
-
-function RuntimeStat({ label, value, detail }: { label: string; value: string; detail: string }) {
- return (
-
- {label}
- {value}
- {detail}
-
- );
-}
-
-function DetailValue({ label, value }: { label: string; value: string }) {
- return (
-
- {label}
- {value}
-
- );
-}
-
-function StatusBadge({ status }: { status: ServiceStatus }) {
- const meta = STATUS_META[status];
- return (
-
-
- {meta.label}
-
- );
-}
-
-function CapabilitySummary({ capabilities }: { capabilities: BackendCapability[] }) {
- if (capabilities.length === 0) {
- return 无;
- }
-
- const [first, ...remaining] = capabilities;
- return (
- capability.interface).join("\n")}
- >
-
-
- {first.package}/{first.name} @{first.version}
-
- {remaining.length > 0 && +{remaining.length}}
-
- );
-}
-
-function ServiceTable({
- services,
- selectedKey,
- onSelect,
- onInvoke,
- onStatus,
- onActivate,
- canActivate,
- deploymentAction,
- compact = false,
-}: {
- services: Service[];
- selectedKey?: string;
- onSelect: (service: Service) => void;
- onInvoke: (service: Service) => void;
- onStatus: (service: Service, status: ServiceStatus) => void;
- onActivate: (service: Service) => void;
- canActivate: boolean;
- deploymentAction: string | null;
- compact?: boolean;
-}) {
- if (services.length === 0) {
- return (
-
-
- 没有匹配的服务
- 调整搜索关键词或状态筛选。
-
- );
- }
-
- return (
-
-
-
-
- | 服务 |
- 状态 |
- {!compact && 制品 | }
- {!compact && Host 能力 | }
- 调用 |
- 延迟 |
-
- 操作
- |
-
-
-
- {services.map((service) => (
-
- |
-
- |
-
-
- |
- {!compact && {service.artifact} | }
- {!compact && (
-
-
- |
- )}
- {service.calls.toLocaleString("zh-CN")} |
-
- {service.latencyMs ? `${service.latencyMs} ms` : "—"}
- |
-
-
-
- {service.status === "running" ? (
- <>
-
-
-
- >
- ) : (
-
- )}
-
- |
-
- ))}
-
-
-
- );
-}
-
-function EventList({ events }: { events: RuntimeEvent[] }) {
- const icons: Record = {
- success: ,
- warning: ,
- danger: ,
- neutral: ,
- };
-
- return (
-
- {events.map((event) => (
-
-
{icons[event.tone]}
-
- {event.title}
- {event.detail}
-
-
-
- ))}
-
- );
-}
-
-function ServicesView({
- services,
- statusFilter,
- onFilter,
- onDeploy,
- onInvoke,
- onSelect,
- onStatus,
- onActivate,
- canActivate,
- deploymentAction,
-}: {
- services: Service[];
- statusFilter: "all" | ServiceStatus;
- onFilter: (status: "all" | ServiceStatus) => void;
- onDeploy: () => void;
- onInvoke: (service: Service) => void;
- onSelect: (service: Service) => void;
- onStatus: (service: Service, status: ServiceStatus) => void;
- onActivate: (service: Service) => void;
- canActivate: boolean;
- deploymentAction: string | null;
-}) {
- return (
- <>
-
-
- 注册组件
-
- }
- />
-
-
-
- {[
- ["all", "全部"],
- ["running", "运行中"],
- ["stopped", "已停止"],
- ["faulted", "故障"],
- ].map(([value, label]) => (
-
- ))}
-
-
{services.length} 个版本
-
-
-
- >
- );
-}
-
-function WitPackagesView({
- apiBase,
- packages,
- onPublish,
-}: {
- apiBase: string;
- packages: BackendWitPackage[];
- onPublish: () => void;
-}) {
- function downloadPackage(packageMetadata: BackendWitPackage) {
- const anchor = document.createElement("a");
- anchor.href = witPackageDownloadUrl(apiBase, packageMetadata);
- anchor.download = `${packageMetadata.name.replace(":", "-")}-${packageMetadata.version}.wasm`;
- document.body.append(anchor);
- anchor.click();
- anchor.remove();
- }
-
- return (
- <>
-
-
- 发布 WIT 包
-
- }
- />
-
-
- 发布后不可覆盖;包名、版本与依赖由制品自动解析。
- {packages.length} 个包版本
-
- {packages.length === 0 ? (
-
-
- 没有匹配的 WIT 包
- 发布二进制 WIT Package,或调整搜索关键词。
-
- ) : (
-
-
-
-
- | Package |
- 依赖 |
- SHA-256 |
-
- 操作
- |
-
-
-
- {packages.map((packageMetadata) => {
- const dependencies = packageMetadata.dependencies
- .map((dependency) => `${dependency.name}@${dependency.version}`)
- .join(", ");
- return (
-
- |
-
-
-
-
-
- {packageMetadata.name}
- {packageMetadata.version}
-
-
- |
-
- {dependencies || "无直接依赖"}
- |
-
- {packageMetadata.sha256}
- |
-
-
-
-
- |
-
- );
- })}
-
-
-
- )}
-
- >
- );
-}
-
-function InstancesView({
- services,
- onInvoke,
- onStatus,
- onRefresh,
-}: {
- services: Service[];
- onInvoke: (service: Service) => void;
- onStatus: (service: Service, status: ServiceStatus) => void;
- onRefresh: () => void;
-}) {
- const instances = services.filter(
- (service) => service.status === "running" || service.status === "faulted",
- );
-
- return (
- <>
-
-
-
- }
- />
-
- {instances.length === 0 ? (
-
- ) : (
- instances.map((service) => (
-
-
-
-
-
-
-
-
- {service.id}@{service.revision}
-
- actor/{service.id}-01
-
-
-
-
-
-
-
- {service.updatedAt}
-
-
-
-
-
-
-
- ))
- )}
-
- >
- );
-}
-
-function ActivityView({ events, services }: { events: RuntimeEvent[]; services: Service[] }) {
- const calls = services.reduce((sum, service) => sum + service.calls, 0);
- const errors = services.reduce((sum, service) => sum + service.errors, 0);
- const latencies = services
- .map((service) => service.latencyMs)
- .filter((latency): latency is number => latency !== null);
- const averageLatency = latencies.length
- ? latencies.reduce((sum, latency) => sum + latency, 0) / latencies.length
- : null;
-
- function exportEvents() {
- const blob = new Blob([JSON.stringify(events, null, 2)], {
- type: "application/json",
- });
- const url = URL.createObjectURL(blob);
- const anchor = document.createElement("a");
- anchor.href = url;
- anchor.download = "wasmeld-events.json";
- anchor.click();
- URL.revokeObjectURL(url);
- }
-
- return (
- <>
-
-
- 导出记录
-
- }
- />
-
-
-
- 时间范围
- 当前进程
-
-
- 成功率
- {calls ? `${(((calls - errors) / calls) * 100).toFixed(2)}%` : "—"}
-
-
- 平均延迟
- {averageLatency === null ? "—" : `${averageLatency.toFixed(1)} ms`}
-
-
- 记录数
- {events.length}
-
-
-
-
- >
- );
-}
-
-function SettingsView({
- endpoint,
- onSaveEndpoint,
-}: {
- endpoint: string;
- onSaveEndpoint: (value: string) => void;
-}) {
- const [value, setValue] = useState(endpoint);
-
- return (
- <>
-
-
- >
- );
-}
-
-function DialogFrame({
- title,
- description,
- icon,
- onClose,
- closeDisabled = false,
- children,
-}: {
- title: string;
- description: string;
- icon: ReactNode;
- onClose: () => void;
- closeDisabled?: boolean;
- children: ReactNode;
-}) {
- return (
-
-
-
- );
-}
-
-function ActivateDeploymentDialog({
- service,
- currentRevision,
- submitting,
- onClose,
- onConfirm,
-}: {
- service: Service;
- currentRevision: string | null;
- submitting: boolean;
- onClose: () => void;
- onConfirm: () => void;
-}) {
- return (
- }
- onClose={onClose}
- closeDisabled={submitting}
- >
-
-
-
- 当前版本
- {currentRevision ?? "尚未部署"}
-
-
-
- 目标版本
- {service.revision}
-
-
-
- Wasmeld 将先确认 {service.id}@{service.revision} 的 Actor 可用,再更新对外路由。
-
-
-
-
-
-
-
- );
-}
-
-function DeployDialog({
- onClose,
- onRegister,
-}: {
- onClose: () => void;
- onRegister: (input: RegisterComponentInput) => Promise;
-}) {
- const [file, setFile] = useState(null);
- const [submitting, setSubmitting] = useState(false);
- const [error, setError] = useState("");
- const fileName = file?.name ?? "";
-
- async function submit(event: FormEvent) {
- event.preventDefault();
- if (!file) return;
- if (!file.name.toLowerCase().endsWith(".wasmpkg")) {
- setError("请选择 .wasmpkg 组件包");
- return;
- }
- setSubmitting(true);
- setError("");
- try {
- await onRegister({ file });
- } catch (submitError) {
- setError(submitError instanceof Error ? submitError.message : "组件注册失败");
- setSubmitting(false);
- }
- }
-
- return (
- }
- onClose={onClose}
- >
-
-
- );
-}
-
-function WitPackageDialog({
- onClose,
- onPublish,
-}: {
- onClose: () => void;
- onPublish: (file: File) => Promise;
-}) {
- const [file, setFile] = useState(null);
- const [submitting, setSubmitting] = useState(false);
- const [error, setError] = useState("");
- const fileName = file?.name ?? "";
-
- async function submit(event: FormEvent) {
- event.preventDefault();
- if (!file) return;
- if (!file.name.toLowerCase().endsWith(".wasm")) {
- setError("请选择由 wasmeld wit build 生成的 .wasm 文件");
- return;
- }
- if (file.size > 4 * 1024 * 1024) {
- setError("WIT 包不能超过 4 MB");
- return;
- }
- setSubmitting(true);
- setError("");
- try {
- await onPublish(file);
- } catch (submitError) {
- setError(submitError instanceof Error ? submitError.message : "WIT 包发布失败");
- setSubmitting(false);
- }
- }
-
- return (
- }
- onClose={onClose}
- >
-
-
- );
-}
-
-function InvokeDialog({
- service,
- onClose,
- onInvoke,
-}: {
- service: Service;
- onClose: () => void;
- onInvoke: (service: Service, input: Uint8Array) => Promise;
-}) {
- const [input, setInput] = useState(service.id === "echo" ? "hello wasm" : "");
- const [format, setFormat] = useState<"utf8" | "hex">("utf8");
- const [output, setOutput] = useState("");
- const [outputBytes, setOutputBytes] = useState(null);
- const [outputFormat, setOutputFormat] = useState(null);
- const [running, setRunning] = useState(false);
- const [failed, setFailed] = useState(false);
- const [latency, setLatency] = useState(null);
-
- async function invoke() {
- setRunning(true);
- setOutput("");
- setOutputBytes(null);
- setOutputFormat(null);
- setFailed(false);
- setLatency(null);
- try {
- const result = await onInvoke(service, parseInvocationInput(input, format));
- const formatted = formatInvocationOutput(service, result.output, format);
- setOutput(formatted.text);
- setOutputBytes(result.outputBytes);
- setOutputFormat(formatted);
- setLatency(result.latencyMs);
- } catch (invokeError) {
- setFailed(true);
- setOutput(invokeError instanceof Error ? invokeError.message : "组件调用失败");
- } finally {
- setRunning(false);
- }
- }
-
- return (
- }
- onClose={onClose}
- >
-
-
-
-
-
-
-
{invocationInputSize(input, format)} B
-
-
-
-
-
-
输出
- {output && latency !== null && outputBytes !== null && outputFormat && (
-
-
- {outputBytes} B · {outputFormat.format}
- {outputFormat.automatic ? " · 自动" : ""}
-
-
-
- {latency} ms
-
-
- )}
-
-
{running ? "invoking..." : output || "等待调用"}
-
-
-
-
-
-
-
- );
-}
diff --git a/console/tests/rendered-html.test.mjs b/console/tests/rendered-html.test.mjs
index e3e65b7..bd0fb0e 100644
--- a/console/tests/rendered-html.test.mjs
+++ b/console/tests/rendered-html.test.mjs
@@ -83,9 +83,22 @@ test("server-renders the Wasm management console", async () => {
});
test("uses TanStack Start routing and produces Node artifacts", async () => {
- const [rootRoute, indexRoute, apiClient, router, packageJson, viteConfig] = await Promise.all([
+ const [
+ rootRoute,
+ indexRoute,
+ views,
+ dialogs,
+ consoleModel,
+ apiClient,
+ router,
+ packageJson,
+ viteConfig,
+ ] = await Promise.all([
readFile(new URL("../src/routes/__root.tsx", import.meta.url), "utf8"),
readFile(new URL("../src/routes/index.tsx", import.meta.url), "utf8"),
+ readFile(new URL("../src/components/views.tsx", import.meta.url), "utf8"),
+ readFile(new URL("../src/components/dialogs.tsx", import.meta.url), "utf8"),
+ readFile(new URL("../src/console-model.ts", import.meta.url), "utf8"),
readFile(new URL("../src/api.ts", import.meta.url), "utf8"),
readFile(new URL("../src/router.tsx", import.meta.url), "utf8"),
readFile(new URL("../package.json", import.meta.url), "utf8"),
@@ -96,11 +109,11 @@ test("uses TanStack Start routing and produces Node artifacts", async () => {
assert.match(rootRoute, //);
assert.match(rootRoute, //);
assert.match(indexRoute, /createFileRoute\("\/"\)/);
- assert.match(indexRoute, /切换对外版本/);
- assert.match(indexRoute, /Host 能力/);
- assert.match(indexRoute, /service\.capabilities/);
- assert.match(indexRoute, /TextDecoder\("utf-8", \{ fatal: true \}\)/);
- assert.match(indexRoute, /outputFormat\.automatic/);
+ assert.match(dialogs, /切换对外版本/);
+ assert.match(views, /Host 能力/);
+ assert.match(views, /service\.capabilities/);
+ assert.match(consoleModel, /TextDecoder\("utf-8", \{ fatal: true \}\)/);
+ assert.match(dialogs, /outputFormat\.automatic/);
assert.match(apiClient, /\/api\/v1\/deployments/);
assert.match(apiClient, /capabilities: BackendCapability\[\]/);
assert.match(router, /createRouter/);