feat: add token sync time display and file monitoring status

- Add last sync time display for each OAuth provider (Kiro, Gemini, Qwen)
- Add file hash monitoring indicator (5s interval) in provider panels
- Add Token sync status card in Dashboard with:
  - Token status
  - Refresh token availability
  - Last sync time
  - Last check time
- Auto-reload credentials when file changes detected
- Update eslint config to ignore _prefixed variables
This commit is contained in:
coso
2025-12-14 10:59:04 +08:00
parent fa69ceeaee
commit 172d50318a
3 changed files with 165 additions and 7 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ export default [
...tseslint.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_", caughtErrorsIgnorePattern: "^_" }],
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_", caughtErrorsIgnorePattern: "^_" }],
"@typescript-eslint/no-explicit-any": "off",
},
},
+100 -3
View File
@@ -1,4 +1,4 @@
import { useState, useEffect } from "react";
import { useState, useEffect, useRef } from "react";
import {
Activity,
Server,
@@ -9,6 +9,7 @@ import {
Check,
ChevronDown,
ChevronUp,
RefreshCw,
} from "lucide-react";
import {
startServer,
@@ -20,6 +21,10 @@ import {
ServerStatus,
Config,
TestResult,
getKiroCredentials,
checkAndReloadCredentials,
getTokenFileHash,
KiroCredentialStatus,
} from "@/hooks/useTauri";
interface TestState {
@@ -39,6 +44,14 @@ export function Dashboard() {
const [copiedCmd, setCopiedCmd] = useState<string | null>(null);
const [expandedTest, setExpandedTest] = useState<string | null>(null);
// Token sync state
const [kiroStatus, setKiroStatus] = useState<KiroCredentialStatus | null>(
null,
);
const [lastSyncTime, setLastSyncTime] = useState<Date | null>(null);
const [lastCheckTime, setLastCheckTime] = useState<Date | null>(null);
const kiroHashRef = useRef<string>("");
const fetchStatus = async () => {
try {
const s = await getServerStatus();
@@ -60,10 +73,50 @@ export function Dashboard() {
useEffect(() => {
fetchStatus();
fetchConfig();
const interval = setInterval(fetchStatus, 2000);
return () => clearInterval(interval);
loadKiroStatus();
initTokenHash();
const statusInterval = setInterval(fetchStatus, 2000);
const tokenInterval = setInterval(checkTokenFileChanges, 5000);
return () => {
clearInterval(statusInterval);
clearInterval(tokenInterval);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const initTokenHash = async () => {
try {
kiroHashRef.current = await getTokenFileHash();
} catch (e) {
console.error("Failed to get initial hash:", e);
}
};
const loadKiroStatus = async () => {
try {
const status = await getKiroCredentials();
setKiroStatus(status);
} catch (e) {
console.error("Failed to load Kiro status:", e);
}
};
const checkTokenFileChanges = async () => {
setLastCheckTime(new Date());
try {
const result = await checkAndReloadCredentials(kiroHashRef.current);
kiroHashRef.current = result.new_hash;
if (result.changed && result.reloaded) {
await loadKiroStatus();
setLastSyncTime(new Date());
}
} catch (e) {
console.error("Token check error:", e);
}
};
const handleStart = async () => {
setLoading(true);
setError(null);
@@ -281,6 +334,50 @@ export function Dashboard() {
</div>
</div>
{/* Token 同步状态 */}
<div className="rounded-lg border bg-card p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="font-semibold flex items-center gap-2">
<RefreshCw className="h-4 w-4" />
Token
</h3>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<span className="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
(5s)
</div>
</div>
<div className="grid grid-cols-4 gap-4 text-sm">
<div>
<span className="text-muted-foreground">Token :</span>
<span
className={`ml-2 ${kiroStatus?.has_access_token ? "text-green-600" : "text-red-500"}`}
>
{kiroStatus?.has_access_token ? "✓ 已加载" : "✗ 未加载"}
</span>
</div>
<div>
<span className="text-muted-foreground">Refresh Token:</span>
<span
className={`ml-2 ${kiroStatus?.has_refresh_token ? "text-green-600" : "text-red-500"}`}
>
{kiroStatus?.has_refresh_token ? "✓ 可用" : "✗ 不可用"}
</span>
</div>
<div>
<span className="text-muted-foreground">:</span>
<span className="ml-2">
{lastSyncTime ? lastSyncTime.toLocaleTimeString() : "从未同步"}
</span>
</div>
<div>
<span className="text-muted-foreground">:</span>
<span className="ml-2">
{lastCheckTime ? lastCheckTime.toLocaleTimeString() : "-"}
</span>
</div>
</div>
</div>
{error && (
<div className="rounded-lg border border-red-500 bg-red-50 p-4 text-red-700">
{error}
+64 -3
View File
@@ -104,12 +104,14 @@ export function Providers() {
);
const [kiroEnvVars, setKiroEnvVars] = useState<EnvVariable[]>([]);
const kiroHashRef = useRef<string>("");
const [kiroLastSync, setKiroLastSync] = useState<Date | null>(null);
// Gemini state
const [geminiStatus, setGeminiStatus] =
useState<GeminiCredentialStatus | null>(null);
const [geminiEnvVars, setGeminiEnvVars] = useState<EnvVariable[]>([]);
const geminiHashRef = useRef<string>("");
const [geminiLastSync, setGeminiLastSync] = useState<Date | null>(null);
// Qwen state
const [qwenStatus, setQwenStatus] = useState<QwenCredentialStatus | null>(
@@ -117,6 +119,10 @@ export function Providers() {
);
const [qwenEnvVars, setQwenEnvVars] = useState<EnvVariable[]>([]);
const qwenHashRef = useRef<string>("");
const [qwenLastSync, setQwenLastSync] = useState<Date | null>(null);
// Last check time (used for display)
const [_lastCheckTime, setLastCheckTime] = useState<Date | null>(null);
// OpenAI Custom state
const [openaiStatus, setOpenaiStatus] = useState<OpenAICustomStatus | null>(
@@ -176,12 +182,15 @@ export function Providers() {
}, []);
const checkFileChanges = async () => {
setLastCheckTime(new Date());
// Check Kiro
try {
const kiroResult = await checkAndReloadCredentials(kiroHashRef.current);
kiroHashRef.current = kiroResult.new_hash;
if (kiroResult.changed && kiroResult.reloaded) {
await loadKiroStatus();
setKiroLastSync(new Date());
setMessage({
type: "success",
text: "[Kiro] 检测到凭证文件变化,已自动重新加载",
@@ -200,6 +209,7 @@ export function Providers() {
geminiHashRef.current = geminiResult.new_hash;
if (geminiResult.changed && geminiResult.reloaded) {
await loadGeminiStatus();
setGeminiLastSync(new Date());
setMessage({
type: "success",
text: "[Gemini] 检测到凭证文件变化,已自动重新加载",
@@ -218,6 +228,7 @@ export function Providers() {
qwenHashRef.current = qwenResult.new_hash;
if (qwenResult.changed && qwenResult.reloaded) {
await loadQwenStatus();
setQwenLastSync(new Date());
setMessage({
type: "success",
text: "[Qwen] 检测到凭证文件变化,已自动重新加载",
@@ -336,14 +347,17 @@ export function Providers() {
await reloadCredentials();
await loadKiroStatus();
kiroHashRef.current = await getTokenFileHash();
setKiroLastSync(new Date());
} else if (provider === "gemini") {
await reloadGeminiCredentials();
await loadGeminiStatus();
geminiHashRef.current = await getGeminiTokenFileHash();
setGeminiLastSync(new Date());
} else if (provider === "qwen") {
await reloadQwenCredentials();
await loadQwenStatus();
qwenHashRef.current = await getQwenTokenFileHash();
setQwenLastSync(new Date());
}
setMessage({ type: "success", text: `[${provider}] 凭证加载成功!` });
} catch (e: any) {
@@ -470,6 +484,11 @@ export function Providers() {
}
};
const formatTime = (date: Date | null) => {
if (!date) return "从未同步";
return date.toLocaleTimeString();
};
const currentEnvVars =
activeProvider === "kiro"
? kiroEnvVars
@@ -529,7 +548,21 @@ export function Providers() {
{/* Kiro Panel */}
{activeProvider === "kiro" && (
<div className="rounded-lg border bg-card p-4">
<h3 className="mb-3 font-semibold">Kiro </h3>
<div className="mb-3 flex items-center justify-between">
<h3 className="font-semibold">Kiro </h3>
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span>
:{" "}
<span className="text-foreground">
{formatTime(kiroLastSync)}
</span>
</span>
<span className="flex items-center gap-1">
<span className="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
(5s)
</span>
</div>
</div>
<div className="mb-4 grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-muted-foreground">:</span>
@@ -592,7 +625,21 @@ export function Providers() {
{/* Gemini Panel */}
{activeProvider === "gemini" && (
<div className="rounded-lg border bg-card p-4">
<h3 className="mb-3 font-semibold">Gemini CLI </h3>
<div className="mb-3 flex items-center justify-between">
<h3 className="font-semibold">Gemini CLI </h3>
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span>
:{" "}
<span className="text-foreground">
{formatTime(geminiLastSync)}
</span>
</span>
<span className="flex items-center gap-1">
<span className="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
(5s)
</span>
</div>
</div>
<div className="mb-4 grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-muted-foreground">:</span>
@@ -658,7 +705,21 @@ export function Providers() {
{/* Qwen Panel */}
{activeProvider === "qwen" && (
<div className="rounded-lg border bg-card p-4">
<h3 className="mb-3 font-semibold"></h3>
<div className="mb-3 flex items-center justify-between">
<h3 className="font-semibold"></h3>
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span>
:{" "}
<span className="text-foreground">
{formatTime(qwenLastSync)}
</span>
</span>
<span className="flex items-center gap-1">
<span className="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
(5s)
</span>
</div>
</div>
<div className="mb-4 grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-muted-foreground">:</span>