From 000cb0c53aef52a2fbe4373145bb183054e6b602 Mon Sep 17 00:00:00 2001 From: zhoujun0601 Date: Thu, 13 Aug 2026 07:51:28 -0400 Subject: [PATCH] fix frontend layout shifts and confirm logout --- frontend/src/main.tsx | 1063 +++++++++++++++++++++++++++++++++++++-- frontend/src/styles.css | 584 ++++++++++++++++++++- 2 files changed, 1618 insertions(+), 29 deletions(-) diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 662de06..6c8b9ab 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,29 +1,1036 @@ -import React,{useEffect,useState} from 'react' -import{createRoot}from'react-dom/client' -import{BrowserRouter,Navigate,Route,Routes,useNavigate}from'react-router-dom' -import{Activity,Bell,Check,LogOut,Moon,RefreshCw,Settings,Sun,Trash2,Zap}from'lucide-react' -import{Area,AreaChart,CartesianGrid,ResponsiveContainer,Tooltip,XAxis,YAxis}from'recharts' -import{api,del,post,put}from'./api' -import'./styles.css' +import React, { useEffect, useState } from "react"; +import { createRoot } from "react-dom/client"; +import { + BrowserRouter, + Navigate, + Route, + Routes, + useNavigate, +} from "react-router-dom"; +import { + Activity, + Bell, + Check, + LogOut, + Moon, + RefreshCw, + Settings, + Sun, + Trash2, + Zap, +} from "lucide-react"; +import { + Area, + AreaChart, + CartesianGrid, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; +import { api, del, post, put } from "./api"; +import "./styles.css"; -type Status={initialized:boolean;appServer:boolean;version:string};type Limit={limitId:string;limitName?:string;windowType:string;usedPercent:number;windowDurationMinutes:number;resetsAt:number};type Point={date:string;totalTokens:number};type Account={id:number;displayName:string;email?:string;planType?:string;connected:boolean};type Dash={accountId:number;displayName:string;account:{email?:string;planType?:string;connected:boolean};limits:Limit[];summary:{lifetimeTokens?:number;peakDailyTokens?:number;currentStreakDays?:number;longestRunningTurnSec?:number};usage:Point[];fetchedAt:number;stale:boolean;lastError?:string}; -function App(){const[s,setS]=useState(null);const[auth,setAuth]=useState(null);useEffect(()=>{api('system/status').then(x=>{setS(x);if(x.initialized)api('auth/me').then(()=>setAuth(true)).catch(()=>setAuth(false));else setAuth(false)})},[]);if(!s||auth===null)return ;if(!s.initialized)return {setS({...s,initialized:true});setAuth(true)}}/>;return {auth?setAuth(false)}/>}/>:setAuth(true)}/>}/>}} -const Splash=()=>
Codex Helper
; -function Setup({onDone}:{onDone:()=>void}){const[form,set]=useState({username:'admin',password:'',timezone:Intl.DateTimeFormat().resolvedOptions().timeZone});const[err,setErr]=useState('');async function submit(e:React.FormEvent){e.preventDefault();try{await post('setup',form);onDone()}catch(x){setErr((x as Error).message)}}return
Codex Helper

连接你的 Codex
看见每一次增长

一个容器内完成用量洞察、重置提醒与账户管理。

首次初始化 · 1 / 1

创建管理员

{err&&

{err}

}

Codex、Telegram 与 SMTP 可在进入系统后通过设置中心完成。

} -function Login({done}:{done:()=>void}){const[u,setU]=useState('admin'),[p,setP]=useState(''),[e,setE]=useState('');return
{x.preventDefault();try{await post('auth/login',{username:u,password:p});done()}catch(q){setE((q as Error).message)}}}>
Codex Helper

欢迎回来

