Maximus upgradus
This commit is contained in:
+249
-82
@@ -4,20 +4,81 @@ import {
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
RefreshControl,
|
||||
ActivityIndicator,
|
||||
} from "react-native";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import { router, useFocusEffect } from "expo-router";
|
||||
import { useCallback } from "react";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { MaterialIcons } from "@expo/vector-icons";
|
||||
import { useCodexUsage } from "@/hooks/useCodexUsage";
|
||||
import { useClaudeUsage } from "@/hooks/useClaudeUsage";
|
||||
import { ServiceStatusCard } from "@/components/ServiceStatusCard";
|
||||
import { onUsageDataLoaded } from "@/lib/notifications";
|
||||
import { ProgressBar } from "@/components/ProgressBar";
|
||||
import { ResetCountdown } from "@/components/ResetCountdown";
|
||||
import { windowLabel } from "@/lib/timeUtils";
|
||||
|
||||
function UsageRow({
|
||||
label,
|
||||
percent,
|
||||
resetAtSeconds,
|
||||
resetAtISO,
|
||||
}: {
|
||||
label: string;
|
||||
percent: number;
|
||||
resetAtSeconds?: number;
|
||||
resetAtISO?: string;
|
||||
}) {
|
||||
const color =
|
||||
percent >= 90 ? "#ef4444" : percent >= 70 ? "#f59e0b" : "#10a37f";
|
||||
return (
|
||||
<View className="mb-4">
|
||||
<View className="flex-row justify-between items-baseline mb-1.5">
|
||||
<Text className="text-xs font-medium text-neutral-500 dark:text-neutral-400">
|
||||
{label}
|
||||
</Text>
|
||||
<Text className="text-sm font-bold" style={{ color }}>
|
||||
{Math.round(percent)}% used
|
||||
</Text>
|
||||
</View>
|
||||
<ProgressBar percent={percent} />
|
||||
<ResetCountdown resetAtSeconds={resetAtSeconds} resetAtISO={resetAtISO} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function SetupPrompt({ label }: { label: string }) {
|
||||
return (
|
||||
<View>
|
||||
<Text className="text-sm text-neutral-400 dark:text-neutral-500 mb-3">
|
||||
{label}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
onPress={() => router.navigate("/(tabs)/settings")}
|
||||
className="flex-row items-center self-start gap-x-1.5 px-3 py-2 rounded-xl border border-neutral-200 dark:border-neutral-700"
|
||||
>
|
||||
<Text className="text-xs font-semibold text-neutral-600 dark:text-neutral-300">
|
||||
Set up in Settings
|
||||
</Text>
|
||||
<MaterialIcons name="arrow-forward" size={12} color="#737373" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DashboardTab() {
|
||||
const codex = useCodexUsage();
|
||||
const claude = useClaudeUsage();
|
||||
|
||||
// Fire daily digest reschedule + threshold check whenever fresh data arrives
|
||||
useEffect(() => {
|
||||
if (codex.status === "success" || claude.status === "success") {
|
||||
void onUsageDataLoaded(
|
||||
claude.status === "success" ? claude.usage : null,
|
||||
codex.status === "success" ? codex.usage : null
|
||||
);
|
||||
}
|
||||
}, [codex.status, claude.status]);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
codex.reload();
|
||||
@@ -33,46 +94,16 @@ export default function DashboardTab() {
|
||||
claude.refresh();
|
||||
};
|
||||
|
||||
const codexRows =
|
||||
codex.usage
|
||||
? [
|
||||
{
|
||||
label: `Primary (${windowLabel(codex.usage.rate_limit.primary_window.limit_window_seconds)})`,
|
||||
percent: codex.usage.rate_limit.primary_window.used_percent,
|
||||
resetAtSeconds: codex.usage.rate_limit.primary_window.reset_at,
|
||||
},
|
||||
{
|
||||
label: `Secondary (${windowLabel(codex.usage.rate_limit.secondary_window.limit_window_seconds)})`,
|
||||
percent: codex.usage.rate_limit.secondary_window.used_percent,
|
||||
resetAtSeconds: codex.usage.rate_limit.secondary_window.reset_at,
|
||||
},
|
||||
]
|
||||
: undefined;
|
||||
|
||||
const claudeRows =
|
||||
claude.usage
|
||||
? [
|
||||
{
|
||||
label: "5-hour window",
|
||||
percent: claude.usage.five_hour.utilization,
|
||||
resetAtISO: claude.usage.five_hour.resets_at,
|
||||
},
|
||||
{
|
||||
label: "7-day window",
|
||||
percent: claude.usage.seven_day.utilization,
|
||||
resetAtISO: claude.usage.seven_day.resets_at,
|
||||
},
|
||||
]
|
||||
: undefined;
|
||||
const connectedCount = [codex.auth, claude.auth].filter(Boolean).length;
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
className="flex-1 bg-neutral-50 dark:bg-neutral-950"
|
||||
edges={["top", "bottom"]}
|
||||
edges={["top"]}
|
||||
>
|
||||
<ScrollView
|
||||
className="flex-1"
|
||||
contentContainerStyle={{ paddingHorizontal: 16, paddingBottom: 32 }}
|
||||
contentContainerStyle={{ paddingHorizontal: 16, paddingBottom: 40 }}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isRefreshing}
|
||||
@@ -82,14 +113,23 @@ export default function DashboardTab() {
|
||||
}
|
||||
>
|
||||
{/* Header */}
|
||||
<View className="flex-row items-center justify-between py-5">
|
||||
<View>
|
||||
<Text className="text-2xl font-bold text-neutral-900 dark:text-white tracking-tight">
|
||||
Codexbar
|
||||
</Text>
|
||||
<Text className="text-xs text-neutral-400 mt-0.5">
|
||||
AI usage monitor
|
||||
</Text>
|
||||
<View className="flex-row items-center justify-between pt-6 pb-5">
|
||||
<View className="flex-row items-center gap-x-3">
|
||||
<View className="w-10 h-10 rounded-2xl bg-emerald-500 items-center justify-center">
|
||||
<MaterialIcons name="bar-chart" size={20} color="white" />
|
||||
</View>
|
||||
<View>
|
||||
<Text className="text-xl font-bold text-neutral-900 dark:text-white tracking-tight">
|
||||
Codexbar
|
||||
</Text>
|
||||
<Text className="text-xs text-neutral-400 mt-0.5">
|
||||
{connectedCount === 0
|
||||
? "No services connected"
|
||||
: connectedCount === 2
|
||||
? "2 services connected"
|
||||
: "1 of 2 services connected"}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={handleRefresh}
|
||||
@@ -100,48 +140,175 @@ export default function DashboardTab() {
|
||||
</View>
|
||||
|
||||
{/* Codex card */}
|
||||
<ServiceStatusCard
|
||||
title="Codex CLI"
|
||||
icon="auto-awesome"
|
||||
accentColor="#10a37f"
|
||||
status={!codex.auth ? "idle" : codex.status}
|
||||
badge={codex.usage?.plan_type}
|
||||
rows={codexRows}
|
||||
unconfiguredLabel="Import auth.json to see your Codex rate limits."
|
||||
onConfigurePress={() => router.navigate("/(tabs)/codex")}
|
||||
footer={
|
||||
codex.usage && !codex.usage.credits.unlimited && codex.usage.credits.has_credits
|
||||
? (
|
||||
<View className="flex-row items-center gap-x-2">
|
||||
<MaterialIcons name="toll" size={14} color="#a3a3a3" />
|
||||
<Text className="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
${Number(codex.usage.credits.balance).toFixed(2)} credits remaining
|
||||
</Text>
|
||||
<View className="rounded-2xl bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 mb-4 overflow-hidden">
|
||||
<View style={{ height: 3, backgroundColor: "#10a37f" }} />
|
||||
<View className="p-4">
|
||||
<View className="flex-row items-center justify-between mb-4">
|
||||
<View className="flex-row items-center gap-x-2.5">
|
||||
<View
|
||||
className="w-8 h-8 rounded-xl items-center justify-center"
|
||||
style={{ backgroundColor: "#10a37f18" }}
|
||||
>
|
||||
<MaterialIcons name="auto-awesome" size={16} color="#10a37f" />
|
||||
</View>
|
||||
)
|
||||
: codex.usage?.credits.unlimited
|
||||
? (
|
||||
<View className="flex-row items-center gap-x-2">
|
||||
<MaterialIcons name="all-inclusive" size={14} color="#10a37f" />
|
||||
<Text className="text-xs text-green-600 dark:text-green-400">
|
||||
Unlimited credits
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
: null
|
||||
}
|
||||
/>
|
||||
<Text className="text-sm font-semibold text-neutral-900 dark:text-white">
|
||||
Codex CLI
|
||||
</Text>
|
||||
</View>
|
||||
<View className="flex-row items-center gap-x-2">
|
||||
{codex.usage?.plan_type && (
|
||||
<View className="px-2.5 py-1 rounded-full bg-green-100 dark:bg-green-900/30">
|
||||
<Text className="text-xs font-semibold text-green-700 dark:text-green-300 capitalize">
|
||||
{codex.usage.plan_type}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{codex.auth && codex.status === "error" && (
|
||||
<View className="w-2 h-2 rounded-full bg-red-500" />
|
||||
)}
|
||||
{codex.status === "success" && !codex.usage?.plan_type && (
|
||||
<View className="w-2 h-2 rounded-full bg-green-500" />
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{!codex.auth && (
|
||||
<SetupPrompt label="Import auth.json to see your Codex rate limits." />
|
||||
)}
|
||||
{codex.auth && codex.status === "loading" && (
|
||||
<ActivityIndicator
|
||||
color="#10a37f"
|
||||
style={{ marginVertical: 16 }}
|
||||
/>
|
||||
)}
|
||||
{codex.auth && codex.status === "error" && (
|
||||
<View className="flex-row items-center gap-x-2 py-1">
|
||||
<MaterialIcons name="error-outline" size={16} color="#ef4444" />
|
||||
<Text className="text-sm text-red-500 dark:text-red-400">
|
||||
Failed to fetch usage
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{codex.status === "success" && codex.usage && (
|
||||
<View>
|
||||
<UsageRow
|
||||
label={`${windowLabel(codex.usage.rate_limit.primary_window.limit_window_seconds)} window`}
|
||||
percent={codex.usage.rate_limit.primary_window.used_percent}
|
||||
resetAtSeconds={codex.usage.rate_limit.primary_window.reset_at}
|
||||
/>
|
||||
<UsageRow
|
||||
label={`${windowLabel(codex.usage.rate_limit.secondary_window.limit_window_seconds)} window`}
|
||||
percent={
|
||||
codex.usage.rate_limit.secondary_window.used_percent
|
||||
}
|
||||
resetAtSeconds={
|
||||
codex.usage.rate_limit.secondary_window.reset_at
|
||||
}
|
||||
/>
|
||||
{!codex.usage.credits.unlimited &&
|
||||
codex.usage.credits.has_credits && (
|
||||
<View className="pt-3 border-t border-neutral-100 dark:border-neutral-800 flex-row items-center gap-x-2">
|
||||
<MaterialIcons name="toll" size={14} color="#a3a3a3" />
|
||||
<Text className="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
${Number(codex.usage.credits.balance).toFixed(2)}{" "}
|
||||
credits remaining
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{codex.usage.credits.unlimited && (
|
||||
<View className="pt-3 border-t border-neutral-100 dark:border-neutral-800 flex-row items-center gap-x-2">
|
||||
<MaterialIcons
|
||||
name="all-inclusive"
|
||||
size={14}
|
||||
color="#10a37f"
|
||||
/>
|
||||
<Text className="text-xs text-green-600 dark:text-green-400">
|
||||
Unlimited credits
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Claude card */}
|
||||
<ServiceStatusCard
|
||||
title="Claude.ai"
|
||||
icon="psychology"
|
||||
accentColor="#d97706"
|
||||
status={!claude.sessionKey ? "idle" : claude.status}
|
||||
rows={claudeRows}
|
||||
unconfiguredLabel="Add your session key to see Claude usage windows."
|
||||
onConfigurePress={() => router.navigate("/(tabs)/claude")}
|
||||
/>
|
||||
<View className="rounded-2xl bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 mb-4 overflow-hidden">
|
||||
<View style={{ height: 3, backgroundColor: "#d97706" }} />
|
||||
<View className="p-4">
|
||||
<View className="flex-row items-center justify-between mb-4">
|
||||
<View className="flex-row items-center gap-x-2.5">
|
||||
<View
|
||||
className="w-8 h-8 rounded-xl items-center justify-center"
|
||||
style={{ backgroundColor: "#d9770618" }}
|
||||
>
|
||||
<MaterialIcons name="psychology" size={16} color="#d97706" />
|
||||
</View>
|
||||
<Text className="text-sm font-semibold text-neutral-900 dark:text-white">
|
||||
Claude.ai
|
||||
</Text>
|
||||
</View>
|
||||
{claude.auth && claude.status === "error" && (
|
||||
<View className="w-2 h-2 rounded-full bg-red-500" />
|
||||
)}
|
||||
{claude.status === "success" && (
|
||||
<View className="w-2 h-2 rounded-full bg-green-500" />
|
||||
)}
|
||||
</View>
|
||||
|
||||
{!claude.auth && (
|
||||
<SetupPrompt label="Add your session key to see Claude usage windows." />
|
||||
)}
|
||||
{claude.auth && claude.status === "loading" && (
|
||||
<ActivityIndicator
|
||||
color="#d97706"
|
||||
style={{ marginVertical: 16 }}
|
||||
/>
|
||||
)}
|
||||
{claude.auth && claude.status === "error" && (
|
||||
<View className="flex-row items-center gap-x-2 py-1">
|
||||
<MaterialIcons name="error-outline" size={16} color="#ef4444" />
|
||||
<Text className="text-sm text-red-500 dark:text-red-400">
|
||||
Failed to fetch usage
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{claude.status === "success" && claude.usage && (
|
||||
<View>
|
||||
<UsageRow
|
||||
label="5-hour window"
|
||||
percent={claude.usage.five_hour.utilization}
|
||||
resetAtISO={claude.usage.five_hour.resets_at}
|
||||
/>
|
||||
<UsageRow
|
||||
label="7-day window"
|
||||
percent={claude.usage.seven_day.utilization}
|
||||
resetAtISO={claude.usage.seven_day.resets_at}
|
||||
/>
|
||||
{(claude.usage.seven_day_sonnet ||
|
||||
claude.usage.seven_day_opus) && (
|
||||
<View className="pt-3 border-t border-neutral-100 dark:border-neutral-800">
|
||||
<Text className="text-xs font-semibold uppercase tracking-widest text-neutral-400 mb-3">
|
||||
By Model (7-day)
|
||||
</Text>
|
||||
{claude.usage.seven_day_sonnet && (
|
||||
<UsageRow
|
||||
label="Sonnet"
|
||||
percent={claude.usage.seven_day_sonnet.utilization}
|
||||
/>
|
||||
)}
|
||||
{claude.usage.seven_day_opus && (
|
||||
<UsageRow
|
||||
label="Opus"
|
||||
percent={claude.usage.seven_day_opus.utilization}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user