feat: PTY kubectl终端 + 侧边栏对齐修复 + 构建缓存清理

**kubectl WebShell 修复:**
- 引入 github.com/creack/pty 库实现真正的伪终端
- 替换 bash --noediting + TERM=dumb 方案为 PTY-backed bash
- 支持 xterm-256color、窗口自适应、readline 交互
- 修复键盘输入无响应问题

**侧边栏优化:**
- 独立菜单项高度从 py-1.5 改为 h-8,与父菜单对齐
- 独立菜单图标从 h-3 w-3 改为 h-3.5 w-3.5,统一 14px

**构建优化:**
- run.sh docker up 增加 docker image prune -f 清理悬空镜像
- 每次部署自动清理 builder cache + dangling images

**认证修复(历史):**
- Token 存储:Zustand + localStorage 双写,http.ts 双源 fallback
- YamlViewer/DeploymentDetail token 来源统一为 Zustand store
This commit is contained in:
attacker committed 2026-06-19 11:22:45 +00:00
1 parent cac1414917
commit a3cd0d3910
29 files changed
+2954 -180

No files matched your search

@@ -16,6 +16,7 @@ import {
} from 'lucide-react';
import { kubernetesApi } from '@/services/api';
import { useToast } from '@/hooks/use-toast';
import { useUserStore } from '@/store';
interface DeploymentDetailProps {
clusterCode: string;
@@ -69,10 +70,9 @@ export function DeploymentDetail({ clusterCode, namespace, deploymentName, onBac
);
setPods(depPods);
// Filter HPAs targeting this deployment
// Filter HPAs targeting this deployment (flattened data)
const depHpas = allHpas.filter((h: any) => {
const ref = h.spec?.scaleTargetRef;
return ref?.name === deploymentName && h.metadata?.namespace === namespace;
return h.target_name === deploymentName && h.namespace === namespace;
});
setHpas(depHpas);
} catch (e) {
@@ -134,9 +134,9 @@ export function DeploymentDetail({ clusterCode, namespace, deploymentName, onBac
const handleHpaToggle = async (hpa: any) => {
// Not direct toggle — delete HPA
if (!confirm(`确定删除 HPA "${hpa.metadata?.name}"?`)) return;
if (!confirm(`确定删除 HPA "${hpa.name}"?`)) return;
try {
await kubernetesApi.deleteHPA(clusterCode, namespace, hpa.metadata?.name);
await kubernetesApi.deleteHPA(clusterCode, namespace, hpa.name);
toast({ title: 'HPA 已删除' });
loadData();
} catch (e: any) {
@@ -406,7 +406,7 @@ export function DeploymentDetail({ clusterCode, namespace, deploymentName, onBac
onClick={async () => {
setLogs('');
// Stream mode
const token = localStorage.getItem('token') || '';
const token = useUserStore.getState().token || '';
const url = `${window.location.protocol}//${window.location.host}/api/v1/kubernetes/${clusterCode}/namespace/${namespace}/pods/${logPod}/logs?tail=${logTail}&stream=true`;
try {
const resp = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
@@ -471,18 +471,18 @@ export function DeploymentDetail({ clusterCode, namespace, deploymentName, onBac
</TableHeader>
<TableBody>
{hpas.map((hpa: any) => {
const spec = hpa.spec || {};
const status = hpa.status || {};
const metrics = (spec.metrics || []).map((m: any) => {
if (m.resource) return `${m.resource.name}: ${m.resource.target.averageUtilization}%`;
return m.type;
}).join(', ');
const cpu = hpa.cpu_percent || 0;
const mem = hpa.mem_percent || 0;
const metricsParts = [];
if (cpu) metricsParts.push(`CPU: ${cpu}%`);
if (mem) metricsParts.push(`MEM: ${mem}%`);
const metrics = metricsParts.join(', ') || '-';
return (
<TableRow key={hpa.metadata?.name} className="border-slate-800 hover:bg-slate-800/30">
<TableCell className="font-medium text-slate-200">{hpa.metadata?.name}</TableCell>
<TableCell className="text-slate-300">{spec.minReplicas} / {spec.maxReplicas}</TableCell>
<TableCell className="text-sm text-slate-400">{metrics || '-'}</TableCell>
<TableCell className="text-slate-300">{status.currentReplicas || 0} / {status.desiredReplicas || 0}</TableCell>
<TableRow key={hpa.name} className="border-slate-800 hover:bg-slate-800/30">
<TableCell className="font-medium text-slate-200">{hpa.name}</TableCell>
<TableCell className="text-slate-300">{hpa.min_replicas} / {hpa.max_replicas}</TableCell>
<TableCell className="text-sm text-slate-400">{metrics}</TableCell>
<TableCell className="text-slate-300">{hpa.current_replicas || 0}</TableCell>
<TableCell>
<Button variant="ghost" size="icon" className="h-7 w-7 text-red-400 hover:text-red-300"
onClick={() => handleHpaToggle(hpa)} title="删除 HPA">