{e&&

{e}

}
} -function Shell({logout}:{logout:()=>void}){const nav=useNavigate();const[theme,setTheme]=useState(localStorage.theme||'system');useEffect(()=>{document.documentElement.dataset.theme=theme==='system'?(matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light'):theme;localStorage.theme=theme},[theme]);return
}/>}/>}/>
} -function Dashboard(){const[accounts,setAccounts]=useState([]),[id,setId]=useState(+(localStorage.accountId||0)),[d,setD]=useState(null),[e,setE]=useState(''),[refresh,setRefresh]=useState<'idle'|'loading'|'done'>('idle');const loadAccounts=()=>api('accounts').then(xs=>{setAccounts(xs);const next=xs.some(x=>x.id===id)?id:xs[0]?.id||0;if(next!==id)setId(next)});const load=async(accountId=id)=>{if(!accountId)return;try{setD(await api('dashboard?accountId='+accountId));setE('')}catch(x){setE((x as Error).message)}};useEffect(()=>{loadAccounts()},[]);useEffect(()=>{if(!id)return;localStorage.accountId=String(id);setD(null);load(id);const t=setInterval(()=>load(id),30000);return()=>clearInterval(t)},[id]);const sync=async()=>{setRefresh('loading');setE('');try{await post(`accounts/${id}/sync`);await load(id);setRefresh('done');setTimeout(()=>setRefresh('idle'),2000)}catch(x){setE((x as Error).message);setRefresh('idle')}};if(!accounts.length)return <>
尚未添加 Codex 账号
;if(!d)return ;return <>
{e&&
{e}
}
Codex Account{d.displayName} · {d.account.email||'尚未连接'}
{d.account.planType||'未识别套餐'}更新于 {d.fetchedAt?new Date(d.fetchedAt*1000).toLocaleString():'尚未同步'}
{d.limits.map((x,i)=>)}
{!d.limits.length&&
该连接暂无限额数据,请确认登录后刷新。
}
} -function LimitCard({x}:{x:Limit}){const used=Math.min(100,Math.max(0,x.usedPercent)),left=100-used,usedLabel=Math.round(used),leftLabel=Math.round(left),leftColor=`hsl(${left*1.2} 70% 45%)`;return
余额 · {x.windowType}
{leftLabel}%剩余
下次重置{x.resetsAt?new Date(x.resetsAt*1000).toLocaleString():'暂未提供'}{x.resetsAt?relative(x.resetsAt):''}
已使用 {usedLabel}%剩余 {leftLabel}%
} -const Stat=({label,v,muted=false}:{label:string,v:string,muted?:boolean})=>
{label}{v}
; -const Chart=({data}:{data:Point[]})=>

每日 Token 趋势

[new Intl.NumberFormat('zh-CN').format(Number(v)),'Tokens']} contentStyle={{background:'var(--panel)',border:'1px solid var(--border)',borderRadius:12}}/>
; -function SettingsPage(){const[tab,setTab]=useState('general');return <>
{tab==='general'?:tab==='codex'?:tab==='telegram'?:}
} -function General(){const[v,setV]=useState(null),[msg,setMsg]=useState('');useEffect(()=>{api('settings/general').then(setV)},[]);if(!v)return ;return
{e.preventDefault();try{await api('settings/general',{method:'PUT',body:JSON.stringify(v)});setMsg('设置已保存')}catch(x){setMsg((x as Error).message)}}}>

通用设置

{msg&&

{msg}

}
} -function CodexSettings(){const[xs,setXs]=useState([]),[result,setResult]=useState(null),[active,setActive]=useState(0),[busy,setBusy]=useState(false),[err,setErr]=useState('');const load=()=>api('accounts').then(setXs);useEffect(()=>{load()},[]);const login=async(id:number)=>{try{setBusy(true);setActive(id);setErr('');setResult(await post(`accounts/${id}/login/device`))}catch(q){setErr((q as Error).message)}finally{setBusy(false)}};const add=async()=>{try{setBusy(true);setActive(0);setErr('');const x=await post('accounts',{displayName:`账号 ${xs.length+1}`});await load();setActive(x.id);setResult(await post(`accounts/${x.id}/login/device`))}catch(q){setErr((q as Error).message)}finally{setBusy(false)}};return

Codex 账户与工作区

个人订阅和 Team 工作区请分别添加为独立连接;同一邮箱可以添加多次。

{xs.map(x=>
{const name=e.target.value.trim();if(name&&name!==x.displayName){await put(`accounts/${x.id}`,{displayName:name});load()}}}/>{x.email||'尚未登录'} · {x.planType||'未识别套餐'}
)}
{result&&
为“{xs.find(x=>x.id===active)?.displayName}”访问 {result.verificationUrl}{result.userCode}
}{err&&

{err}

}
} -function Telegram(){const[v,setV]=useState(null),[msg,setMsg]=useState(''),[code,setCode]=useState('');useEffect(()=>{api('settings/telegram').then(setV)},[]);if(!v)return ;return
{e.preventDefault();try{const x:any=await api('settings/telegram',{method:'PUT',body:JSON.stringify(v)});setV(x);setMsg('Bot 已验证并保存')}catch(x){setMsg((x as Error).message)}}}>

Telegram Bot

