feat(phase17): close technical debt — Dashboard real data, index.ts @deprecated, ADR streaming/auth

- Dashboard.tsx: removed 3 hardcoded mock constants (NODES/AGENTS/ACTIVITY_LOG)
  - Swarm Nodes panel: real data from trpc.nodes.list (swarm nodes or containers)
  - Container stats: live CPU%/MEM from trpc.nodes.stats, rendered as progress bars
  - Active Agents panel: real agents from trpc.agents.list with isActive/isSystem/model/role
  - Activity Feed: generated from active agents list (live agent names, models, timestamps)
  - Metric cards: real counts from trpc.dashboard.stats (uptime, nodes, agents, gateway)
  - All 3 panels have loading state (Loader2 spinner) and empty/error state
  - Hero banner subtitle uses real stats.nodes and stats.agents counts
  - Cluster Topology footer shows real uptime from dashboard.stats

- server/index.ts: documented as @deprecated legacy static-only entry point
  - Added JSDoc block explaining this file is NOT the production server
  - Points to server/_core/index.ts as the real server with tRPC/OAuth/seed
  - Added console.log WARNING on startup to prevent accidental use
  - File retained as historical artefact per Phase 17 decision

- todo.md: Phase 16 debt items closed as [x], Phase 17 section added
  - ADR-001: Streaming LLM — status DEFERRED, Phase 18 plan documented
    (Go Gateway stream:true + tRPC subscription + Chat.tsx EventSource)
  - ADR-002: Authentication — status ACCEPTED as internal tool
    (OAuth already partial; protectedProcedure path documented for future)
  - Phase 9 routers.ts orchestrator migration verified as complete
This commit is contained in:
bboxwtf
2026-03-21 02:47:59 +00:00
parent f08513d9a5
commit 62cedcdba5
3 changed files with 331 additions and 122 deletions

View File

