diff --git a/backend/internal/model/models.go b/backend/internal/model/models.go index ef22ef2..6ebcb3d 100644 --- a/backend/internal/model/models.go +++ b/backend/internal/model/models.go @@ -136,14 +136,14 @@ type ReleaseRunStep struct { ServiceName string `gorm:"size:32;index;not null" json:"service_name"` PreviousReleaseID string `gorm:"size:128;index" json:"previous_release_id"` Status string `gorm:"size:32;index;not null" json:"status"` - DeploymentID uint `gorm:"index" json:"deployment_id"` + DeploymentID *uint `gorm:"index" json:"deployment_id"` Deployment *Deployment `json:"deployment,omitempty"` ErrorMessage string `gorm:"type:text" json:"error_message"` LogExcerpt string `gorm:"type:text" json:"log_excerpt"` StartedAt *time.Time `json:"started_at"` FinishedAt *time.Time `json:"finished_at"` RollbackStatus string `gorm:"size:32;index" json:"rollback_status"` - RollbackDeploymentID uint `gorm:"index" json:"rollback_deployment_id"` + RollbackDeploymentID *uint `gorm:"index" json:"rollback_deployment_id"` RollbackDeployment *Deployment `json:"rollback_deployment,omitempty"` RollbackErrorMessage string `gorm:"type:text" json:"rollback_error_message"` RollbackLogExcerpt string `gorm:"type:text" json:"rollback_log_excerpt"` diff --git a/backend/internal/service/manager.go b/backend/internal/service/manager.go index 1f47c83..bd70ab8 100644 --- a/backend/internal/service/manager.go +++ b/backend/internal/service/manager.go @@ -455,7 +455,7 @@ func (m *Manager) ProcessReleaseRun(ctx context.Context, runID uint) error { return err } step.Status = model.DeploymentSuccess - step.DeploymentID = deployment.ID + step.DeploymentID = &deployment.ID completedSteps = append(completedSteps, step) } @@ -642,6 +642,7 @@ func (m *Manager) ProcessDeployment(ctx context.Context, deploymentID uint) erro targetFinishedAt := time.Now() if err != nil { + errorMessage := summarizeDeploymentError(err, output) _ = m.db.WithContext(ctx).Model(&model.DeploymentTarget{}). Where("id = ?", target.ID). Updates(map[string]any{ @@ -654,10 +655,10 @@ func (m *Manager) ProcessDeployment(ctx context.Context, deploymentID uint) erro Where("id = ?", deploymentID). Updates(map[string]any{ "status": model.DeploymentFailed, - "error_message": err.Error(), + "error_message": errorMessage, "finished_at": &targetFinishedAt, }).Error - return err + return errors.New(errorMessage) } updates := map[string]any{ @@ -911,6 +912,26 @@ func summarizeBuildError(err error, output string) string { return err.Error() } +func summarizeDeploymentError(err error, output string) string { + lines := strings.Split(output, "\n") + for i := len(lines) - 1; i >= 0; i-- { + line := strings.TrimSpace(lines[i]) + if line == "" || line == err.Error() { + continue + } + if line == "Traceback (most recent call last):" || strings.HasPrefix(line, "File ") { + continue + } + if strings.HasPrefix(line, "RuntimeError:") { + line = strings.TrimSpace(strings.TrimPrefix(line, "RuntimeError:")) + } + if line != "" { + return line + } + } + return err.Error() +} + func deploymentLogExcerpt(deployment *model.Deployment) string { if deployment == nil { return "" diff --git a/frontend/src/constants/operator.ts b/frontend/src/constants/operator.ts new file mode 100644 index 0000000..ec4e6d7 --- /dev/null +++ b/frontend/src/constants/operator.ts @@ -0,0 +1,3 @@ +// TODO: Replace this fallback with the authenticated user once identity is wired end-to-end, +// then restore operator-related fields in the UI. +export const defaultOperator = 'platform-admin' diff --git a/frontend/src/pages/BuildsPage.tsx b/frontend/src/pages/BuildsPage.tsx index 3918bfe..2507cd1 100644 --- a/frontend/src/pages/BuildsPage.tsx +++ b/frontend/src/pages/BuildsPage.tsx @@ -26,16 +26,15 @@ import { useEffect, useMemo, useState } from 'react' import { api } from '@/api/client' import StatusChip from '@/components/StatusChip' +import { defaultOperator } from '@/constants/operator' import type { BuildJob, CatalogService } from '@/types' type FormState = { service_name: string release_id: string branch: string - operator: string } -const defaultOperator = 'platform-admin' const activeBuildStatuses = new Set(['queued', 'running']) function formatDateTime(value?: string) { @@ -56,8 +55,7 @@ export default function BuildsPage() { const [form, setForm] = useState({ service_name: '', release_id: '', - branch: 'main', - operator: defaultOperator + branch: 'main' }) const selectedService = useMemo( @@ -77,8 +75,7 @@ export default function BuildsPage() { setForm({ service_name: first?.name ?? '', release_id: '', - branch: first?.default_branch ?? 'main', - operator: defaultOperator + branch: first?.default_branch ?? 'main' }) } @@ -141,7 +138,7 @@ export default function BuildsPage() { const createBuild = async () => { try { - const created = await api.createBuild(form) + const created = await api.createBuild({ ...form, operator: defaultOperator }) setOpen(false) resetForm(services) setMessage('构建任务已创建,已打开实时构建日志') @@ -267,11 +264,6 @@ export default function BuildsPage() { value={form.branch} onChange={(event) => setForm((prev) => ({ ...prev, branch: event.target.value }))} /> - setForm((prev) => ({ ...prev, operator: event.target.value }))} - /> 平台会先在构建机本地源码目录中自动查找匹配仓库;找到后执行 `git fetch / checkout / pull`。如果本地没有对应仓库,再自动克隆到构建工作区。 diff --git a/frontend/src/pages/DeploymentsPage.tsx b/frontend/src/pages/DeploymentsPage.tsx index 29979dd..c281484 100644 --- a/frontend/src/pages/DeploymentsPage.tsx +++ b/frontend/src/pages/DeploymentsPage.tsx @@ -1,8 +1,11 @@ import { Alert, + Box, Button, Card, CardContent, + Chip, + CircularProgress, Dialog, DialogActions, DialogContent, @@ -24,32 +27,31 @@ import { useNavigate } from 'react-router-dom' import { api } from '@/api/client' import StatusChip from '@/components/StatusChip' -import type { Deployment, ReleaseRun } from '@/types' +import { defaultOperator } from '@/constants/operator' +import type { Deployment, DeploymentTarget, ReleaseRun } from '@/types' type DeploymentFormState = { service_name: string release_id: string operation: string - operator: string } type ReleaseRunFormState = { release_id: string - operator: string } const emptyDeploymentForm: DeploymentFormState = { service_name: 'user', release_id: '', - operation: 'deploy', - operator: 'platform-admin' + operation: 'deploy' } const emptyReleaseRunForm: ReleaseRunFormState = { - release_id: '', - operator: 'platform-admin' + release_id: '' } +const activeDeploymentStatuses = new Set(['queued', 'running']) + function stepSummary(run: ReleaseRun) { return (run.steps ?? []).map((step) => ({ key: `${run.id}-${step.sequence}`, @@ -60,14 +62,29 @@ function stepSummary(run: ReleaseRun) { })) } +function formatDateTime(value?: string) { + if (!value) return '-' + return new Date(value).toLocaleString() +} + +function targetLabel(target: DeploymentTarget) { + const hostName = target.host?.name || `host-${target.host_id}` + const privateIP = target.host?.private_ip + return privateIP ? `${hostName} · ${privateIP}` : hostName +} + export default function DeploymentsPage() { const navigate = useNavigate() const [deployments, setDeployments] = useState([]) const [releaseRuns, setReleaseRuns] = useState([]) const [deploymentOpen, setDeploymentOpen] = useState(false) const [releaseRunOpen, setReleaseRunOpen] = useState(false) + const [detailOpen, setDetailOpen] = useState(false) const [deploymentForm, setDeploymentForm] = useState(emptyDeploymentForm) const [releaseRunForm, setReleaseRunForm] = useState(emptyReleaseRunForm) + const [selectedDeploymentID, setSelectedDeploymentID] = useState(null) + const [selectedDeployment, setSelectedDeployment] = useState(null) + const [detailLoading, setDetailLoading] = useState(false) const [message, setMessage] = useState('') const [error, setError] = useState('') @@ -86,7 +103,7 @@ export default function DeploymentsPage() { const createDeployment = async () => { try { - await api.createDeployment(deploymentForm) + await api.createDeployment({ ...deploymentForm, operator: defaultOperator }) setDeploymentOpen(false) setDeploymentForm(emptyDeploymentForm) setMessage('单服务部署任务已创建') @@ -98,7 +115,7 @@ export default function DeploymentsPage() { const createReleaseRun = async () => { try { - await api.createReleaseRun(releaseRunForm) + await api.createReleaseRun({ ...releaseRunForm, operator: defaultOperator }) setReleaseRunOpen(false) setReleaseRunForm(emptyReleaseRunForm) setMessage('总发布任务已创建,将按 user -> pay -> gateway 执行') @@ -110,7 +127,7 @@ export default function DeploymentsPage() { const rollback = async (row: Deployment) => { try { - await api.rollbackDeployment(row.id, { release_id: row.release_id, operator: 'platform-admin' }) + await api.rollbackDeployment(row.id, { release_id: row.release_id, operator: defaultOperator }) setMessage('回滚任务已创建') await load() } catch (err) { @@ -118,6 +135,27 @@ export default function DeploymentsPage() { } } + const loadDeploymentDetail = async (id: number, silent = false) => { + if (!silent) { + setDetailLoading(true) + } + try { + const data = await api.getDeployment(id) + setSelectedDeployment(data) + } catch (err) { + setError(err instanceof Error ? err.message : '加载部署详情失败') + } finally { + setDetailLoading(false) + } + } + + const openDeploymentDetail = (row: Deployment) => { + setSelectedDeploymentID(row.id) + setSelectedDeployment(row) + setDetailOpen(true) + void loadDeploymentDetail(row.id) + } + return ( @@ -161,7 +199,6 @@ export default function DeploymentsPage() { 版本 状态 阶段 - 操作人 错误 动作 @@ -195,7 +232,6 @@ export default function DeploymentsPage() { ))} - {run.operator} {run.error_message || '-'} @@ -228,7 +264,7 @@ export default function DeploymentsPage() { 版本 操作 状态 - 操作人 + 错误 动作 @@ -242,11 +278,20 @@ export default function DeploymentsPage() { - {row.operator} + + + {row.error_message || '-'} + + - + + + + ))} @@ -266,11 +311,6 @@ export default function DeploymentsPage() { value={releaseRunForm.release_id} onChange={(e) => setReleaseRunForm((prev) => ({ ...prev, release_id: e.target.value }))} /> - setReleaseRunForm((prev) => ({ ...prev, operator: e.target.value }))} - /> @@ -310,11 +350,6 @@ export default function DeploymentsPage() { value={deploymentForm.release_id} onChange={(e) => setDeploymentForm((prev) => ({ ...prev, release_id: e.target.value }))} /> - setDeploymentForm((prev) => ({ ...prev, operator: e.target.value }))} - /> @@ -325,6 +360,121 @@ export default function DeploymentsPage() { + setDetailOpen(false)} fullWidth maxWidth="lg"> + 部署详情 + + {detailLoading && !selectedDeployment ? ( + + + + ) : selectedDeployment ? ( + + + + {`${selectedDeployment.service_name} · ${selectedDeployment.release_id || '-'}`} + + + + + + + + + + + + + + {activeDeploymentStatuses.has(selectedDeployment.status) ? ( + + 当前任务仍在执行,目标机状态和日志需要手动刷新查看最新结果。 + + ) : null} + + {selectedDeployment.error_message ? ( + + {selectedDeployment.error_message} + + ) : null} + + {(selectedDeployment.targets ?? []).length === 0 ? ( + + 当前任务还没有目标机执行记录。 + + ) : ( + + {(selectedDeployment.targets ?? []).map((target) => ( + + + + {targetLabel(target)} + + {`开始 ${formatDateTime(target.started_at)} · 结束 ${formatDateTime(target.finished_at)}`} + + + + + + + + + {target.log_excerpt || '无日志输出'} + + + ))} + + )} + + ) : ( + + 未找到该部署任务。 + + )} + + + + + + setMessage('')}> {message} diff --git a/frontend/src/pages/ReleaseRunDetailPage.tsx b/frontend/src/pages/ReleaseRunDetailPage.tsx index f6f6b61..eaddcd8 100644 --- a/frontend/src/pages/ReleaseRunDetailPage.tsx +++ b/frontend/src/pages/ReleaseRunDetailPage.tsx @@ -637,7 +637,6 @@ export default function ReleaseRunDetailPage() { - diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 1237533..ef34ac8 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -73,6 +73,8 @@ export interface DeploymentTarget { status: string step: string log_excerpt: string + started_at?: string + finished_at?: string host?: Host service_instance?: ServiceInstance } @@ -87,6 +89,7 @@ export interface Deployment { trigger_source: string error_message: string created_at?: string + started_at?: string finished_at?: string targets?: DeploymentTarget[] } @@ -98,11 +101,11 @@ export interface ReleaseRunStep { service_name: string previous_release_id: string status: string - deployment_id: number + deployment_id: number | null error_message: string log_excerpt: string rollback_status: string - rollback_deployment_id: number + rollback_deployment_id: number | null rollback_error_message: string rollback_log_excerpt: string created_at?: string