diff --git a/src/App.tsx b/src/App.tsx index f966ddc..ec50883 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,8 @@ import { useReducer, useState } from "react"; import { SessionQueryIntent, SessionTable } from "./SessionTable"; +import { demoSessions } from "./protocol/fixtures"; import { initializeSessionTableStates, updateSessionTableState } from "./sessionTableState"; +import { toSessionRow } from "./sessionPresentation"; import { createInitialWorkspaceState, selectActiveTab, @@ -8,12 +10,8 @@ import { workspaceReducer, } from "./workspace"; -const sessions = [ - { id: "NW-042", status: "ONLINE", host: "ws-fin-042", user: "nora.chen", os: "Windows 11", address: "10.42.8.17", seen: "14s ago", tags: ["finance", "priority"] }, - { id: "NW-038", status: "ONLINE", host: "srv-app-03", user: "svc.deploy", os: "Ubuntu 24.04", address: "10.42.3.8", seen: "51s ago", tags: ["server"] }, - { id: "NW-031", status: "IDLE", host: "mac-design-07", user: "alex.k", os: "macOS 15.6", address: "10.42.12.27", seen: "8m ago", tags: ["design"] }, - { id: "NW-019", status: "OFFLINE", host: "ws-ops-019", user: "operator", os: "Windows 10", address: "10.42.6.91", seen: "2h ago", tags: ["legacy"] }, -]; +const fixtureNow = new Date("2026-08-03T19:12:02Z"); +const sessions = demoSessions.map((session) => toSessionRow(session, fixtureNow)); export function App() { const [workspace, dispatch] = useReducer(workspaceReducer, undefined, createInitialWorkspaceState); diff --git a/src/protocol/fixtures/index.ts b/src/protocol/fixtures/index.ts index d29ac73..d23034e 100644 --- a/src/protocol/fixtures/index.ts +++ b/src/protocol/fixtures/index.ts @@ -1,18 +1,43 @@ -import type { Session } from "../model"; +import type { CapabilityId, ProjectId, ServerId, Session, SessionId, Timestamp } from "../model"; -/** UI 迁移期可直接使用的最小 Session fixture。 */ -export const demoSession = { - server_id: "demo-server", - project_id: "demo-project", - session_id: "demo-session", - name: "演示会话", - state: "ONLINE", - hostname: "demo-host", - username: "operator", - os: "linux", - architecture: "amd64", - first_seen_at: "2026-08-03T00:00:00Z", - last_active_at: "2026-08-03T00:00:05Z", - tags: ["fixture"], - capabilities: ["session.overview"], -} as unknown as Session; +const serverId = "atlas" as ServerId; +const projectId = "northwind" as ProjectId; +const overview = "session.overview" as CapabilityId; + +interface DemoSessionInput { + readonly session_id: string; + readonly name: string; + readonly state: Session["state"]; + readonly hostname?: string; + readonly username?: string; + readonly os?: string; + readonly architecture?: string; + readonly source_address?: string; + readonly first_seen_at: string; + readonly last_active_at: string; + readonly tags: readonly string[]; +} + +function session( + value: DemoSessionInput, +): Session { + return { + ...value, + server_id: serverId, + project_id: projectId, + session_id: value.session_id as SessionId, + first_seen_at: value.first_seen_at as Timestamp, + last_active_at: value.last_active_at as Timestamp, + capabilities: [overview], + }; +} + +/** 界面只通过展示适配器消费的标准协议 Session fixture。 */ +export const demoSessions: readonly Session[] = [ + session({ session_id: "NW-042", name: "NW-042", state: "ONLINE", hostname: "ws-fin-042", username: "nora.chen", os: "Windows 11", architecture: "amd64", source_address: "10.42.8.17", first_seen_at: "2026-08-01T08:00:00Z", last_active_at: "2026-08-03T19:11:48Z", tags: ["finance", "priority"] }), + session({ session_id: "NW-038", name: "NW-038", state: "ONLINE", hostname: "srv-app-03", username: "svc.deploy", os: "Ubuntu 24.04", architecture: "amd64", source_address: "10.42.3.8", first_seen_at: "2026-07-28T10:30:00Z", last_active_at: "2026-08-03T19:11:11Z", tags: ["server"] }), + session({ session_id: "NW-031", name: "NW-031", state: "DORMANT", hostname: "mac-design-07", username: "alex.k", os: "macOS 15.6", architecture: "arm64", source_address: "10.42.12.27", first_seen_at: "2026-07-22T09:15:00Z", last_active_at: "2026-08-03T19:04:00Z", tags: ["design"] }), + session({ session_id: "NW-019", name: "NW-019", state: "OFFLINE", hostname: "ws-ops-019", username: "operator", os: "Windows 10", architecture: "amd64", source_address: "10.42.6.91", first_seen_at: "2026-07-10T14:00:00Z", last_active_at: "2026-08-03T17:12:00Z", tags: ["legacy"] }), +]; + +export const demoSession = demoSessions[0]; diff --git a/src/protocol/model.ts b/src/protocol/model.ts index 82e2cae..0e6783d 100644 --- a/src/protocol/model.ts +++ b/src/protocol/model.ts @@ -86,6 +86,7 @@ export interface Session extends SessionContext, Extensible { readonly username?: string; readonly os?: string; readonly architecture?: string; + readonly source_address?: string; readonly first_seen_at: Timestamp; readonly last_active_at: Timestamp; readonly tags: readonly string[]; diff --git a/src/sessionPresentation.test.ts b/src/sessionPresentation.test.ts new file mode 100644 index 0000000..3098b12 --- /dev/null +++ b/src/sessionPresentation.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from "vitest"; +import { demoSessions } from "./protocol/fixtures"; +import { toSessionRow } from "./sessionPresentation"; + +describe("toSessionRow", () => { + it("将协议 Session 转换为稳定的表格展示值", () => { + expect(toSessionRow(demoSessions[0], new Date("2026-08-03T19:12:02Z"))).toEqual({ + id: "NW-042", + status: "ONLINE", + host: "ws-fin-042", + user: "nora.chen", + os: "Windows 11 / amd64", + address: "10.42.8.17", + seen: "14s ago", + tags: ["finance", "priority"], + }); + }); +}); diff --git a/src/sessionPresentation.ts b/src/sessionPresentation.ts new file mode 100644 index 0000000..294a2a9 --- /dev/null +++ b/src/sessionPresentation.ts @@ -0,0 +1,24 @@ +import type { Session } from "./protocol/model"; +import type { SessionRow } from "./SessionTable"; + +/** 将服务端权威模型转换为表格展示值,不在 UI 复制业务模型。 */ +export function toSessionRow(session: Session, now: Date = new Date()): SessionRow { + return { + id: session.session_id, + status: session.state, + host: session.hostname ?? session.name, + user: session.username ?? "—", + os: [session.os, session.architecture].filter(Boolean).join(" / ") || "未知", + address: session.source_address ?? "—", + seen: formatRelativeTime(session.last_active_at, now), + tags: [...session.tags], + }; +} + +function formatRelativeTime(timestamp: string, now: Date): string { + const elapsedSeconds = Math.max(0, Math.floor((now.getTime() - Date.parse(timestamp)) / 1000)); + if (elapsedSeconds < 60) return `${elapsedSeconds}s ago`; + if (elapsedSeconds < 3600) return `${Math.floor(elapsedSeconds / 60)}m ago`; + if (elapsedSeconds < 86400) return `${Math.floor(elapsedSeconds / 3600)}h ago`; + return `${Math.floor(elapsedSeconds / 86400)}d ago`; +}