mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-05 09:36:37 +08:00
fix: bound and configure tunnel quality probes (#533)
Closes #528 and #532.
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
export const TUNNEL_QUALITY_INTERVAL_CONFIG_KEY =
|
||||
"monitor_tunnel_quality_interval_sec";
|
||||
export const DEFAULT_TUNNEL_QUALITY_INTERVAL_SEC = 1;
|
||||
export const MIN_TUNNEL_QUALITY_INTERVAL_SEC = 1;
|
||||
export const MAX_TUNNEL_QUALITY_INTERVAL_SEC = 3600;
|
||||
|
||||
export const parseTunnelQualityIntervalSeconds = (value: unknown): number => {
|
||||
const seconds = Number(value);
|
||||
|
||||
return Number.isInteger(seconds) &&
|
||||
seconds >= MIN_TUNNEL_QUALITY_INTERVAL_SEC &&
|
||||
seconds <= MAX_TUNNEL_QUALITY_INTERVAL_SEC
|
||||
? seconds
|
||||
: DEFAULT_TUNNEL_QUALITY_INTERVAL_SEC;
|
||||
};
|
||||
|
||||
export const validateTunnelQualityInterval = (value: string): string | null => {
|
||||
const normalized = value.trim();
|
||||
|
||||
if (!normalized) {
|
||||
return "请输入探测间隔";
|
||||
}
|
||||
const seconds = Number(normalized);
|
||||
|
||||
if (!Number.isInteger(seconds)) {
|
||||
return "探测间隔必须是整数";
|
||||
}
|
||||
if (
|
||||
seconds < MIN_TUNNEL_QUALITY_INTERVAL_SEC ||
|
||||
seconds > MAX_TUNNEL_QUALITY_INTERVAL_SEC
|
||||
) {
|
||||
return `探测间隔必须在 ${MIN_TUNNEL_QUALITY_INTERVAL_SEC} 到 ${MAX_TUNNEL_QUALITY_INTERVAL_SEC} 秒之间`;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const tunnelQualityIntervalLabel = (seconds: number): string =>
|
||||
seconds === 1 ? "每秒" : `每 ${seconds} 秒`;
|
||||
@@ -43,6 +43,14 @@ import { BackIcon, SettingsIcon } from "@/components/icons";
|
||||
import { ThemeSettings } from "@/components/theme-settings";
|
||||
import { isAdmin } from "@/utils/auth";
|
||||
import { getCachedConfigs, configCache, updateSiteConfig } from "@/config/site";
|
||||
import {
|
||||
DEFAULT_TUNNEL_QUALITY_INTERVAL_SEC,
|
||||
MAX_TUNNEL_QUALITY_INTERVAL_SEC,
|
||||
MIN_TUNNEL_QUALITY_INTERVAL_SEC,
|
||||
parseTunnelQualityIntervalSeconds,
|
||||
TUNNEL_QUALITY_INTERVAL_CONFIG_KEY,
|
||||
validateTunnelQualityInterval,
|
||||
} from "@/config/tunnel-quality";
|
||||
import {
|
||||
type UpdateReleaseChannel,
|
||||
getUpdateReleaseChannel,
|
||||
@@ -157,6 +165,16 @@ const CONFIG_ITEMS: ConfigItem[] = [
|
||||
"关闭后,前端停止自动刷新,后端停止实时隧道质量探测(全局配置)",
|
||||
type: "switch",
|
||||
},
|
||||
{
|
||||
key: TUNNEL_QUALITY_INTERVAL_CONFIG_KEY,
|
||||
label: "隧道质量探测间隔",
|
||||
placeholder: String(DEFAULT_TUNNEL_QUALITY_INTERVAL_SEC),
|
||||
description:
|
||||
"设置实时隧道质量检测的执行频率,单位为秒;允许 1–3600 秒,默认 1 秒。",
|
||||
type: "input",
|
||||
dependsOn: "monitor_tunnel_quality_enabled",
|
||||
dependsValue: "true",
|
||||
},
|
||||
{
|
||||
key: "monitor_retention_days",
|
||||
label: "监控数据保留天数",
|
||||
@@ -239,6 +257,7 @@ const getInitialConfigs = (): Record<string, string> => {
|
||||
"cloudflare_secret_key",
|
||||
"forward_compact_mode",
|
||||
"monitor_tunnel_quality_enabled",
|
||||
TUNNEL_QUALITY_INTERVAL_CONFIG_KEY,
|
||||
"monitor_retention_days",
|
||||
"ip",
|
||||
"panel_domain",
|
||||
@@ -622,6 +641,19 @@ export default function ConfigPage() {
|
||||
|
||||
// 保存配置
|
||||
const handleSave = async () => {
|
||||
const intervalValue = configs[TUNNEL_QUALITY_INTERVAL_CONFIG_KEY];
|
||||
const intervalChanged =
|
||||
intervalValue !== originalConfigs[TUNNEL_QUALITY_INTERVAL_CONFIG_KEY];
|
||||
const intervalError = intervalChanged
|
||||
? validateTunnelQualityInterval(intervalValue || "")
|
||||
: null;
|
||||
|
||||
if (intervalError) {
|
||||
toast.error(intervalError);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
const changedKeys = Object.keys(configs).filter(
|
||||
@@ -667,12 +699,22 @@ export default function ConfigPage() {
|
||||
}),
|
||||
);
|
||||
|
||||
// 如果隧道质量检测开关变更,通知 tunnel-monitor-view
|
||||
if (changedKeys.includes("monitor_tunnel_quality_enabled")) {
|
||||
// 如果隧道质量检测配置变更,通知 tunnel-monitor-view
|
||||
if (
|
||||
changedKeys.some((key) =>
|
||||
[
|
||||
"monitor_tunnel_quality_enabled",
|
||||
TUNNEL_QUALITY_INTERVAL_CONFIG_KEY,
|
||||
].includes(key),
|
||||
)
|
||||
) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("monitorTunnelQualityEnabledChanged", {
|
||||
detail: {
|
||||
enabled: configs["monitor_tunnel_quality_enabled"] === "true",
|
||||
intervalSec: parseTunnelQualityIntervalSeconds(
|
||||
configs[TUNNEL_QUALITY_INTERVAL_CONFIG_KEY],
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -1075,11 +1117,21 @@ export default function ConfigPage() {
|
||||
case "bg_image":
|
||||
return renderBgImageUploader();
|
||||
|
||||
case "input":
|
||||
case "input": {
|
||||
if (isBrandPreviewKey(item.key)) {
|
||||
return renderBrandAssetUploader(item.key, isChanged);
|
||||
}
|
||||
|
||||
const isTunnelQualityInterval =
|
||||
item.key === TUNNEL_QUALITY_INTERVAL_CONFIG_KEY;
|
||||
const intervalValue = isTunnelQualityInterval
|
||||
? (configs[item.key] ?? String(DEFAULT_TUNNEL_QUALITY_INTERVAL_SEC))
|
||||
: (configs[item.key] ?? "");
|
||||
const intervalError =
|
||||
isTunnelQualityInterval && configs[item.key] !== undefined
|
||||
? validateTunnelQualityInterval(intervalValue)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Input
|
||||
classNames={{
|
||||
@@ -1091,14 +1143,30 @@ export default function ConfigPage() {
|
||||
description={
|
||||
isCommercialDisabled ? "需商业版授权才能修改此项" : undefined
|
||||
}
|
||||
endContent={isTunnelQualityInterval ? "秒" : undefined}
|
||||
errorMessage={intervalError || undefined}
|
||||
isDisabled={isCommercialDisabled}
|
||||
isInvalid={Boolean(intervalError)}
|
||||
max={
|
||||
isTunnelQualityInterval
|
||||
? MAX_TUNNEL_QUALITY_INTERVAL_SEC
|
||||
: undefined
|
||||
}
|
||||
min={
|
||||
isTunnelQualityInterval
|
||||
? MIN_TUNNEL_QUALITY_INTERVAL_SEC
|
||||
: undefined
|
||||
}
|
||||
placeholder={item.placeholder}
|
||||
size="md"
|
||||
value={configs[item.key] || ""}
|
||||
step={isTunnelQualityInterval ? 1 : undefined}
|
||||
type={isTunnelQualityInterval ? "number" : "text"}
|
||||
value={intervalValue}
|
||||
variant="bordered"
|
||||
onChange={(e) => handleConfigChange(item.key, e.target.value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
case "switch":
|
||||
return (
|
||||
|
||||
@@ -53,12 +53,17 @@ import {
|
||||
TableRow,
|
||||
TableCell,
|
||||
} from "@/shadcn-bridge/heroui/table";
|
||||
import {
|
||||
DEFAULT_TUNNEL_QUALITY_INTERVAL_SEC,
|
||||
parseTunnelQualityIntervalSeconds,
|
||||
TUNNEL_QUALITY_INTERVAL_CONFIG_KEY,
|
||||
tunnelQualityIntervalLabel,
|
||||
} from "@/config/tunnel-quality";
|
||||
|
||||
interface TunnelMonitorViewProps {
|
||||
viewMode?: "list" | "grid";
|
||||
}
|
||||
|
||||
const QUALITY_POLL_INTERVAL = 1_000; // 1 second
|
||||
const MONITOR_TUNNEL_QUALITY_ENABLED_CONFIG_KEY =
|
||||
"monitor_tunnel_quality_enabled";
|
||||
const MONITOR_TUNNEL_QUALITY_ENABLED_EVENT =
|
||||
@@ -562,6 +567,9 @@ export function TunnelMonitorView({
|
||||
const qualityTimerRef = useRef<number | null>(null);
|
||||
const [monitorTunnelQualityEnabled, setMonitorTunnelQualityEnabled] =
|
||||
useState(true);
|
||||
const [tunnelQualityIntervalSec, setTunnelQualityIntervalSec] = useState(
|
||||
DEFAULT_TUNNEL_QUALITY_INTERVAL_SEC,
|
||||
);
|
||||
|
||||
// Detail view state
|
||||
const [detailTunnelId, setDetailTunnelId] = useState<number | null>(null);
|
||||
@@ -618,26 +626,28 @@ export function TunnelMonitorView({
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadMonitorTunnelQualityEnabled = useCallback(async () => {
|
||||
try {
|
||||
const response = await getConfigByName(
|
||||
MONITOR_TUNNEL_QUALITY_ENABLED_CONFIG_KEY,
|
||||
);
|
||||
const loadTunnelQualityConfig = useCallback(async () => {
|
||||
const [enabledResponse, intervalResponse] = await Promise.all([
|
||||
getConfigByName(MONITOR_TUNNEL_QUALITY_ENABLED_CONFIG_KEY).catch(
|
||||
() => null,
|
||||
),
|
||||
getConfigByName(TUNNEL_QUALITY_INTERVAL_CONFIG_KEY).catch(() => null),
|
||||
]);
|
||||
|
||||
setMonitorTunnelQualityEnabled(
|
||||
typeof response.data?.value === "string"
|
||||
? response.data.value === "true"
|
||||
: true,
|
||||
);
|
||||
} catch {
|
||||
setMonitorTunnelQualityEnabled(true);
|
||||
}
|
||||
setMonitorTunnelQualityEnabled(
|
||||
typeof enabledResponse?.data?.value === "string"
|
||||
? enabledResponse.data.value === "true"
|
||||
: true,
|
||||
);
|
||||
setTunnelQualityIntervalSec(
|
||||
parseTunnelQualityIntervalSeconds(intervalResponse?.data?.value),
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void loadTunnels();
|
||||
void loadMonitorTunnelQualityEnabled();
|
||||
}, [loadMonitorTunnelQualityEnabled, loadTunnels]);
|
||||
void loadTunnelQualityConfig();
|
||||
}, [loadTunnelQualityConfig, loadTunnels]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setInterval(() => {
|
||||
@@ -649,8 +659,11 @@ export function TunnelMonitorView({
|
||||
|
||||
useEffect(() => {
|
||||
const handleMonitorTunnelQualityEnabledChanged = (event: Event) => {
|
||||
const enabled = (event as CustomEvent<{ enabled?: boolean }>).detail
|
||||
?.enabled;
|
||||
const detail = (
|
||||
event as CustomEvent<{ enabled?: boolean; intervalSec?: number }>
|
||||
).detail;
|
||||
const enabled = detail?.enabled;
|
||||
const intervalSec = detail?.intervalSec;
|
||||
|
||||
if (typeof enabled === "boolean") {
|
||||
setMonitorTunnelQualityEnabled(enabled);
|
||||
@@ -658,7 +671,12 @@ export function TunnelMonitorView({
|
||||
setQualityLoading(false);
|
||||
}
|
||||
} else {
|
||||
void loadMonitorTunnelQualityEnabled();
|
||||
void loadTunnelQualityConfig();
|
||||
}
|
||||
if (typeof intervalSec === "number") {
|
||||
setTunnelQualityIntervalSec(
|
||||
parseTunnelQualityIntervalSeconds(String(intervalSec)),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -673,7 +691,7 @@ export function TunnelMonitorView({
|
||||
handleMonitorTunnelQualityEnabledChanged as EventListener,
|
||||
);
|
||||
};
|
||||
}, [loadMonitorTunnelQualityEnabled]);
|
||||
}, [loadTunnelQualityConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tunnels.length > 0 && !initialHistoryFetched.current) {
|
||||
@@ -726,7 +744,7 @@ export function TunnelMonitorView({
|
||||
}
|
||||
}, [tunnels]);
|
||||
|
||||
// --- Load quality snapshots (auto-polling every 10s) ---
|
||||
// --- Load quality snapshots using the configured probe interval ---
|
||||
const loadQuality = useCallback(async (options?: { silent?: boolean }) => {
|
||||
const silent = options?.silent ?? false;
|
||||
|
||||
@@ -788,7 +806,7 @@ export function TunnelMonitorView({
|
||||
|
||||
qualityTimerRef.current = window.setInterval(() => {
|
||||
void loadQuality({ silent: true });
|
||||
}, QUALITY_POLL_INTERVAL);
|
||||
}, tunnelQualityIntervalSec * 1000);
|
||||
|
||||
return () => {
|
||||
if (qualityTimerRef.current) {
|
||||
@@ -796,7 +814,7 @@ export function TunnelMonitorView({
|
||||
qualityTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [loadQuality, monitorTunnelQualityEnabled]);
|
||||
}, [loadQuality, monitorTunnelQualityEnabled, tunnelQualityIntervalSec]);
|
||||
|
||||
// --- Load quality history for detail chart ---
|
||||
const loadQualityHistory = useCallback(
|
||||
@@ -1065,7 +1083,7 @@ export function TunnelMonitorView({
|
||||
{monitorTunnelQualityEnabled ? (
|
||||
<>
|
||||
<LiveDot />
|
||||
<span>自动探测中(每秒测试,30秒上报)</span>
|
||||
<span>{`自动探测中(${tunnelQualityIntervalLabel(tunnelQualityIntervalSec)}测试)`}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -1129,7 +1147,7 @@ export function TunnelMonitorView({
|
||||
{monitorTunnelQualityEnabled ? (
|
||||
<>
|
||||
<LiveDot />
|
||||
<span>每秒探测 · 更新于 {lastQualityUpdate}</span>
|
||||
<span>{`${tunnelQualityIntervalLabel(tunnelQualityIntervalSec)}探测 · 更新于 ${lastQualityUpdate}`}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user