mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-04 09:06:38 +08:00
fix: panel self-upgrade helper not recreating containers (#495)
## Summary - **Helper container `docker compose up` 不会强制重建容器**:原脚本缺少 `--force-recreate`,Docker Compose 在检测不到配置变化时不会替换运行中的容器,导致拉取了新镜像但旧容器继续运行。新增 `--force-recreate --remove-orphans` 确保容器被替换。 - **无错误日志**:helper 容器执行失败时没有任何可见反馈。新增 `upgrade.log` 写入部署目录,每一步操作和错误都有记录。 - **"立即升级"按钮永久禁用**:按钮 `isDisabled` 绑定了 `!canOpenSystemUpgradeModal`,该条件要求已完成检查更新且有可用更新,但页面本身有点击时自动检查的逻辑,导致按钮永远无法点击。改为 `!canTriggerSystemUpgrade` 允许自动检查流程触发。 ## Verification | Check | Result | |-------|--------| | `go test ./...` | ✅ 498 passed | | `pnpm run build` | ✅ passed | | `pnpm run lint` | ✅ passed |
This commit is contained in:
@@ -180,13 +180,41 @@ func (e *systemUpgradeExecutor) selectComposeAsset(current []byte) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *systemUpgradeExecutor) helperScript() string {
|
func (e *systemUpgradeExecutor) helperScript() string {
|
||||||
return strings.Join([]string{
|
return `set -eu
|
||||||
"set -eu",
|
LOGFILE="$PANEL_DEPLOY_DIR/upgrade.log"
|
||||||
`cd "$PANEL_DEPLOY_DIR"`,
|
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOGFILE"; }
|
||||||
"docker compose pull backend frontend",
|
|
||||||
"sleep 5",
|
cd "$PANEL_DEPLOY_DIR"
|
||||||
"docker compose up -d backend frontend",
|
echo "" > "$LOGFILE"
|
||||||
}, "\n")
|
log "开始面板升级"
|
||||||
|
log "工作目录: $(pwd)"
|
||||||
|
|
||||||
|
if [ ! -f docker-compose.yml ]; then
|
||||||
|
log "错误: docker-compose.yml 不存在"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
log "错误: .env 不存在"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "拉取新镜像..."
|
||||||
|
if ! docker compose pull backend frontend 2>&1 | tee -a "$LOGFILE"; then
|
||||||
|
log "错误: 拉取镜像失败"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "等待旧容器释放资源..."
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
log "重启服务(force-recreate)..."
|
||||||
|
if ! docker compose up -d --force-recreate --remove-orphans backend frontend 2>&1 | tee -a "$LOGFILE"; then
|
||||||
|
log "错误: 重启服务失败"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "升级完成"
|
||||||
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *systemUpgradeExecutor) buildHelperRunArgs(imageID, helperName string) ([]string, error) {
|
func (e *systemUpgradeExecutor) buildHelperRunArgs(imageID, helperName string) ([]string, error) {
|
||||||
|
|||||||
@@ -508,8 +508,7 @@ export interface SystemUpgradeVersionApiData {
|
|||||||
capability: SystemUpgradeCapabilityApiData;
|
capability: SystemUpgradeCapabilityApiData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SystemUpgradeCheckApiData
|
export interface SystemUpgradeCheckApiData extends SystemUpgradeVersionApiData {
|
||||||
extends SystemUpgradeVersionApiData {
|
|
||||||
releases: SystemUpgradeReleaseApiItem[];
|
releases: SystemUpgradeReleaseApiItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
import type {
|
||||||
|
SystemUpgradeCheckApiData,
|
||||||
|
SystemUpgradeRunApiData,
|
||||||
|
SystemUpgradeReleaseApiItem,
|
||||||
|
SystemUpgradeVersionApiData,
|
||||||
|
} from "@/api/types";
|
||||||
|
|
||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
@@ -32,12 +39,6 @@ import {
|
|||||||
runSystemUpgrade,
|
runSystemUpgrade,
|
||||||
type AnnouncementData,
|
type AnnouncementData,
|
||||||
} from "@/api";
|
} from "@/api";
|
||||||
import type {
|
|
||||||
SystemUpgradeCheckApiData,
|
|
||||||
SystemUpgradeRunApiData,
|
|
||||||
SystemUpgradeReleaseApiItem,
|
|
||||||
SystemUpgradeVersionApiData,
|
|
||||||
} from "@/api/types";
|
|
||||||
import { BackIcon, SettingsIcon } from "@/components/icons";
|
import { BackIcon, SettingsIcon } from "@/components/icons";
|
||||||
import { ThemeSettings } from "@/components/theme-settings";
|
import { ThemeSettings } from "@/components/theme-settings";
|
||||||
import { isAdmin } from "@/utils/auth";
|
import { isAdmin } from "@/utils/auth";
|
||||||
@@ -328,6 +329,12 @@ export default function ConfigPage() {
|
|||||||
systemUpgradeReleasesMatchChannel &&
|
systemUpgradeReleasesMatchChannel &&
|
||||||
systemUpgradeReleases.length > 0,
|
systemUpgradeReleases.length > 0,
|
||||||
);
|
);
|
||||||
|
const canTriggerSystemUpgrade = Boolean(
|
||||||
|
!systemUpgradeLoading &&
|
||||||
|
!systemUpgradeChecking &&
|
||||||
|
!systemUpgradeExecuting &&
|
||||||
|
systemUpgradeInfo?.capability.capable !== false,
|
||||||
|
);
|
||||||
const canOpenSystemUpgradeModal = Boolean(
|
const canOpenSystemUpgradeModal = Boolean(
|
||||||
systemUpgradeInfo?.capability.capable &&
|
systemUpgradeInfo?.capability.capable &&
|
||||||
systemUpgradeHasConfirmedUpdate &&
|
systemUpgradeHasConfirmedUpdate &&
|
||||||
@@ -421,7 +428,9 @@ export default function ConfigPage() {
|
|||||||
hasUpdate:
|
hasUpdate:
|
||||||
response.data.channel === channel ? response.data.hasUpdate : false,
|
response.data.channel === channel ? response.data.hasUpdate : false,
|
||||||
latestVersion:
|
latestVersion:
|
||||||
response.data.channel === channel ? response.data.latestVersion : "",
|
response.data.channel === channel
|
||||||
|
? response.data.latestVersion
|
||||||
|
: "",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setSystemUpgradeInfo(null);
|
setSystemUpgradeInfo(null);
|
||||||
@@ -512,7 +521,9 @@ export default function ConfigPage() {
|
|||||||
: "未获取到可用版本",
|
: "未获取到可用版本",
|
||||||
);
|
);
|
||||||
|
|
||||||
return Boolean(data.capability.capable && data.hasUpdate && data.releases?.length);
|
return Boolean(
|
||||||
|
data.capability.capable && data.hasUpdate && data.releases?.length,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
setSystemUpgradeReleases([]);
|
setSystemUpgradeReleases([]);
|
||||||
setSystemUpgradeCheckedChannel(null);
|
setSystemUpgradeCheckedChannel(null);
|
||||||
@@ -1568,7 +1579,8 @@ export default function ConfigPage() {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-default-500">部署目录</p>
|
<p className="text-xs text-default-500">部署目录</p>
|
||||||
<p className="mt-1 break-all font-medium">
|
<p className="mt-1 break-all font-medium">
|
||||||
{systemUpgradeInfo?.capability.deployDir || "未获取到部署目录"}
|
{systemUpgradeInfo?.capability.deployDir ||
|
||||||
|
"未获取到部署目录"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -1625,7 +1637,9 @@ export default function ConfigPage() {
|
|||||||
size="md"
|
size="md"
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
onSelectionChange={(keys) => {
|
onSelectionChange={(keys) => {
|
||||||
const selected = Array.from(keys)[0] as string | undefined;
|
const selected = Array.from(keys)[0] as
|
||||||
|
| string
|
||||||
|
| undefined;
|
||||||
|
|
||||||
setSystemUpgradeSelectedVersion(selected || "");
|
setSystemUpgradeSelectedVersion(selected || "");
|
||||||
}}
|
}}
|
||||||
@@ -1648,17 +1662,15 @@ export default function ConfigPage() {
|
|||||||
|
|
||||||
<div className="flex flex-col gap-3 pt-1 sm:flex-row sm:justify-end">
|
<div className="flex flex-col gap-3 pt-1 sm:flex-row sm:justify-end">
|
||||||
<Button
|
<Button
|
||||||
variant="flat"
|
|
||||||
isLoading={systemUpgradeChecking}
|
isLoading={systemUpgradeChecking}
|
||||||
|
variant="flat"
|
||||||
onPress={handleCheckSystemUpgrade}
|
onPress={handleCheckSystemUpgrade}
|
||||||
>
|
>
|
||||||
检查更新
|
检查更新
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
isDisabled={
|
isDisabled={!canTriggerSystemUpgrade}
|
||||||
!canOpenSystemUpgradeModal
|
|
||||||
}
|
|
||||||
isLoading={systemUpgradeExecuting}
|
isLoading={systemUpgradeExecuting}
|
||||||
onPress={handleOpenSystemUpgradeModal}
|
onPress={handleOpenSystemUpgradeModal}
|
||||||
>
|
>
|
||||||
@@ -1925,10 +1937,12 @@ export default function ConfigPage() {
|
|||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div className="space-y-3 text-sm text-default-700 dark:text-default-300">
|
<div className="space-y-3 text-sm text-default-700 dark:text-default-300">
|
||||||
<p>
|
<p>
|
||||||
升级过程需要访问 Docker Socket,并会在短时间内中断当前面板服务。
|
升级过程需要访问 Docker
|
||||||
|
Socket,并会在短时间内中断当前面板服务。
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
请确认已经允许面板管理容器与宿主机 Docker 交互,并且可以接受升级期间的临时不可用。
|
请确认已经允许面板管理容器与宿主机 Docker
|
||||||
|
交互,并且可以接受升级期间的临时不可用。
|
||||||
</p>
|
</p>
|
||||||
<div className="space-y-2 rounded-lg border border-warning-200 bg-warning-50 px-4 py-3 text-warning-800 dark:border-warning-900/40 dark:bg-warning-950/30 dark:text-warning-200">
|
<div className="space-y-2 rounded-lg border border-warning-200 bg-warning-50 px-4 py-3 text-warning-800 dark:border-warning-900/40 dark:bg-warning-950/30 dark:text-warning-200">
|
||||||
<p className="text-xs font-medium">升级前请确认</p>
|
<p className="text-xs font-medium">升级前请确认</p>
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ const isObjectRecord = (value: unknown): value is Record<string, unknown> =>
|
|||||||
const toSafeString = (value: unknown): string => {
|
const toSafeString = (value: unknown): string => {
|
||||||
if (typeof value === "string") return value;
|
if (typeof value === "string") return value;
|
||||||
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -265,11 +266,13 @@ const normalizeBestExitState = (value: unknown): BestExitState | undefined => {
|
|||||||
|
|
||||||
const bestExitOwnerRoleText = (role?: string) => {
|
const bestExitOwnerRoleText = (role?: string) => {
|
||||||
if (role === "chain") return "中转";
|
if (role === "chain") return "中转";
|
||||||
|
|
||||||
return "入口";
|
return "入口";
|
||||||
};
|
};
|
||||||
|
|
||||||
const bestExitDetailTitle = (state?: BestExitState) => {
|
const bestExitDetailTitle = (state?: BestExitState) => {
|
||||||
if (!state?.items?.length) return undefined;
|
if (!state?.items?.length) return undefined;
|
||||||
|
|
||||||
return state.items
|
return state.items
|
||||||
.map(
|
.map(
|
||||||
(item) =>
|
(item) =>
|
||||||
@@ -3108,7 +3111,9 @@ export default function TunnelPage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectItem key="1">单向计算(仅上传)</SelectItem>
|
<SelectItem key="1">单向计算(仅上传)</SelectItem>
|
||||||
<SelectItem key="2">双向计算(上传+下载)</SelectItem>
|
<SelectItem key="2">
|
||||||
|
双向计算(上传+下载)
|
||||||
|
</SelectItem>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
@@ -3171,7 +3176,9 @@ export default function TunnelPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium">质量检测目标</div>
|
<div className="text-sm font-medium">
|
||||||
|
质量检测目标
|
||||||
|
</div>
|
||||||
<p className="text-xs text-default-500 mt-0.5">
|
<p className="text-xs text-default-500 mt-0.5">
|
||||||
用于实时隧道质量检测、诊断目标和 best
|
用于实时隧道质量检测、诊断目标和 best
|
||||||
最优出口评分,留空使用 www.bing.com:443
|
最优出口评分,留空使用 www.bing.com:443
|
||||||
|
|||||||
@@ -401,6 +401,7 @@ export function Select<T>({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<select
|
<select
|
||||||
|
aria-label={label ? undefined : ariaLabel}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full rounded-md border border-input bg-background px-3 py-2 text-foreground shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-ring dark:[color-scheme:dark]",
|
"w-full rounded-md border border-input bg-background px-3 py-2 text-foreground shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-ring dark:[color-scheme:dark]",
|
||||||
sizeClass(size),
|
sizeClass(size),
|
||||||
@@ -409,7 +410,6 @@ export function Select<T>({
|
|||||||
)}
|
)}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
id={generatedId}
|
id={generatedId}
|
||||||
aria-label={label ? undefined : ariaLabel}
|
|
||||||
required={isRequired}
|
required={isRequired}
|
||||||
value={singleValue}
|
value={singleValue}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
|||||||
Reference in New Issue
Block a user