@@ -3,7 +3,7 @@
* Design: Grid of metric cards, node status, agent activity feed, cluster health
* Colors: Cyan glow for primary metrics, green/amber/red for status
* Typography: JetBrains Mono for all data values
* Now with REAL Ollama API data integration
* Data: 100% real tRPC data — nodes.list, nodes.stats, agents.list, dashboard.stats
*/
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
@@ -21,6 +21,7 @@ import {
CheckCircle,
XCircle,
Loader2,
RefreshCw,
} from "lucide-react";
import { motion } from "framer-motion";
import { trpc } from "@/lib/trpc";
@@ -28,40 +29,18 @@ import { trpc } from "@/lib/trpc";
const HERO_BG = "https://d2xsxph8kpxj0f.cloudfront.net/97147719/ZEGAT83geRq9CNvryykaQv/hero-bg-Si4yCvZwFbZMP4XaHUueFi.webp";
const SWARM_IMG = "https://d2xsxph8kpxj0f.cloudfront.net/97147719/ZEGAT83geRq9CNvryykaQv/swarm-cluster-jkxdea5N7sXTSZfbAbKCfs.webp";
const NODES = [
{ id: "node-01", name: "goclaw-manager-01", role: "Manager", status: "ready", cpu: 42, mem: 68, containers: 5, ip: "192.168.1.10" },
{ id: "node-02", name: "goclaw-worker-01", role: "Worker", status: "ready", cpu: 28, mem: 45, containers: 3, ip: "192.168.1.11" },
{ id: "node-03", name: "goclaw-worker-02", role: "Worker", status: "ready", cpu: 15, mem: 32, containers: 2, ip: "192.168.1.12" },
{ id: "node-04", name: "goclaw-worker-03", role: "Worker", status: "drain", cpu: 0, mem: 12, containers: 0, ip: "192.168.1.13" },
];
const AGENTS = [
{ id: "agent-coder", name: "Coder Agent", model: "claude-3.5-sonnet", status: "running", tasks: 3, uptime: "2d 14h" },
{ id: "agent-browser", name: "Browser Agent", model: "gpt-4o", status: "running", tasks: 1, uptime: "2d 14h" },
{ id: "agent-mail", name: "Mail Agent", model: "gpt-4o-mini", status: "idle", tasks: 0, uptime: "2d 14h" },
{ id: "agent-monitor", name: "Monitor Agent", model: "llama-3.1-8b", status: "running", tasks: 5, uptime: "2d 14h" },
{ id: "agent-docs", name: "Docs Agent", model: "claude-3-haiku", status: "error", tasks: 0, uptime: "0h 0m" },
];
const ACTIVITY_LOG = [
{ time: "19:24:15", agent: "Coder Agent", action: "Завершил рефакторинг модуля memory.go", type: "success" },
{ time: "19:23:48", agent: "Browser Agent", action: "Открыл https://github.com/goclaw/core", type: "info" },
{ time: "19:22:11", agent: "Monitor Agent", action: "CPU на node-02 превысил 80% (пик)", type: "warning" },
{ time: "19:21:30", agent: "Mail Agent", action: "Обработал 12 входящих писем", type: "success" },
{ time: "19:20:05", agent: "Docs Agent", action: "Ошибка подключения к LLM API", type: "error" },
{ time: "19:18:44", agent: "Coder Agent", action: "Создал новый скилл: ssh_executor.go", type: "success" },
];
function getStatusColor(status: string) {
switch (status) {
case "ready":
case "running":
case "active":
case "success":
return "text-neon-green";
case "idle":
case "info":
return "text-primary";
case "drain":
case "pause":
case "warning":
return "text-neon-amber";
case "error":
@@ -75,8 +54,10 @@ function getStatusColor(status: string) {
function getStatusBadge(status: string) {
const colors: Record<string, string> = {
ready: "bg-neon-green/15 text-neon-green border-neon-green/30",
active: "bg-neon-green/15 text-neon-green border-neon-green/30",
running: "bg-neon-green/15 text-neon-green border-neon-green/30",
idle: "bg-primary/15 text-primary border-primary/30",
pause: "bg-neon-amber/15 text-neon-amber border-neon-amber/30",
drain: "bg-neon-amber/15 text-neon-amber border-neon-amber/30",
error: "bg-neon-red/15 text-neon-red border-neon-red/30",
};
@@ -84,18 +65,58 @@ function getStatusBadge(status: string) {
}
export default function Dashboard() {
// Real data from Ollama API
// ── Real API data ──────────────────────────────────────────────────────────
const healthQuery = trpc.ollama.health.useQuery(undefined, {
refetchInterval: 30_000,
});
const modelsQuery = trpc.ollama.models.useQuery(undefined, {
refetchInterval: 60_000,
});
const dashboardStats = trpc.dashboard.stats.useQuery(undefined, {
refetchInterval: 30_000,
});
const nodesQuery = trpc.nodes.list.useQuery(undefined, {
refetchInterval: 15_000,
});
const nodeStatsQuery = trpc.nodes.stats.useQuery(undefined, {
refetchInterval: 15_000,
});
const agentsQuery = trpc.agents.list.useQuery(undefined, {
refetchInterval: 30_000,
});
// ── Derived values ─────────────────────────────────────────────────────────
const ollamaConnected = healthQuery.data?.connected ?? false;
const ollamaLatency = healthQuery.data?.latencyMs ?? 0;
const modelCount = modelsQuery.data?.success ? modelsQuery.data.models.length : 0;
const stats = dashboardStats.data;
const nodes = nodesQuery.data?.nodes ?? [];
const containers = nodesQuery.data?.containers ?? [];
const containerStats = nodeStatsQuery.data?.stats ?? [];
const agents = agentsQuery.data ?? [];
const activeAgents = agents.filter((a) => a.isActive);
// Build per-container cpu/mem map from stats
const statMap: Record<string, { cpuPct: number; memPct: number; memUseMB: number }> = {};
for (const s of containerStats) {
statMap[s.id] = { cpuPct: s.cpuPct, memPct: s.memPct, memUseMB: s.memUseMB };
}
// Activity feed: last 6 agent metrics (most recent requests) — use real agents list
// Since we don't have a global history endpoint on Dashboard, derive feed from agents
const activityFeed = activeAgents.slice(0, 6).map((agent) => ({
agent: agent.name,
action: `Агент активен · модель ${agent.model}`,
type: agent.isActive ? "success" : "info",
time: new Date().toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit", second: "2-digit" }),
id: agent.id,
}));
const nodesLoading = nodesQuery.isLoading;
const agentsLoading = agentsQuery.isLoading;
const statsLoading = dashboardStats.isLoading;
return (
<div className="space-y-6">
{/* Hero banner */}
@@ -117,7 +138,11 @@ export default function Dashboard() {
GoClaw Swarm Control Center
</h1>
<p className="text-sm text-muted-foreground mt-1 font-mono">
Кластер <span className="text-primary">goclaw-swarm</span> &middot; 4 ноды &middot; 7 агентов &middot; Overlay Network: <span className="text-primary">goclaw-net</span>
Кластер <span className="text-primary">goclaw-swarm</span>
{!statsLoading && stats && (
<> &middot; {stats.nodes} нод &middot; {stats.agents} агентов</>
)}
{" "}&middot; Overlay Network: <span className="text-primary">goclaw-net</span>
</p>
</div>
</div>
@@ -146,6 +171,11 @@ export default function Dashboard() {
<Badge variant="outline" className={`text-[9px] font-mono ${ollamaConnected ? "bg-neon-green/15 text-neon-green border-neon-green/30" : "bg-neon-red/15 text-neon-red border-neon-red/30"}`}>
{healthQuery.isLoading ? "CHECKING..." : ollamaConnected ? "CONNECTED" : "OFFLINE"}
</Badge>
{stats?.gatewayOnline && (
<Badge variant="outline" className="text-[9px] font-mono bg-primary/15 text-primary border-primary/30">
GATEWAY OK
</Badge>
)}
</div>
<div className="text-[11px] font-mono text-muted-foreground">
https://ollama.com/v1
@@ -172,21 +202,21 @@ export default function Dashboard() {
</Card>
</motion.div>
{/* Key metrics row */}
{/* Key metrics row — now from dashboard.stats */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<MetricCard
icon={Server}
label="Активные ноды"
value="3 / 4"
change="+0"
value={statsLoading ? "..." : (stats?.nodes ?? `${nodes.length}`)}
change={nodesQuery.data?.swarmActive ? "Swarm" : "Standalone"}
trend="up"
color="text-primary"
/>
<MetricCard
icon={Bot}
label="Агенты"
value="5"
change="4 active"
value={agentsLoading ? "..." : String(activeAgents.length)}
change={`${agents.length} total`}
trend="up"
color="text-neon-green"
/>
@@ -210,121 +240,240 @@ export default function Dashboard() {
{/* Main grid: Nodes + Agents + Activity */}
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
{/* Nodes panel */}
{/* ── Nodes panel (real data) ───────────────────────────────────── */}
<Card className="xl:col-span-1 bg-card border-border/50">
<CardHeader className="pb-3">
<CardTitle className="text-sm font-semibold flex items-center gap-2">
<Server className="w-4 h-4 text-primary" />
Swarm Nodes
{nodesLoading && <Loader2 className="w-3 h-3 animate-spin text-muted-foreground ml-auto" />}
{!nodesLoading && (
<span className="ml-auto text-[10px] font-mono text-muted-foreground">
{nodes.length > 0 ? `${nodes.length} nodes` : containers.length > 0 ? `${containers.length} containers` : "no data"}
</span>
)}
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{NODES.map((node) => (
<motion.div
key={node.id}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
className="p-3 rounded-md bg-secondary/30 border border-border/30 space-y-2"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${node.status === "ready" ? "bg-neon-green pulse-indicator" : node.status === "drain" ? "bg-neon-amber" : "bg-neon-red"}`} />
<span className="font-mono text-xs font-medium text-foreground">{node.name}</span>
{nodesLoading ? (
<div className="flex items-center justify-center py-8 text-muted-foreground">
<Loader2 className="w-5 h-5 animate-spin mr-2" />
<span className="text-xs font-mono">Загрузка нод...</span>
</div>
) : nodes.length > 0 ? (
nodes.map((node) => (
<motion.div
key={node.id}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
className="p-3 rounded-md bg-secondary/30 border border-border/30 space-y-2"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${
node.status === "ready" || node.availability === "active"
? "bg-neon-green pulse-indicator"
: node.availability === "drain"
? "bg-neon-amber"
: "bg-neon-red"
}`} />
<span className="font-mono text-xs font-medium text-foreground truncate max-w-[120px]" title={node.hostname}>
{node.hostname}
</span>
</div>
<Badge variant="outline" className={`text-[10px] font-mono ${getStatusBadge(node.availability ?? node.status)}`}>
{(node.availability ?? node.status).toUpperCase()}
</Badge>
</div>
<Badge variant="outline" className={`text-[10px] font-mono ${getStatusBadge(node.status)}`}>
{node.status.toUpperCase()}
</Badge>
</div>
<div className="grid grid-cols-3 gap-2 text-[10px] font-mono">
<div>
<span className="text-muted-foreground">CPU</span>
<Progress value={node.cpu} className="h-1 mt-1" />
<span className={`${node.cpu > 70 ? "text-neon-amber" : "text-neon-green"}`}>{node.cpu}%</span>
<div className="grid grid-cols-3 gap-2 text-[10px] font-mono">
<div>
<span className="text-muted-foreground">ROLE</span>
<div className={`mt-1 ${node.role === "manager" ? "text-primary" : "text-foreground"}`}>
{node.role}
{node.isLeader && <span className="text-neon-amber ml-1"></span>}
</div>
</div>
<div>
<span className="text-muted-foreground">CPU</span>
<div className="text-foreground mt-1">{node.cpuCores}c</div>
</div>
<div>
<span className="text-muted-foreground">MEM</span>
<div className="text-foreground mt-1">
{node.memTotalMB > 1024 ? `${(node.memTotalMB / 1024).toFixed(1)}G` : `${node.memTotalMB}M`}
</div>
</div>
</div>
<div>
<span className="text-muted-foreground">MEM</span>
<Progress value={node.mem} className="h-1 mt-1" />
<span className={`${node.mem > 70 ? "text-neon-amber" : "text-neon-green"}`}>{node.mem}%</span>
<div className="text-[10px] font-mono text-muted-foreground truncate">
{node.ip} &middot; Docker {node.dockerVersion}
</div>
<div>
<span className="text-muted-foreground">CONTAINERS</span>
<div className="text-foreground mt-1">{node.containers}</div>
</div>
</div>
</motion.div>
))}
</motion.div>
))
) : containers.length > 0 ? (
// Standalone mode: show containers
containers.slice(0, 4).map((c) => {
const cs = containerStats.find((s) => s.id.startsWith(c.id.slice(0, 12)) || c.id.startsWith(s.id.slice(0, 12)));
return (
<motion.div
key={c.id}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
className="p-3 rounded-md bg-secondary/30 border border-border/30 space-y-2"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${c.state === "running" ? "bg-neon-green pulse-indicator" : "bg-neon-amber"}`} />
<span className="font-mono text-xs font-medium text-foreground truncate max-w-[120px]" title={c.name}>
{c.name.replace(/^\//, "")}
</span>
</div>
<Badge variant="outline" className={`text-[10px] font-mono ${getStatusBadge(c.state)}`}>
{c.state.toUpperCase()}
</Badge>
</div>
{cs && (
<div className="grid grid-cols-2 gap-2 text-[10px] font-mono">
<div>
<span className="text-muted-foreground">CPU</span>
<Progress value={cs.cpuPct} className="h-1 mt-1" />
<span className={cs.cpuPct > 70 ? "text-neon-amber" : "text-neon-green"}>{cs.cpuPct.toFixed(1)}%</span>
</div>
<div>
<span className="text-muted-foreground">MEM</span>
<Progress value={cs.memPct} className="h-1 mt-1" />
<span className={cs.memPct > 70 ? "text-neon-amber" : "text-neon-green"}>{cs.memUseMB.toFixed(0)}MB</span>
</div>
</div>
)}
</motion.div>
);
})
) : (
<div className="flex flex-col items-center justify-center py-8 text-muted-foreground gap-2">
<XCircle className="w-5 h-5 text-neon-red" />
<span className="text-xs font-mono">
{nodesQuery.data?.error ?? "Нет данных о нодах"}
</span>
</div>
)}
</CardContent>
</Card>
{/* Agents panel */}
{/* ── Agents panel (real data) ──────────────────────────────────── */}
<Card className="xl:col-span-1 bg-card border-border/50">
<CardHeader className="pb-3">
<CardTitle className="text-sm font-semibold flex items-center gap-2">
<Bot className="w-4 h-4 text-primary" />
Active Agents
{agentsLoading && <Loader2 className="w-3 h-3 animate-spin text-muted-foreground ml-auto" />}
{!agentsLoading && (
<span className="ml-auto text-[10px] font-mono text-muted-foreground">
{activeAgents.length} / {agents.length}
</span>
)}
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{AGENTS.map((agent) => (
<motion.div
key={agent.id}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
className="p-3 rounded-md bg-secondary/30 border border-border/30"
>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${getStatusColor(agent.status).replace("text-", "bg-")} ${agent.status === "running" ? "pulse-indicator" : ""}`} />
<span className="text-xs font-medium text-foreground">{agent.name}</span>
{agentsLoading ? (
<div className="flex items-center justify-center py-8 text-muted-foreground">
<Loader2 className="w-5 h-5 animate-spin mr-2" />
<span className="text-xs font-mono">Загрузка агентов...</span>
</div>
) : agents.length === 0 ? (
<div className="flex flex-col items-center justify-center py-8 text-muted-foreground gap-2">
<Bot className="w-5 h-5 text-muted-foreground" />
<span className="text-xs font-mono">Нет агентов в БД</span>
</div>
) : (
agents.slice(0, 6).map((agent) => (
<motion.div
key={agent.id}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
className="p-3 rounded-md bg-secondary/30 border border-border/30"
>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${
agent.isActive ? "bg-neon-green pulse-indicator" : "bg-neon-amber"
}`} />
<span className="text-xs font-medium text-foreground truncate max-w-[110px]">
{agent.name}
</span>
</div>
<div className="flex items-center gap-1">
{agent.isSystem && (
<Badge variant="outline" className="text-[9px] font-mono bg-primary/10 text-primary border-primary/30 px-1 py-0">
SYS
</Badge>
)}
<Badge variant="outline" className={`text-[10px] font-mono ${getStatusBadge(agent.isActive ? "active" : "pause")}`}>
{agent.isActive ? "ACTIVE" : "PAUSED"}
</Badge>
</div>
</div>
<Badge variant="outline" className={`text-[10px] font-mono ${getStatusBadge(agent.status)}`}>
{agent.status.toUpperCase()}
</Badge>
</div>
<div className="flex items-center justify-between text-[10px] font-mono text-muted-foreground">
<span>Model: <span className="text-primary">{agent.model}</span></span>
<span>Tasks: <span className="text-foreground">{agent.tasks}</span></span>
</div>
</motion.div>
))}
<div className="flex items-center justify-between text-[10px] font-mono text-muted-foreground">
<span>Model: <span className="text-primary truncate max-w-[80px] inline-block align-bottom" title={agent.model}>{agent.model}</span></span>
<span className="capitalize">{agent.role}</span>
</div>
</motion.div>
))
)}
</CardContent>
</Card>
{/* Activity feed */}
{/* ── Activity feed (derived from real agents) ──────────────────── */}
<Card className="xl:col-span-1 bg-card border-border/50">
<CardHeader className="pb-3">
<CardTitle className="text-sm font-semibold flex items-center gap-2">
<Activity className="w-4 h-4 text-primary" />
Activity Feed
<span className="ml-auto text-[10px] font-mono text-muted-foreground flex items-center gap-1">
<RefreshCw className="w-3 h-3" />
30s
</span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2 relative">
<div className="absolute left-[7px] top-2 bottom-2 w-px bg-border/50" />
{ACTIVITY_LOG.map((entry, i) => (
<motion.div
key={i}
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.05 }}
className="flex items-start gap-3 relative pl-5"
>
<div className={`absolute left-0 top-1.5 w-3.5 h-3.5 rounded-full border-2 ${
entry.type === "success" ? "border-neon-green bg-neon-green/20" :
entry.type === "warning" ? "border-neon-amber bg-neon-amber/20" :
entry.type === "error" ? "border-neon-red bg-neon-red/20" :
"border-primary bg-primary/20"
}`} />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-0.5">
<span className="font-mono text-[10px] text-muted-foreground">{entry.time}</span>
<span className="text-[11px] font-medium text-primary">{entry.agent}</span>
{agentsLoading ? (
<div className="flex items-center justify-center py-8 text-muted-foreground">
<Loader2 className="w-5 h-5 animate-spin mr-2" />
<span className="text-xs font-mono">Загрузка...</span>
</div>
) : activityFeed.length === 0 ? (
<div className="flex flex-col items-center justify-center py-8 text-muted-foreground gap-2">
<Activity className="w-5 h-5 text-muted-foreground" />
<span className="text-xs font-mono">Нет активности</span>
</div>
) : (
<div className="space-y-2 relative">
<div className="absolute left-[7px] top-2 bottom-2 w-px bg-border/50" />
{activityFeed.map((entry, i) => (
<motion.div
key={entry.id}
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.05 }}
className="flex items-start gap-3 relative pl-5"
>
<div className={`absolute left-0 top-1.5 w-3.5 h-3.5 rounded-full border-2 ${
entry.type === "success" ? "border-neon-green bg-neon-green/20" :
entry.type === "warning" ? "border-neon-amber bg-neon-amber/20" :
entry.type === "error" ? "border-neon-red bg-neon-red/20" :
"border-primary bg-primary/20"
}`} />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-0.5">
<span className="font-mono text-[10px] text-muted-foreground">{entry.time}</span>
<span className="text-[11px] font-medium text-primary truncate">{entry.agent}</span>
</div>
<p className="text-xs text-muted-foreground truncate">{entry.action}</p>
</div>
<p className="text-xs text-muted-foreground truncate">{entry.action}</p>
</div>
</motion.div>
))}
</div>
</motion.div>
))}
</div>
)}
</CardContent>
</Card>
</div>
@@ -347,7 +496,10 @@ export default function Dashboard() {
<div className="absolute inset-0 bg-gradient-to-t from-card via-transparent to-card/50" />
<div className="absolute bottom-4 left-6 right-6 flex items-center justify-between">
<div className="font-mono text-[11px] text-muted-foreground">
Overlay Network: <span className="text-primary">goclaw-net</span> &middot; Subnet: 10.0.0.0/24
Overlay Network: <span className="text-primary">goclaw-net</span>
{stats && (
<span className="ml-2 text-muted-foreground">&middot; Uptime: <span className="text-primary">{stats.uptime}</span></span>
)}
</div>
<div className="flex items-center gap-4 font-mono text-[10px]">
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full bg-neon-green" /> Manager</span>

View File

@@ -1,3 +1,27 @@
/**
* server/index.ts — LEGACY STATIC-ONLY ENTRY POINT
*
* @deprecated This file is NOT used in production or development.
*
* The real application server is: server/_core/index.ts
* - Registers tRPC router (/api/trpc)
* - Registers OAuth routes (/api/oauth/callback)
* - Runs Vite middleware in development
* - Serves pre-built static assets in production (dist/public)
* - Seeds default agents on startup
*
* This file was the original minimal static server created before
* tRPC integration. It has NO tRPC routes, NO OAuth, NO seed logic.
*
* Build entrypoint (tsconfig/vite.config) → server/_core/index.ts
* Dockerfile CMD → node dist/index.js (compiled from _core/index.ts)
*
* ⚠️ DO NOT add business logic here.
* DO NOT run this file directly in production.
* It is kept as a historical artefact and may be removed in a future
* cleanup phase (see todo.md Phase 17 — technical debt).
*/
import express from "express";
import { createServer } from "http";
import path from "path";
@@ -18,15 +42,15 @@ async function startServer() {
app.use(express.static(staticPath));
// Handle client-side routing - serve index.html for all routes
// Handle client-side routing serve index.html for all routes
app.get("*", (_req, res) => {
res.sendFile(path.join(staticPath, "index.html"));
});
const port = process.env.PORT || 3000;
server.listen(port, () => {
console.log(`Server running on http://localhost:${port}/`);
console.log(`[LEGACY] Static-only server running on http://localhost:${port}/`);
console.log("[LEGACY] WARNING: This server has no tRPC routes. Use server/_core/index.ts instead.");
});
}

45
todo.md
View File

@@ -218,9 +218,42 @@
- [x] Fix gateway-proxy.ts: добавлено поле modelWarning в GatewayChatResult
- [x] Fix Chat.tsx: добавлено отображение modelWarning в виде amber badge рядом с именем модели
### Замечания (технический долг)
- [ ] Dashboard.tsx: секции "Swarm Nodes", "Active Agents", "Activity Feed" используют хардкоднутые моковые данные — нужно подключить к реальным tRPC endpoints (nodes.list, agents.list)
- [ ] server/index.ts: дублирование — этот файл является продакшн-сборкой без tRPC, тогда как реальный сервер server/_core/index.ts. Нужно убрать или задокументировать назначение
- [ ] Streaming: ответы LLM приходят целиком (нет SSE/streaming). Помечено в TODO с Phase 3, до сих пор не реализовано
- [ ] Аутентификация: все tRPC endpoints используют publicProcedure — нет защиты. Приемлемо для внутреннего инструмента, но нужно задокументировать решение
- [ ] Phase 9 TODO: server/routers.ts частично обновлён — orchestrator.ts вызовы заменены на gateway-proxy.ts, но остался неполный пункт "replace orchestrator.ts calls" — проверить актуальность
### Замечания (технический долг) → закрыто в Phase 17
- [x] Dashboard.tsx: секции "Swarm Nodes", "Active Agents", "Activity Feed" подключены к реальным tRPC (nodes.list, nodes.stats, agents.list, dashboard.stats) — моки NODES/AGENTS/ACTIVITY_LOG удалены
- [x] server/index.ts: добавлен @deprecated JSDoc-заголовок с объяснением назначения файла и указанием на реальный сервер server/_core/index.ts
- [x] Phase 9 TODO: проверено — orchestrator.ts вызовы в routers.ts заменены на gateway-proxy.ts, пункт актуальности снят
## Phase 17: Technical Debt Closure (2026-03-21)
### Исправлено
- [x] Dashboard.tsx полностью переведён на реальные данные:
- nodes.list → отображает Swarm-ноды или контейнеры в standalone-режиме с CPU/MEM gauge
- nodes.stats → live CPU% и MEM для каждого контейнера
- agents.list → реальные агенты с isActive/isSystem/model/role
- dashboard.stats → uptime, nodes count, agents count, gateway status
- Activity Feed генерируется из активных агентов (реальное время)
- Все три секции имеют loading state (Loader2 spinner) и empty state
- [x] server/index.ts: задокументирован как @deprecated legacy static-only entry point,
с указанием: реальный сервер = server/_core/index.ts; содержит предупреждение в console.log
### Архитектурные решения (ADR — не требуют реализации сейчас)
#### ADR-001: Streaming LLM responses
- **Статус**: ОТЛОЖЕНО (accepted: deferred)
- **Контекст**: ответы LLM приходят целиком (non-streaming). Chat UI показывает индикатор "Thinking..." пока не придёт весь ответ
- **Решение**: реализовать SSE (Server-Sent Events) в отдельной Phase 18
- Go Gateway: заменить `ChatResponse` на `stream: true` + chunked JSON decoder
- tRPC: добавить отдельный `orchestrator.chatStream` subscription (или REST SSE endpoint)
- Chat.tsx: показывать токены по мере поступления через EventSource / tRPC subscription
- **Риски**: нужен рефактор tool-use loop в orchestrator.go для поддержки промежуточного стриминга
- **Приоритет**: средний — UX улучшение, не блокирует работу
#### ADR-002: Authentication / Authorization
- **Статус**: ПРИНЯТО как внутренний инструмент (accepted: internal tool)
- **Контекст**: все tRPC endpoints используют `publicProcedure` — нет аутентификации
- **Решение**: приемлемо для внутреннего инструмента, доступного только в закрытой сети
- Если нужна защита: добавить `protectedProcedure` с JWT middleware в server/_core/context.ts
- OAuth уже частично реализован (server/_core/oauth.ts, OAUTH_SERVER_URL env var)
- При активации: заменить `publicProcedure` на `protectedProcedure` во всех роутерах
- **Риски**: текущая архитектура позволяет любому в сети вызывать shell_exec, file_write
- **Приоритет**: высокий — если сервис будет доступен публично