mirror of
https://github.com/YouXam/claude-code-usage-dashboard.git
synced 2026-02-04 15:10:16 +08:00
fix: resets time update
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { UsageProgressBar } from './UsageProgressBar';
|
import { UsageProgressBar } from './UsageProgressBar';
|
||||||
|
|
||||||
interface ClaudeAccount {
|
interface ClaudeAccount {
|
||||||
@@ -73,17 +73,20 @@ export function AIAccounts({ apiKey }: AIAccountsProps) {
|
|||||||
const [openaiAccounts, setOpenaiAccounts] = useState<OpenAIAccount[]>([]);
|
const [openaiAccounts, setOpenaiAccounts] = useState<OpenAIAccount[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
const [error, setError] = useState<string>('');
|
const [refreshError, setRefreshError] = useState<string>('');
|
||||||
|
const [initialError, setInitialError] = useState<string>('');
|
||||||
|
const hasLoadedDataRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchAccounts();
|
fetchAccounts('initial');
|
||||||
const interval = setInterval(fetchAccounts, 30000);
|
const interval = setInterval(() => fetchAccounts('auto'), 30000);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [apiKey]);
|
}, [apiKey]);
|
||||||
|
|
||||||
const fetchAccounts = async (isRefresh = false) => {
|
const fetchAccounts = async (type: 'initial' | 'manual' | 'auto' = 'auto') => {
|
||||||
if (isRefresh) {
|
if (type === 'manual') {
|
||||||
setIsRefreshing(true);
|
setIsRefreshing(true);
|
||||||
|
setRefreshError('');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -100,18 +103,34 @@ export function AIAccounts({ apiKey }: AIAccountsProps) {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setClaudeAccounts(data.claude || []);
|
setClaudeAccounts(data.claude || []);
|
||||||
setOpenaiAccounts(data.openai || []);
|
setOpenaiAccounts(data.openai || []);
|
||||||
setError('');
|
setRefreshError('');
|
||||||
|
setInitialError('');
|
||||||
|
hasLoadedDataRef.current = true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching AI accounts:', err);
|
console.error('Error fetching AI accounts:', err);
|
||||||
setError(err instanceof Error ? err.message : 'Failed to load AI accounts');
|
const errorMessage = err instanceof Error ? err.message : 'Failed to load AI accounts';
|
||||||
|
|
||||||
|
if (type === 'initial') {
|
||||||
|
setInitialError(errorMessage);
|
||||||
|
} else if (hasLoadedDataRef.current) {
|
||||||
|
// Only set refresh error if we have previously loaded data successfully
|
||||||
|
setRefreshError(errorMessage);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
if (type === 'initial') {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
if (isRefresh) {
|
}
|
||||||
|
if (type === 'manual') {
|
||||||
setIsRefreshing(false);
|
setIsRefreshing(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRetry = () => {
|
||||||
|
setRefreshError('');
|
||||||
|
fetchAccounts('manual');
|
||||||
|
};
|
||||||
|
|
||||||
const formatLastUsed = (lastUsedAt: string | null) => {
|
const formatLastUsed = (lastUsedAt: string | null) => {
|
||||||
if (!lastUsedAt) return 'Never';
|
if (!lastUsedAt) return 'Never';
|
||||||
const date = new Date(lastUsedAt);
|
const date = new Date(lastUsedAt);
|
||||||
@@ -127,20 +146,6 @@ export function AIAccounts({ apiKey }: AIAccountsProps) {
|
|||||||
return `${diffDays}d ago`;
|
return `${diffDays}d ago`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatTime = (seconds: number) => {
|
|
||||||
if (seconds <= 0) return '0m';
|
|
||||||
const days = Math.floor(seconds / 86400);
|
|
||||||
const hours = Math.floor((seconds % 86400) / 3600);
|
|
||||||
const minutes = Math.floor((seconds % 3600) / 60);
|
|
||||||
|
|
||||||
if (days > 0) {
|
|
||||||
if (hours > 0) return `${days}d ${hours}h`;
|
|
||||||
return `${days}d`;
|
|
||||||
}
|
|
||||||
if (hours > 0) return `${hours}h ${minutes}m`;
|
|
||||||
return `${minutes}m`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
const getStatusColor = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'active':
|
case 'active':
|
||||||
@@ -186,16 +191,16 @@ export function AIAccounts({ apiKey }: AIAccountsProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (initialError) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-card rounded-lg shadow-sm border border-border overflow-hidden mb-6">
|
<div className="bg-card rounded-lg shadow-sm border border-border overflow-hidden mb-6">
|
||||||
<div className="px-6 py-4 border-b border-border">
|
<div className="px-6 py-4 border-b border-border">
|
||||||
<h3 className="text-lg font-medium text-card-foreground">AI Accounts Status</h3>
|
<h3 className="text-lg font-medium text-card-foreground">AI Accounts Status</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<p className="text-destructive">{error}</p>
|
<p className="text-destructive">{initialError}</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => fetchAccounts(true)}
|
onClick={() => fetchAccounts('initial')}
|
||||||
className="mt-3 text-sm text-primary hover:underline"
|
className="mt-3 text-sm text-primary hover:underline"
|
||||||
>
|
>
|
||||||
Retry
|
Retry
|
||||||
@@ -210,8 +215,20 @@ export function AIAccounts({ apiKey }: AIAccountsProps) {
|
|||||||
<div className="px-6 py-4 border-b border-border">
|
<div className="px-6 py-4 border-b border-border">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-lg font-medium text-card-foreground">AI Accounts Status</h3>
|
<h3 className="text-lg font-medium text-card-foreground">AI Accounts Status</h3>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{refreshError && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm text-destructive">{refreshError}</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => fetchAccounts(true)}
|
onClick={handleRetry}
|
||||||
|
className="text-sm text-primary hover:underline"
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => fetchAccounts('manual')}
|
||||||
disabled={isRefreshing}
|
disabled={isRefreshing}
|
||||||
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-secondary text-secondary-foreground rounded-md hover:bg-secondary/80 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-secondary text-secondary-foreground rounded-md hover:bg-secondary/80 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
@@ -227,6 +244,7 @@ export function AIAccounts({ apiKey }: AIAccountsProps) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{allAccounts.length > 0 ? (
|
{allAccounts.length > 0 ? (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
@@ -275,27 +293,21 @@ export function AIAccounts({ apiKey }: AIAccountsProps) {
|
|||||||
{claudeAcc.claudeUsage.fiveHour && (
|
{claudeAcc.claudeUsage.fiveHour && (
|
||||||
<UsageProgressBar
|
<UsageProgressBar
|
||||||
label="5h Window"
|
label="5h Window"
|
||||||
resetTime={formatTime(claudeAcc.claudeUsage.fiveHour.remainingSeconds)}
|
|
||||||
percentage={claudeAcc.claudeUsage.fiveHour.utilization}
|
percentage={claudeAcc.claudeUsage.fiveHour.utilization}
|
||||||
resetAfterSeconds={claudeAcc.claudeUsage.fiveHour.remainingSeconds}
|
|
||||||
resetAt={claudeAcc.claudeUsage.fiveHour.resetsAt}
|
resetAt={claudeAcc.claudeUsage.fiveHour.resetsAt}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{claudeAcc.claudeUsage.sevenDay && (
|
{claudeAcc.claudeUsage.sevenDay && (
|
||||||
<UsageProgressBar
|
<UsageProgressBar
|
||||||
label="7d Window"
|
label="7d Window"
|
||||||
resetTime={formatTime(claudeAcc.claudeUsage.sevenDay.remainingSeconds)}
|
|
||||||
percentage={claudeAcc.claudeUsage.sevenDay.utilization}
|
percentage={claudeAcc.claudeUsage.sevenDay.utilization}
|
||||||
resetAfterSeconds={claudeAcc.claudeUsage.sevenDay.remainingSeconds}
|
|
||||||
resetAt={claudeAcc.claudeUsage.sevenDay.resetsAt}
|
resetAt={claudeAcc.claudeUsage.sevenDay.resetsAt}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{claudeAcc.claudeUsage.sevenDayOpus && (
|
{claudeAcc.claudeUsage.sevenDayOpus && (
|
||||||
<UsageProgressBar
|
<UsageProgressBar
|
||||||
label="Opus Window"
|
label="Opus Window"
|
||||||
resetTime={formatTime(claudeAcc.claudeUsage.sevenDayOpus.remainingSeconds)}
|
|
||||||
percentage={claudeAcc.claudeUsage.sevenDayOpus.utilization}
|
percentage={claudeAcc.claudeUsage.sevenDayOpus.utilization}
|
||||||
resetAfterSeconds={claudeAcc.claudeUsage.sevenDayOpus.remainingSeconds}
|
|
||||||
resetAt={claudeAcc.claudeUsage.sevenDayOpus.resetsAt}
|
resetAt={claudeAcc.claudeUsage.sevenDayOpus.resetsAt}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -305,18 +317,14 @@ export function AIAccounts({ apiKey }: AIAccountsProps) {
|
|||||||
{openaiAcc.codexUsage.primary && (
|
{openaiAcc.codexUsage.primary && (
|
||||||
<UsageProgressBar
|
<UsageProgressBar
|
||||||
label="5h Window"
|
label="5h Window"
|
||||||
resetTime={formatTime(openaiAcc.codexUsage.primary.resetAfterSeconds)}
|
|
||||||
percentage={openaiAcc.codexUsage.primary.usedPercent}
|
percentage={openaiAcc.codexUsage.primary.usedPercent}
|
||||||
resetAfterSeconds={openaiAcc.codexUsage.primary.resetAfterSeconds}
|
|
||||||
resetAt={openaiAcc.codexUsage.primary.resetAt}
|
resetAt={openaiAcc.codexUsage.primary.resetAt}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{openaiAcc.codexUsage.secondary && (
|
{openaiAcc.codexUsage.secondary && (
|
||||||
<UsageProgressBar
|
<UsageProgressBar
|
||||||
label="7d Window"
|
label="7d Window"
|
||||||
resetTime={formatTime(openaiAcc.codexUsage.secondary.resetAfterSeconds)}
|
|
||||||
percentage={openaiAcc.codexUsage.secondary.usedPercent}
|
percentage={openaiAcc.codexUsage.secondary.usedPercent}
|
||||||
resetAfterSeconds={openaiAcc.codexUsage.secondary.resetAfterSeconds}
|
|
||||||
resetAt={openaiAcc.codexUsage.secondary.resetAt}
|
resetAt={openaiAcc.codexUsage.secondary.resetAt}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,18 +1,65 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
interface UsageProgressBarProps {
|
interface UsageProgressBarProps {
|
||||||
label: string;
|
label: string;
|
||||||
resetTime: string;
|
|
||||||
percentage: number;
|
percentage: number;
|
||||||
resetAfterSeconds?: number;
|
resetAt: string;
|
||||||
resetAt?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UsageProgressBar({ label, resetTime, percentage, resetAfterSeconds, resetAt }: UsageProgressBarProps) {
|
export function UsageProgressBar({ label, percentage, resetAt }: UsageProgressBarProps) {
|
||||||
// Check if reset time has passed
|
const [remainingTime, setRemainingTime] = useState('');
|
||||||
const resetElapsed =
|
const [resetElapsed, setResetElapsed] = useState(false);
|
||||||
resetAfterSeconds !== undefined && (
|
|
||||||
resetAfterSeconds <= 0 ||
|
useEffect(() => {
|
||||||
(resetAt && !isNaN(Date.parse(resetAt)) && Date.now() >= Date.parse(resetAt))
|
const updateRemainingTime = () => {
|
||||||
);
|
const resetTimestamp = Date.parse(resetAt);
|
||||||
|
|
||||||
|
if (isNaN(resetTimestamp)) {
|
||||||
|
setRemainingTime('--');
|
||||||
|
setResetElapsed(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const diffMs = resetTimestamp - now;
|
||||||
|
|
||||||
|
if (diffMs <= 0) {
|
||||||
|
setResetElapsed(true);
|
||||||
|
setRemainingTime('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setResetElapsed(false);
|
||||||
|
|
||||||
|
const totalSeconds = Math.floor(diffMs / 1000);
|
||||||
|
const days = Math.floor(totalSeconds / 86400);
|
||||||
|
const hours = Math.floor((totalSeconds % 86400) / 3600);
|
||||||
|
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||||
|
const seconds = totalSeconds % 60;
|
||||||
|
|
||||||
|
if (days > 0) {
|
||||||
|
if (hours > 0) {
|
||||||
|
setRemainingTime(`${days}d ${hours}h`);
|
||||||
|
} else {
|
||||||
|
setRemainingTime(`${days}d`);
|
||||||
|
}
|
||||||
|
} else if (hours > 0) {
|
||||||
|
setRemainingTime(`${hours}h ${minutes}m`);
|
||||||
|
} else if (minutes > 0) {
|
||||||
|
setRemainingTime(`${minutes}m ${seconds}s`);
|
||||||
|
} else {
|
||||||
|
setRemainingTime(`${seconds}s`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial update
|
||||||
|
updateRemainingTime();
|
||||||
|
|
||||||
|
// Update every second
|
||||||
|
const interval = setInterval(updateRemainingTime, 1000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [resetAt]);
|
||||||
|
|
||||||
// If reset time has passed, show 0%
|
// If reset time has passed, show 0%
|
||||||
const displayPercentage = resetElapsed ? 0 : percentage;
|
const displayPercentage = resetElapsed ? 0 : percentage;
|
||||||
@@ -20,7 +67,7 @@ export function UsageProgressBar({ label, resetTime, percentage, resetAfterSecon
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs text-muted-foreground mb-1">
|
<div className="text-xs text-muted-foreground mb-1">
|
||||||
{label}{!resetElapsed && ` · Resets in ${resetTime}`}
|
{label}{!resetElapsed && remainingTime && ` · Resets in ${remainingTime}`}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex-1 h-2 bg-chart-3 rounded-full overflow-hidden">
|
<div className="flex-1 h-2 bg-chart-3 rounded-full overflow-hidden">
|
||||||
|
|||||||
Reference in New Issue
Block a user