修复 CI 部署:使用原生 ssh 替代 appleboy action
This commit is contained in:
@@ -69,14 +69,14 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Deploy to VPS
|
- name: Deploy to VPS
|
||||||
uses: appleboy/ssh-action@v1
|
run: |
|
||||||
with:
|
mkdir -p ~/.ssh
|
||||||
host: ${{ secrets.VPS_HOST }}
|
echo "${{ secrets.VPS_SSH_KEY }}" > ~/.ssh/id_rsa
|
||||||
username: ${{ secrets.VPS_USER }}
|
chmod 600 ~/.ssh/id_rsa
|
||||||
key: ${{ secrets.VPS_SSH_KEY }}
|
ssh -o StrictHostKeyChecking=accept-new ${{ secrets.VPS_USER }}@${{ secrets.VPS_HOST }} "
|
||||||
script: |
|
|
||||||
cd /root/PolyWeather
|
cd /root/PolyWeather
|
||||||
git pull origin main
|
git pull origin main
|
||||||
docker compose up -d --build
|
docker compose up -d --build
|
||||||
sleep 10
|
sleep 10
|
||||||
curl -s http://localhost:8000/healthz
|
curl -s http://localhost:8000/healthz
|
||||||
|
"
|
||||||
|
|||||||
@@ -3,13 +3,7 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { RefreshCw } from "lucide-react";
|
import { RefreshCw } from "lucide-react";
|
||||||
import {
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import styles from "./Dashboard.module.css";
|
import styles from "./Dashboard.module.css";
|
||||||
import { scanRootClass } from "./scan-root-styles";
|
import { scanRootClass } from "./scan-root-styles";
|
||||||
import {
|
import {
|
||||||
@@ -18,9 +12,7 @@ import {
|
|||||||
useProAccess,
|
useProAccess,
|
||||||
} from "@/hooks/useDashboardStore";
|
} from "@/hooks/useDashboardStore";
|
||||||
import { I18nProvider, useI18n } from "@/hooks/useI18n";
|
import { I18nProvider, useI18n } from "@/hooks/useI18n";
|
||||||
import type {
|
import type { ScanOpportunityRow } from "@/lib/dashboard-types";
|
||||||
ScanOpportunityRow,
|
|
||||||
} from "@/lib/dashboard-types";
|
|
||||||
import { AiPinnedForecastView } from "@/components/dashboard/scan-terminal/AiPinnedForecastView";
|
import { AiPinnedForecastView } from "@/components/dashboard/scan-terminal/AiPinnedForecastView";
|
||||||
import { MobileCityPicker } from "@/components/dashboard/scan-terminal/MobileCityPicker";
|
import { MobileCityPicker } from "@/components/dashboard/scan-terminal/MobileCityPicker";
|
||||||
import { WelcomeOverlay } from "@/components/dashboard/scan-terminal/WelcomeOverlay";
|
import { WelcomeOverlay } from "@/components/dashboard/scan-terminal/WelcomeOverlay";
|
||||||
@@ -78,18 +70,16 @@ function ScanTerminalScreen() {
|
|||||||
const accountHref = proAccess.authenticated
|
const accountHref = proAccess.authenticated
|
||||||
? "/account"
|
? "/account"
|
||||||
: "/auth/login?next=%2Faccount";
|
: "/auth/login?next=%2Faccount";
|
||||||
const {
|
const { refreshScanTerminalManually, scanError, scanLoading, terminalData } =
|
||||||
refreshScanTerminalManually,
|
useScanTerminalQuery({
|
||||||
scanError,
|
isPro,
|
||||||
scanLoading,
|
proAccessLoading: proAccess.loading,
|
||||||
terminalData,
|
});
|
||||||
} = useScanTerminalQuery({
|
|
||||||
isPro,
|
|
||||||
proAccessLoading: proAccess.loading,
|
|
||||||
});
|
|
||||||
const [selectedRowId, setSelectedRowId] = useState<string | null>(null);
|
const [selectedRowId, setSelectedRowId] = useState<string | null>(null);
|
||||||
const [activeView, setActiveView] = useState<ScanTerminalContentView>("map");
|
const [activeView, setActiveView] = useState<ScanTerminalContentView>("map");
|
||||||
const [mapSelectedCityName, setMapSelectedCityName] = useState<string | null>(null);
|
const [mapSelectedCityName, setMapSelectedCityName] = useState<string | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
const [showScanPaywall, setShowScanPaywall] = useState(false);
|
const [showScanPaywall, setShowScanPaywall] = useState(false);
|
||||||
const [isMobileViewport, setIsMobileViewport] = useState(false);
|
const [isMobileViewport, setIsMobileViewport] = useState(false);
|
||||||
|
|
||||||
@@ -185,12 +175,17 @@ function ScanTerminalScreen() {
|
|||||||
});
|
});
|
||||||
const selectedRow = useMemo(() => {
|
const selectedRow = useMemo(() => {
|
||||||
if (!timeSortedRows.length) return null;
|
if (!timeSortedRows.length) return null;
|
||||||
return timeSortedRows.find((row) => row.id === selectedRowId) || timeSortedRows[0] || null;
|
return (
|
||||||
|
timeSortedRows.find((row) => row.id === selectedRowId) ||
|
||||||
|
timeSortedRows[0] ||
|
||||||
|
null
|
||||||
|
);
|
||||||
}, [timeSortedRows, selectedRowId]);
|
}, [timeSortedRows, selectedRowId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!timeSortedRows.length) return;
|
if (!timeSortedRows.length) return;
|
||||||
if (selectedRowId && timeSortedRows.some((row) => row.id === selectedRowId)) return;
|
if (selectedRowId && timeSortedRows.some((row) => row.id === selectedRowId))
|
||||||
|
return;
|
||||||
setSelectedRowId(timeSortedRows[0].id);
|
setSelectedRowId(timeSortedRows[0].id);
|
||||||
}, [selectedRowId, timeSortedRows]);
|
}, [selectedRowId, timeSortedRows]);
|
||||||
|
|
||||||
@@ -205,7 +200,8 @@ function ScanTerminalScreen() {
|
|||||||
const cityKey = normalizeCityKey(rawCityName);
|
const cityKey = normalizeCityKey(rawCityName);
|
||||||
if (!cityKey || mapFocusedRow) return null;
|
if (!cityKey || mapFocusedRow) return null;
|
||||||
const selectedDetail =
|
const selectedDetail =
|
||||||
store.selectedDetail && normalizeCityKey(store.selectedDetail.name) === cityKey
|
store.selectedDetail &&
|
||||||
|
normalizeCityKey(store.selectedDetail.name) === cityKey
|
||||||
? store.selectedDetail
|
? store.selectedDetail
|
||||||
: Object.values(store.cityDetailsByName).find(
|
: Object.values(store.cityDetailsByName).find(
|
||||||
(detail) => normalizeCityKey(detail?.name) === cityKey,
|
(detail) => normalizeCityKey(detail?.name) === cityKey,
|
||||||
@@ -246,7 +242,8 @@ function ScanTerminalScreen() {
|
|||||||
display_name: displayName,
|
display_name: displayName,
|
||||||
selected_date: selectedDetail?.local_date || null,
|
selected_date: selectedDetail?.local_date || null,
|
||||||
local_date: selectedDetail?.local_date || null,
|
local_date: selectedDetail?.local_date || null,
|
||||||
local_time: selectedDetail?.local_time || selectedSummary?.local_time || null,
|
local_time:
|
||||||
|
selectedDetail?.local_time || selectedSummary?.local_time || null,
|
||||||
temp_symbol: tempSymbol,
|
temp_symbol: tempSymbol,
|
||||||
current_temp: currentTemp,
|
current_temp: currentTemp,
|
||||||
current_max_so_far:
|
current_max_so_far:
|
||||||
@@ -309,28 +306,32 @@ function ScanTerminalScreen() {
|
|||||||
"多模型高温预测",
|
"多模型高温预测",
|
||||||
"实时观测偏差",
|
"实时观测偏差",
|
||||||
"未来日期城市决策卡",
|
"未来日期城市决策卡",
|
||||||
"Telegram 群内价 5U",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeDetailRow) return;
|
if (!activeDetailRow) return;
|
||||||
if (!findDetailForCity(store.cityDetailsByName, activeDetailRow.city)) {
|
if (!findDetailForCity(store.cityDetailsByName, activeDetailRow.city)) {
|
||||||
void store.ensureCityDetail(activeDetailRow.city, false, "panel").catch(() => {});
|
void store
|
||||||
|
.ensureCityDetail(activeDetailRow.city, false, "panel")
|
||||||
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
}, [activeDetailRow, store.cityDetailsByName, store.ensureCityDetail]);
|
}, [activeDetailRow, store.cityDetailsByName, store.ensureCityDetail]);
|
||||||
|
|
||||||
const handleMapCitySelect = useCallback((cityName: string) => {
|
const handleMapCitySelect = useCallback(
|
||||||
setMapSelectedCityName(cityName);
|
(cityName: string) => {
|
||||||
lastMapSelectedCityRef.current = normalizeCityKey(cityName);
|
setMapSelectedCityName(cityName);
|
||||||
const matchedRow = findRowForCity(timeSortedRows, cityName);
|
lastMapSelectedCityRef.current = normalizeCityKey(cityName);
|
||||||
if (matchedRow) {
|
const matchedRow = findRowForCity(timeSortedRows, cityName);
|
||||||
store.preloadCityFromRow(matchedRow);
|
if (matchedRow) {
|
||||||
setSelectedRowId(matchedRow.id);
|
store.preloadCityFromRow(matchedRow);
|
||||||
} else {
|
setSelectedRowId(matchedRow.id);
|
||||||
void store.ensureCityDetail(cityName, false, "panel").catch(() => {});
|
} else {
|
||||||
setSelectedRowId(null);
|
void store.ensureCityDetail(cityName, false, "panel").catch(() => {});
|
||||||
}
|
setSelectedRowId(null);
|
||||||
}, [store, timeSortedRows]);
|
}
|
||||||
|
},
|
||||||
|
[store, timeSortedRows],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeView !== "map") return;
|
if (activeView !== "map") return;
|
||||||
@@ -344,36 +345,44 @@ function ScanTerminalScreen() {
|
|||||||
addAiPinnedCity(selectedCity);
|
addAiPinnedCity(selectedCity);
|
||||||
}, [activeView, addAiPinnedCity, store.selectedCity, timeSortedRows]);
|
}, [activeView, addAiPinnedCity, store.selectedCity, timeSortedRows]);
|
||||||
|
|
||||||
const handleSelectRow = useCallback((row: ScanOpportunityRow) => {
|
const handleSelectRow = useCallback(
|
||||||
const cityName = row.city || row.city_display_name || row.display_name || "";
|
(row: ScanOpportunityRow) => {
|
||||||
if (!cityName) return;
|
const cityName =
|
||||||
setSelectedRowId(row.id);
|
row.city || row.city_display_name || row.display_name || "";
|
||||||
store.preloadCityFromRow(row);
|
if (!cityName) return;
|
||||||
const selectedCityKey = normalizeCityKey(store.selectedCity);
|
setSelectedRowId(row.id);
|
||||||
const rowCityKey = normalizeCityKey(cityName);
|
store.preloadCityFromRow(row);
|
||||||
const hasCachedDetail =
|
const selectedCityKey = normalizeCityKey(store.selectedCity);
|
||||||
Boolean(findDetailForCity(store.cityDetailsByName, cityName)) ||
|
const rowCityKey = normalizeCityKey(cityName);
|
||||||
Object.values(store.cityDetailsByName).some((detail) =>
|
const hasCachedDetail =
|
||||||
rowMatchesCity(row, detail?.name || detail?.display_name || ""),
|
Boolean(findDetailForCity(store.cityDetailsByName, cityName)) ||
|
||||||
);
|
Object.values(store.cityDetailsByName).some((detail) =>
|
||||||
if (store.isPanelOpen && selectedCityKey === rowCityKey) {
|
rowMatchesCity(row, detail?.name || detail?.display_name || ""),
|
||||||
if (!hasCachedDetail) {
|
);
|
||||||
void store.ensureCityDetail(cityName, false, "panel").catch(() => {});
|
if (store.isPanelOpen && selectedCityKey === rowCityKey) {
|
||||||
|
if (!hasCachedDetail) {
|
||||||
|
void store.ensureCityDetail(cityName, false, "panel").catch(() => {});
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return;
|
void store.selectCity(cityName);
|
||||||
}
|
},
|
||||||
void store.selectCity(cityName);
|
[store],
|
||||||
}, [store]);
|
);
|
||||||
|
|
||||||
const handleOpenDecisionRow = useCallback((row: ScanOpportunityRow) => {
|
const handleOpenDecisionRow = useCallback(
|
||||||
const cityName = row.city || row.city_display_name || row.display_name || "";
|
(row: ScanOpportunityRow) => {
|
||||||
if (!cityName) return;
|
const cityName =
|
||||||
setSelectedRowId(row.id);
|
row.city || row.city_display_name || row.display_name || "";
|
||||||
store.preloadCityFromRow(row);
|
if (!cityName) return;
|
||||||
addAiPinnedCity(cityName);
|
setSelectedRowId(row.id);
|
||||||
setActiveView("analysis");
|
store.preloadCityFromRow(row);
|
||||||
void store.selectCity(cityName);
|
addAiPinnedCity(cityName);
|
||||||
}, [addAiPinnedCity, store]);
|
setActiveView("analysis");
|
||||||
|
void store.selectCity(cityName);
|
||||||
|
},
|
||||||
|
[addAiPinnedCity, store],
|
||||||
|
);
|
||||||
|
|
||||||
const openScanPaywall = useCallback(() => {
|
const openScanPaywall = useCallback(() => {
|
||||||
setShowScanPaywall(true);
|
setShowScanPaywall(true);
|
||||||
@@ -430,7 +439,6 @@ function ScanTerminalScreen() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={scanTerminalRootClassName}>
|
<div className={scanTerminalRootClassName}>
|
||||||
<div
|
<div
|
||||||
@@ -498,7 +506,11 @@ function ScanTerminalScreen() {
|
|||||||
|
|
||||||
<section className="scan-list-section">
|
<section className="scan-list-section">
|
||||||
<div className="scan-list-header">
|
<div className="scan-list-header">
|
||||||
<div className="scan-list-tabs" role="tablist" aria-label={isEn ? "Content view" : "内容视图"}>
|
<div
|
||||||
|
className="scan-list-tabs"
|
||||||
|
role="tablist"
|
||||||
|
aria-label={isEn ? "Content view" : "内容视图"}
|
||||||
|
>
|
||||||
{isMobileViewport ? (
|
{isMobileViewport ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -518,7 +530,9 @@ function ScanTerminalScreen() {
|
|||||||
aria-selected={resolvedView === "map"}
|
aria-selected={resolvedView === "map"}
|
||||||
className={resolvedView === "map" ? "active" : ""}
|
className={resolvedView === "map" ? "active" : ""}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
lastMapSelectedCityRef.current = normalizeCityKey(store.selectedCity);
|
lastMapSelectedCityRef.current = normalizeCityKey(
|
||||||
|
store.selectedCity,
|
||||||
|
);
|
||||||
setActiveView("map");
|
setActiveView("map");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -541,13 +555,19 @@ function ScanTerminalScreen() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="scan-list-status">
|
<div className="scan-list-status">
|
||||||
{terminalData?.generated_at ? (
|
{terminalData?.generated_at ? (
|
||||||
<span className={clsx("scan-status-chip", terminalData?.stale ? "stale" : "live")}>
|
<span
|
||||||
|
className={clsx(
|
||||||
|
"scan-status-chip",
|
||||||
|
terminalData?.stale ? "stale" : "live",
|
||||||
|
)}
|
||||||
|
>
|
||||||
{isEn ? "Updated" : "已更新"} {serverAgeText || ""}
|
{isEn ? "Updated" : "已更新"} {serverAgeText || ""}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
{terminalData?.stale && localAgeText ? (
|
{terminalData?.stale && localAgeText ? (
|
||||||
<span className="scan-status-chip stale">
|
<span className="scan-status-chip stale">
|
||||||
{isEn ? "Local fetch " : "本地下发 "}{localAgeText}
|
{isEn ? "Local fetch " : "本地下发 "}
|
||||||
|
{localAgeText}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
{isPro ? (
|
{isPro ? (
|
||||||
@@ -557,12 +577,13 @@ function ScanTerminalScreen() {
|
|||||||
onClick={refreshScanTerminalManually}
|
onClick={refreshScanTerminalManually}
|
||||||
disabled={scanLoading}
|
disabled={scanLoading}
|
||||||
title={
|
title={
|
||||||
isEn
|
isEn ? "Force refresh decision cards" : "强制刷新决策卡"
|
||||||
? "Force refresh decision cards"
|
|
||||||
: "强制刷新决策卡"
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<RefreshCw size={14} className={scanLoading ? "spin" : undefined} />
|
<RefreshCw
|
||||||
|
size={14}
|
||||||
|
className={scanLoading ? "spin" : undefined}
|
||||||
|
/>
|
||||||
{isEn ? "Refresh" : "刷新"}
|
{isEn ? "Refresh" : "刷新"}
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -613,4 +634,3 @@ export function ScanTerminalDashboard() {
|
|||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user