{code&&
向 Bot 发送/bind {code}
}{msg&&

{msg}

}
} -function SMTP(){const[v,setV]=useState(null),[msg,setMsg]=useState('');useEffect(()=>{api('settings/smtp').then(setV)},[]);if(!v)return ;const save=async(e:React.FormEvent)=>{e.preventDefault();try{setV(await api('settings/smtp',{method:'PUT',body:JSON.stringify(v)}));setMsg('SMTP 设置已保存')}catch(x){setMsg((x as Error).message)}};return

SMTP 邮件

{msg&&

{msg}

}
} -function Header({title,sub,children}:{title:string,sub?:string,children?:React.ReactNode}){return

{title}

{sub&&

{sub}

}
{children}
} -const num=(v?:number)=>v==null?'暂无':new Intl.NumberFormat('zh-CN',{notation:'compact',maximumFractionDigits:1}).format(v); -const compactAxis=(v:number)=>new Intl.NumberFormat('en',{notation:'compact',maximumFractionDigits:1}).format(v); -const duration=(v?:number)=>v==null?'暂无':v<60?`${v} 秒`:v<3600?`${Math.floor(v/60)} 分 ${v%60} 秒`:`${(v/3600).toFixed(1)} 小时`; -const relative=(ts:number)=>{const s=Math.max(0,ts-Math.floor(Date.now()/1000));if(!s)return'即将重置';const d=Math.floor(s/86400),h=Math.floor(s%86400/3600),m=Math.floor(s%3600/60);return d?`${d} 天 ${h} 小时后`:h?`${h} 小时 ${m} 分后`:`${m} 分钟后`}; -createRoot(document.getElementById('root')!).render() +type Status = { initialized: boolean; appServer: boolean; version: string }; +type Limit = { + limitId: string; + limitName?: string; + windowType: string; + usedPercent: number; + windowDurationMinutes: number; + resetsAt: number; +}; +type Point = { date: string; totalTokens: number }; +type Account = { + id: number; + displayName: string; + email?: string; + planType?: string; + connected: boolean; +}; +type Dash = { + accountId: number; + displayName: string; + account: { email?: string; planType?: string; connected: boolean }; + limits: Limit[]; + summary: { + lifetimeTokens?: number; + peakDailyTokens?: number; + currentStreakDays?: number; + longestRunningTurnSec?: number; + }; + usage: Point[]; + fetchedAt: number; + stale: boolean; + lastError?: string; +}; +function App() { + const [s, setS] = useState(null); + const [auth, setAuth] = useState(null); + useEffect(() => { + api("system/status").then((x) => { + setS(x); + if (x.initialized) + api("auth/me") + .then(() => setAuth(true)) + .catch(() => setAuth(false)); + else setAuth(false); + }); + }, []); + if (!s || auth === null) return ; + if (!s.initialized) + return ( + { + setS({ ...s, initialized: true }); + setAuth(true); + }} + /> + ); + return ( + + + {auth ? ( + setAuth(false)} />} /> + ) : ( + setAuth(true)} />} /> + )} + + + ); +} +const Splash = () => ( +
+
+ Codex Helper +
+
+
+); +function Setup({ onDone }: { onDone: () => void }) { + const [form, set] = useState({ + username: "admin", + password: "", + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + }); + const [err, setErr] = useState(""); + async function submit(e: React.FormEvent) { + e.preventDefault(); + try { + await post("setup", form); + onDone(); + } catch (x) { + setErr((x as Error).message); + } + } + return ( +
+
+
+ Codex Helper +
+

+ 连接你的 Codex +
+ 看见每一次增长 +

+

一个容器内完成用量洞察、重置提醒与账户管理。

+
+
+ 首次初始化 · 1 / 1 +

创建管理员

+ + + + {err &&

{err}

} + +

+ Codex、Telegram 与 SMTP 可在进入系统后通过设置中心完成。 +

+
+
+ ); +} +function Login({ done }: { done: () => void }) { + const [u, setU] = useState("admin"), + [p, setP] = useState(""), + [e, setE] = useState(""); + return ( +
+
{ + x.preventDefault(); + try { + await post("auth/login", { username: u, password: p }); + done(); + } catch (q) { + setE((q as Error).message); + } + }} + > +
+ Codex Helper +
+

欢迎回来

+ + + {e &&

{e}

} + +
+
+ ); +} +function Shell({ logout }: { logout: () => void }) { + const nav = useNavigate(); + const [theme, setTheme] = useState(localStorage.theme || "system"); + useEffect(() => { + document.documentElement.dataset.theme = + theme === "system" + ? matchMedia("(prefers-color-scheme:dark)").matches + ? "dark" + : "light" + : theme; + localStorage.theme = theme; + }, [theme]); + return ( +
+ +
+ + } /> + } /> + } /> + +
+
+ ); +} +function Dashboard() { + const [accounts, setAccounts] = useState([]), + [id, setId] = useState(+(localStorage.accountId || 0)), + [d, setD] = useState(null), + [e, setE] = useState(""), + [refresh, setRefresh] = useState<"idle" | "loading" | "done">("idle"); + const loadAccounts = () => + api("accounts").then((xs) => { + setAccounts(xs); + const next = xs.some((x) => x.id === id) ? id : xs[0]?.id || 0; + if (next !== id) setId(next); + }); + const load = async (accountId = id) => { + if (!accountId) return; + try { + setD(await api("dashboard?accountId=" + accountId)); + setE(""); + } catch (x) { + setE((x as Error).message); + } + }; + useEffect(() => { + loadAccounts(); + }, []); + useEffect(() => { + if (!id) return; + localStorage.accountId = String(id); + setD(null); + load(id); + const t = setInterval(() => load(id), 30000); + return () => clearInterval(t); + }, [id]); + const sync = async () => { + setRefresh("loading"); + setE(""); + try { + await post(`accounts/${id}/sync`); + await load(id); + setRefresh("done"); + setTimeout(() => setRefresh("idle"), 2000); + } catch (x) { + setE((x as Error).message); + setRefresh("idle"); + } + }; + if (!accounts.length) + return ( + <> +
+
尚未添加 Codex 账号
+ + ); + if (!d) return ; + return ( + <> +
+
+ + +
+
+ {e &&
{e}
} +
+ +
+ Codex Account + + {d.displayName} · {d.account.email || "尚未连接"} + +
+ {d.account.planType || "未识别套餐"} + + 更新于{" "} + {d.fetchedAt + ? new Date(d.fetchedAt * 1000).toLocaleString() + : "尚未同步"} + +
+
+ {d.limits.map((x, i) => ( + + ))} +
+ {!d.limits.length && ( +
+ 该连接暂无限额数据,请确认登录后刷新。 +
+ )} +
+ + + + +
+ + + ); +} +function LimitCard({ x }: { x: Limit }) { + const used = Math.min(100, Math.max(0, x.usedPercent)), + left = 100 - used, + usedLabel = Math.round(used), + leftLabel = Math.round(left), + leftColor = `hsl(${left * 1.2} 70% 45%)`; + return ( +
+ 余额 · {x.windowType} +
+ + {leftLabel} + % + + 剩余 +
+
+ 下次重置 + + {x.resetsAt + ? new Date(x.resetsAt * 1000).toLocaleString() + : "暂未提供"} + + {x.resetsAt ? relative(x.resetsAt) : ""} +
+
+ + +
+
+ 已使用 {usedLabel}% + 剩余 {leftLabel}% +
+
+ ); +} +const Stat = ({ + label, + v, + muted = false, +}: { + label: string; + v: string; + muted?: boolean; +}) => ( +
+ {label} + {v} +
+); +const Chart = ({ data }: { data: Point[] }) => ( +
+
+

每日 Token 趋势

+
+ + + + + + + + + + + + [ + new Intl.NumberFormat("zh-CN").format(Number(v)), + "Tokens", + ]} + contentStyle={{ + background: "var(--panel)", + border: "1px solid var(--border)", + borderRadius: 12, + }} + /> + + + +
+); +function SettingsPage() { + const [tab, setTab] = useState("general"); + return ( + <> +
+
+
+ + + + +
+ {tab === "general" ? ( + + ) : tab === "codex" ? ( + + ) : tab === "telegram" ? ( + + ) : ( + + )} +
+ + ); +} +const SettingsLoading = () => ( +
+
+
+); +function General() { + const [v, setV] = useState(null), + [msg, setMsg] = useState(""); + useEffect(() => { + api("settings/general").then(setV); + }, []); + if (!v) return ; + return ( +
{ + e.preventDefault(); + try { + await api("settings/general", { + method: "PUT", + body: JSON.stringify(v), + }); + setMsg("设置已保存"); + } catch (x) { + setMsg((x as Error).message); + } + }} + > +

通用设置

+
+ + + + + + +
+ + {msg &&

{msg}

} +
+ ); +} +function CodexSettings() { + const [xs, setXs] = useState([]), + [result, setResult] = useState(null), + [active, setActive] = useState(0), + [busy, setBusy] = useState(false), + [err, setErr] = useState(""); + const load = () => api("accounts").then(setXs); + useEffect(() => { + load(); + }, []); + const login = async (id: number) => { + try { + setBusy(true); + setActive(id); + setErr(""); + setResult(await post(`accounts/${id}/login/device`)); + } catch (q) { + setErr((q as Error).message); + } finally { + setBusy(false); + } + }; + const add = async () => { + try { + setBusy(true); + setActive(0); + setErr(""); + const x = await post("accounts", { + displayName: `账号 ${xs.length + 1}`, + }); + await load(); + setActive(x.id); + setResult(await post(`accounts/${x.id}/login/device`)); + } catch (q) { + setErr((q as Error).message); + } finally { + setBusy(false); + } + }; + return ( +
+
+
+

Codex 账户与工作区

+

+ 个人订阅和 Team 工作区请分别添加为独立连接;同一邮箱可以添加多次。 +

+
+ +
+
+ {xs.map((x) => ( +
+ +
+ { + const name = e.target.value.trim(); + if (name && name !== x.displayName) { + await put(`accounts/${x.id}`, { displayName: name }); + load(); + } + }} + /> + + {x.email || "尚未登录"} · {x.planType || "未识别套餐"} + +
+ + + +
+ ))} +
+ {result && ( +
+ + 为“{xs.find((x) => x.id === active)?.displayName}”访问{" "} + + {result.verificationUrl} + + + {result.userCode} +
+ )} + {err &&

{err}

} +
+ ); +} +function Telegram() { + const [v, setV] = useState(null), + [msg, setMsg] = useState(""), + [code, setCode] = useState(""); + useEffect(() => { + api("settings/telegram").then(setV); + }, []); + if (!v) return ; + return ( +
{ + e.preventDefault(); + try { + const x: any = await api("settings/telegram", { + method: "PUT", + body: JSON.stringify(v), + }); + setV(x); + setMsg("Bot 已验证并保存"); + } catch (x) { + setMsg((x as Error).message); + } + }} + > +

Telegram Bot

+ +
+ + +
+ +
+ + +
+ {code && ( +
+ 向 Bot 发送 + /bind {code} +
+ )} + {msg &&

{msg}

} +
+ ); +} +function SMTP() { + const [v, setV] = useState(null), + [msg, setMsg] = useState(""); + useEffect(() => { + api("settings/smtp").then(setV); + }, []); + if (!v) return ; + const save = async (e: React.FormEvent) => { + e.preventDefault(); + try { + setV( + await api("settings/smtp", { method: "PUT", body: JSON.stringify(v) }), + ); + setMsg("SMTP 设置已保存"); + } catch (x) { + setMsg((x as Error).message); + } + }; + return ( +
+

SMTP 邮件

+
+ + + + + + + + +
+ +
+ + +
+ {msg &&

{msg}

} +
+ ); +} +function Header({ + title, + sub, + children, +}: { + title: string; + sub?: string; + children?: React.ReactNode; +}) { + return ( +
+
+

{title}

+ {sub &&

{sub}

} +
+ {children} +
+ ); +} +const num = (v?: number) => + v == null + ? "暂无" + : new Intl.NumberFormat("zh-CN", { + notation: "compact", + maximumFractionDigits: 1, + }).format(v); +const compactAxis = (v: number) => + new Intl.NumberFormat("en", { + notation: "compact", + maximumFractionDigits: 1, + }).format(v); +const duration = (v?: number) => + v == null + ? "暂无" + : v < 60 + ? `${v} 秒` + : v < 3600 + ? `${Math.floor(v / 60)} 分 ${v % 60} 秒` + : `${(v / 3600).toFixed(1)} 小时`; +const relative = (ts: number) => { + const s = Math.max(0, ts - Math.floor(Date.now() / 1000)); + if (!s) return "即将重置"; + const d = Math.floor(s / 86400), + h = Math.floor((s % 86400) / 3600), + m = Math.floor((s % 3600) / 60); + return d ? `${d} 天 ${h} 小时后` : h ? `${h} 小时 ${m} 分后` : `${m} 分钟后`; +}; +createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/frontend/src/styles.css b/frontend/src/styles.css index b9a4e23..8573507 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -1,2 +1,584 @@ /* Fully self-contained: no remote font dependency. */ -:root{font-family:Inter,"PingFang SC",sans-serif;color-scheme:light;--bg:#f4f7fa;--panel:#fff;--text:#101820;--muted:#71808d;--border:#dfe7ec;--accent:#25bfd5;--accent2:#596ef3;--track:#e8eff2;--grid:#e5ecef;--danger:#e84e61;--green:#25b978}html[data-theme=dark]{color-scheme:dark;--bg:#071015;--panel:#0d181f;--text:#edfafd;--muted:#7f98a4;--border:#1b3039;--track:#263740;--grid:#183039;--green:#38d996}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 85% 5%,color-mix(in srgb,var(--accent) 9%,transparent),transparent 32%),var(--bg);color:var(--text)}button,input,select{font:inherit}.center{min-height:100vh;display:grid;place-content:center;gap:24px}.spinner{width:34px;height:34px;border:3px solid var(--track);border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite;margin:auto}@keyframes spin{to{transform:rotate(360deg)}}.logo{display:flex;align-items:center;gap:10px;font-weight:700;letter-spacing:-.03em}.logo svg{color:var(--accent);width:22px}.setup{min-height:100vh;display:grid;grid-template-columns:1.1fr .9fr;align-items:center;gap:8vw;padding:7vw}.hero h1{font-size:clamp(42px,5vw,76px);line-height:1.05;letter-spacing:-.055em}.hero h1 span{color:var(--accent)}.hero p{color:var(--muted);font-size:18px}.panel{background:color-mix(in srgb,var(--panel) 94%,transparent);border:1px solid var(--border);border-radius:18px;box-shadow:0 16px 50px rgba(0,0,0,.05)}.form{padding:32px;display:flex;flex-direction:column;gap:18px}.form h2{margin:0;font-size:26px}.form small,.hint,.form p{color:var(--muted)}label{display:flex;flex-direction:column;gap:8px;font-size:13px;color:var(--muted)}input,select{border:1px solid var(--border);background:var(--bg);color:var(--text);border-radius:10px;padding:12px;outline:none}input:focus,select:focus{border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 16%,transparent)}button{border:0;border-radius:10px;padding:11px 16px;background:linear-gradient(120deg,var(--accent),var(--accent2));color:#fff;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:8px;font-weight:600}button:disabled{opacity:.65;cursor:not-allowed}button svg{width:17px}.login{width:min(420px,90vw)}.error{color:var(--danger)!important}.app{display:grid;grid-template-columns:230px 1fr;min-height:100vh}aside{border-right:1px solid var(--border);padding:28px 18px;display:flex;flex-direction:column;position:sticky;top:0;height:100vh;background:color-mix(in srgb,var(--bg) 88%,transparent);backdrop-filter:blur(15px)}aside .logo{padding:0 10px 28px}nav{display:flex;flex-direction:column;gap:6px}aside button,.tabs button{justify-content:flex-start;background:transparent;color:var(--muted)}aside button:hover,.tabs button:hover,.tabs button.active{background:var(--track);color:var(--text)}.aside-bottom{margin-top:auto;display:flex;flex-direction:column;gap:5px}.content{padding:42px;max-width:1500px;width:100%;margin:auto}header{display:flex;align-items:center;justify-content:space-between;margin-bottom:28px}header h1{margin:0;font-size:30px;letter-spacing:-.04em}header p{margin:8px 0;color:var(--muted)}.header-actions{display:flex;gap:10px}.account-select{min-width:220px}.secondary{background:var(--panel);border:1px solid var(--border);color:var(--text)}.refresh.loading svg{animation:spin .8s linear infinite}.refresh.done{color:var(--green);border-color:var(--green)}.account{padding:20px 24px;display:flex;gap:14px;align-items:center;margin-bottom:20px}.account div{display:flex;flex-direction:column}.account small,.limit small,.stat small,.chart small{color:var(--muted)}.dot{flex:none;width:10px;height:10px;border-radius:50%;background:var(--danger);box-shadow:0 0 10px currentColor}.dot.ok{background:var(--green)}.badge{background:var(--track);padding:6px 10px;border-radius:99px;font-size:12px}.fresh{margin-left:auto;color:var(--muted);font-size:12px}.limits{display:grid;grid-template-columns:repeat(auto-fit,minmax(350px,1fr));gap:18px}.limit{padding:24px;display:grid;grid-template-columns:1fr auto;gap:16px;align-items:start}.limit>small{grid-column:1/-1}.remaining strong{display:block;font-size:50px;line-height:1;font-variant-numeric:tabular-nums;color:var(--green)}.remaining strong i{font-size:20px;font-style:normal}.remaining span,.reset span{display:block;color:var(--muted);font-size:12px;margin-top:7px}.reset{text-align:right}.reset b{display:block;font-size:18px;margin-top:7px}.bar{grid-column:1/-1;display:flex;height:14px;border-radius:99px;overflow:hidden;background:var(--track)}.bar i{background:#9aa6ad}.bar em{background:var(--green)}.limit footer{grid-column:1/-1;border-top:1px solid var(--border);padding-top:14px;display:flex;gap:18px;color:var(--muted);font-size:12px}.limit footer span:last-child{margin-left:auto}.green{color:var(--green)}.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin:20px 0}.stat{padding:20px}.stat b{display:block;font-size:24px;margin-top:10px}.chart{padding:24px}.chart h3{margin:0 0 5px}.banner{padding:14px 18px;border:1px solid color-mix(in srgb,var(--danger) 35%,var(--border));background:color-mix(in srgb,var(--danger) 8%,var(--panel));border-radius:12px;margin-bottom:18px;color:var(--danger)}.empty{padding:32px;text-align:center;color:var(--muted)}.settings{display:grid;grid-template-columns:190px 1fr;gap:20px}.tabs{display:flex;flex-direction:column;gap:5px}.wide{max-width:960px}.grid2{display:grid;grid-template-columns:1fr 1fr;gap:16px}.checks{display:flex;flex-direction:row;align-items:center;gap:10px}.checks input{width:auto}.actions,.settings-title{display:flex;gap:10px}.settings-title{justify-content:space-between;align-items:start}.settings-title p{margin-bottom:0}.danger{background:transparent;border:1px solid var(--danger);color:var(--danger)}.icon{padding:10px}.codebox{padding:16px;border:1px dashed var(--accent);border-radius:12px;display:flex;justify-content:space-between;align-items:center}.codebox strong{font-family:monospace;font-size:20px;color:var(--accent)}.account-list{display:flex;flex-direction:column;gap:10px}.account-row{display:grid;grid-template-columns:auto minmax(180px,1fr) auto auto auto;gap:10px;align-items:center;padding:14px;border:1px solid var(--border);border-radius:12px}.account-row>div{display:flex;flex-direction:column;gap:5px}.account-row input{padding:7px;font-weight:700}.account-row small{padding-left:8px}a{color:var(--accent)}@media(max-width:800px){.setup{grid-template-columns:1fr;padding:24px}.hero{display:none}.app{grid-template-columns:1fr}aside{position:static;height:auto;border:0;padding:14px;flex-direction:row;align-items:center;overflow:auto}aside .logo{padding:0;margin-right:auto}aside nav{flex-direction:row}.aside-bottom{margin:0;flex-direction:row}aside button{font-size:0;padding:10px}.content{padding:24px 16px}header{align-items:start;gap:15px}.header-actions{flex-direction:column}.account-select{min-width:0}.stats{grid-template-columns:1fr 1fr}.settings{grid-template-columns:1fr}.tabs{flex-direction:row;overflow:auto}.grid2{grid-template-columns:1fr}.fresh{display:none}.account-row{grid-template-columns:auto 1fr}.account-row button{grid-column:auto}.limit{grid-template-columns:1fr}.reset{text-align:left}.limit>small,.bar,.limit footer{grid-column:1}.settings-title{flex-direction:column}}@media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}} +:root { + font-family: Inter, "PingFang SC", sans-serif; + color-scheme: light; + scrollbar-gutter: stable; + --bg: #f4f7fa; + --panel: #fff; + --text: #101820; + --muted: #71808d; + --border: #dfe7ec; + --accent: #25bfd5; + --accent2: #596ef3; + --track: #e8eff2; + --grid: #e5ecef; + --danger: #e84e61; + --green: #25b978; +} +html[data-theme="dark"] { + color-scheme: dark; + --bg: #071015; + --panel: #0d181f; + --text: #edfafd; + --muted: #7f98a4; + --border: #1b3039; + --track: #263740; + --grid: #183039; + --green: #38d996; +} +* { + box-sizing: border-box; +} +body { + margin: 0; + background: + radial-gradient( + circle at 85% 5%, + color-mix(in srgb, var(--accent) 9%, transparent), + transparent 32% + ), + var(--bg); + color: var(--text); +} +button, +input, +select { + font: inherit; +} +.center { + min-height: 100vh; + display: grid; + place-content: center; + gap: 24px; +} +.spinner { + width: 34px; + height: 34px; + border: 3px solid var(--track); + border-top-color: var(--accent); + border-radius: 50%; + animation: spin 0.8s linear infinite; + margin: auto; +} +@keyframes spin { + to { + transform: rotate(360deg); + } +} +.logo { + display: flex; + align-items: center; + gap: 10px; + font-weight: 700; + letter-spacing: -0.03em; +} +.logo svg { + color: var(--accent); + width: 22px; +} +.setup { + min-height: 100vh; + display: grid; + grid-template-columns: 1.1fr 0.9fr; + align-items: center; + gap: 8vw; + padding: 7vw; +} +.hero h1 { + font-size: clamp(42px, 5vw, 76px); + line-height: 1.05; + letter-spacing: -0.055em; +} +.hero h1 span { + color: var(--accent); +} +.hero p { + color: var(--muted); + font-size: 18px; +} +.panel { + background: color-mix(in srgb, var(--panel) 94%, transparent); + border: 1px solid var(--border); + border-radius: 18px; + box-shadow: 0 16px 50px rgba(0, 0, 0, 0.05); +} +.form { + padding: 32px; + display: flex; + flex-direction: column; + gap: 18px; +} +.form h2 { + margin: 0; + font-size: 26px; +} +.form small, +.hint, +.form p { + color: var(--muted); +} +label { + display: flex; + flex-direction: column; + gap: 8px; + font-size: 13px; + color: var(--muted); +} +input, +select { + border: 1px solid var(--border); + background: var(--bg); + color: var(--text); + border-radius: 10px; + padding: 12px; + outline: none; +} +input:focus, +select:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 16%, transparent); +} +button { + border: 0; + border-radius: 10px; + padding: 11px 16px; + background: linear-gradient(120deg, var(--accent), var(--accent2)); + color: #fff; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + font-weight: 600; +} +button:disabled { + opacity: 0.65; + cursor: not-allowed; +} +button svg { + width: 17px; +} +.login { + width: min(420px, 90vw); +} +.error { + color: var(--danger) !important; +} +.app { + display: grid; + grid-template-columns: 230px 1fr; + min-height: 100vh; +} +aside { + border-right: 1px solid var(--border); + padding: 28px 18px; + display: flex; + flex-direction: column; + position: sticky; + top: 0; + height: 100vh; + background: color-mix(in srgb, var(--bg) 88%, transparent); + backdrop-filter: blur(15px); +} +aside .logo { + padding: 0 10px 28px; +} +nav { + display: flex; + flex-direction: column; + gap: 6px; +} +aside button, +.tabs button { + justify-content: flex-start; + background: transparent; + color: var(--muted); +} +aside button:hover, +.tabs button:hover, +.tabs button.active { + background: var(--track); + color: var(--text); +} +.aside-bottom { + margin-top: auto; + display: flex; + flex-direction: column; + gap: 5px; +} +.content { + padding: 42px; + max-width: 1500px; + width: 100%; + margin: auto; +} +header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 28px; +} +header h1 { + margin: 0; + font-size: 30px; + letter-spacing: -0.04em; +} +header p { + margin: 8px 0; + color: var(--muted); +} +.header-actions { + display: flex; + gap: 10px; +} +.account-select { + min-width: 220px; +} +.secondary { + background: var(--panel); + border: 1px solid var(--border); + color: var(--text); +} +.refresh.loading svg { + animation: spin 0.8s linear infinite; +} +.refresh { + width: 116px; + white-space: nowrap; +} +.refresh.done { + color: var(--green); + border-color: var(--green); +} +.account { + padding: 20px 24px; + display: flex; + gap: 14px; + align-items: center; + margin-bottom: 20px; +} +.account div { + display: flex; + flex-direction: column; +} +.account small, +.limit small, +.stat small, +.chart small { + color: var(--muted); +} +.dot { + flex: none; + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--danger); + box-shadow: 0 0 10px currentColor; +} +.dot.ok { + background: var(--green); +} +.badge { + background: var(--track); + padding: 6px 10px; + border-radius: 99px; + font-size: 12px; +} +.fresh { + margin-left: auto; + color: var(--muted); + font-size: 12px; +} +.limits { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); + gap: 18px; +} +.limit { + padding: 24px; + display: grid; + grid-template-columns: 1fr auto; + gap: 16px; + align-items: start; +} +.limit > small { + grid-column: 1/-1; +} +.remaining strong { + display: block; + font-size: 50px; + line-height: 1; + font-variant-numeric: tabular-nums; + color: var(--green); +} +.remaining strong i { + font-size: 20px; + font-style: normal; +} +.remaining span, +.reset span { + display: block; + color: var(--muted); + font-size: 12px; + margin-top: 7px; +} +.reset { + text-align: right; +} +.reset b { + display: block; + font-size: 18px; + margin-top: 7px; +} +.bar { + grid-column: 1/-1; + display: flex; + height: 14px; + border-radius: 99px; + overflow: hidden; + background: var(--track); +} +.bar i { + background: #9aa6ad; +} +.bar em { + background: var(--green); +} +.limit footer { + grid-column: 1/-1; + border-top: 1px solid var(--border); + padding-top: 14px; + display: flex; + gap: 18px; + color: var(--muted); + font-size: 12px; +} +.limit footer span:last-child { + margin-left: auto; +} +.green { + color: var(--green); +} +.stats { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 14px; + margin: 20px 0; +} +.stat { + padding: 20px; +} +.stat b { + display: block; + font-size: 24px; + margin-top: 10px; +} +.chart { + padding: 24px; +} +.chart h3 { + margin: 0 0 5px; +} +.banner { + padding: 14px 18px; + border: 1px solid color-mix(in srgb, var(--danger) 35%, var(--border)); + background: color-mix(in srgb, var(--danger) 8%, var(--panel)); + border-radius: 12px; + margin-bottom: 18px; + color: var(--danger); +} +.empty { + padding: 32px; + text-align: center; + color: var(--muted); +} +.settings { + display: grid; + grid-template-columns: 190px minmax(0, 1fr); + gap: 20px; + align-items: start; +} +.tabs { + display: flex; + flex-direction: column; + gap: 5px; +} +.wide { + width: 100%; + max-width: 960px; + min-height: 480px; +} +.settings-loading { + display: grid; + place-content: center; +} +.grid2 { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} +.checks { + display: flex; + flex-direction: row; + align-items: center; + gap: 10px; +} +.checks input { + width: auto; +} +.actions, +.settings-title { + display: flex; + gap: 10px; +} +.settings-title { + justify-content: space-between; + align-items: start; +} +.settings-title p { + margin-bottom: 0; +} +.danger { + background: transparent; + border: 1px solid var(--danger); + color: var(--danger); +} +.icon { + padding: 10px; +} +.codebox { + padding: 16px; + border: 1px dashed var(--accent); + border-radius: 12px; + display: flex; + justify-content: space-between; + align-items: center; +} +.codebox strong { + font-family: monospace; + font-size: 20px; + color: var(--accent); +} +.account-list { + display: flex; + flex-direction: column; + gap: 10px; +} +.account-row { + display: grid; + grid-template-columns: auto minmax(180px, 1fr) auto auto auto; + gap: 10px; + align-items: center; + padding: 14px; + border: 1px solid var(--border); + border-radius: 12px; +} +.account-row > div { + display: flex; + flex-direction: column; + gap: 5px; +} +.account-row input { + padding: 7px; + font-weight: 700; +} +.account-row small { + padding-left: 8px; +} +a { + color: var(--accent); +} +@media (max-width: 800px) { + .setup { + grid-template-columns: 1fr; + padding: 24px; + } + .hero { + display: none; + } + .app { + grid-template-columns: 1fr; + } + aside { + position: static; + height: auto; + border: 0; + padding: 14px; + flex-direction: row; + align-items: center; + overflow: auto; + } + aside .logo { + padding: 0; + margin-right: auto; + } + aside nav { + flex-direction: row; + } + .aside-bottom { + margin: 0; + flex-direction: row; + } + aside button { + font-size: 0; + padding: 10px; + } + .content { + padding: 24px 16px; + } + header { + align-items: start; + gap: 15px; + } + .header-actions { + flex-direction: column; + } + .refresh { + width: 100%; + } + .account-select { + min-width: 0; + } + .stats { + grid-template-columns: 1fr 1fr; + } + .settings { + grid-template-columns: 1fr; + } + .tabs { + flex-direction: row; + overflow: auto; + } + .grid2 { + grid-template-columns: 1fr; + } + .fresh { + display: none; + } + .account-row { + grid-template-columns: auto 1fr; + } + .account-row button { + grid-column: auto; + } + .limit { + grid-template-columns: 1fr; + } + .reset { + text-align: left; + } + .limit > small, + .bar, + .limit footer { + grid-column: 1; + } + .settings-title { + flex-direction: column; + } +} +@media (prefers-reduced-motion: reduce) { + * { + animation: none !important; + transition: none !important; + } +}