Skip to content

Commit e906451

Browse files
committed
feat(accounts): add usage health insights and oauth reauth flow
1 parent b3df094 commit e906451

27 files changed

Lines changed: 1144 additions & 106 deletions

app.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,11 @@ type RelayLocalApplyResult struct {
153153
ConfigPath string `json:"configPath"`
154154
}
155155

156+
type UsageStatisticsResponse struct {
157+
Usage map[string]interface{} `json:"usage"`
158+
FailedRequests int64 `json:"failedRequests,omitempty"`
159+
}
160+
156161
func NewApp() *App {
157162
return &App{
158163
core: wailsapp.New(Version, ReleaseLabel, GitHubRepo),
@@ -268,6 +273,18 @@ func (a *App) DownloadAuthFile(name string) (*DownloadFileResponse, error) {
268273
}, nil
269274
}
270275

276+
func (a *App) GetUsageStatistics() (*UsageStatisticsResponse, error) {
277+
result, err := a.core.GetUsageStatistics()
278+
if err != nil {
279+
return nil, err
280+
}
281+
282+
return &UsageStatisticsResponse{
283+
Usage: result.Usage,
284+
FailedRequests: result.FailedRequests,
285+
}, nil
286+
}
287+
271288
func (a *App) StartCodexOAuth() (*OAuthStartResult, error) {
272289
result, err := a.core.StartCodexOAuth()
273290
if err != nil {

frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"build": "vite build",
99
"preview": "vite preview",
1010
"typecheck": "tsc --noEmit",
11-
"test:unit": "node --test src/utils/version.test.mjs src/utils/pagePersistence.test.mjs src/features/settings/settingsRelease.test.mjs src/features/accounts/tests/accountConfig.test.mjs src/features/accounts/tests/accountFilters.test.mjs src/features/accounts/tests/accountSelectors.test.mjs src/features/accounts/tests/accountSelection.test.mjs src/features/accounts/tests/accountTransfer.test.mjs src/features/accounts/tests/accountPresentation.test.mjs src/features/accounts/tests/accountOAuth.test.mjs src/features/accounts/tests/accountCardInteractions.test.mjs src/features/accounts/tests/accountRotation.test.mjs src/components/biz/accountDetailClipboard.test.mjs"
11+
"test:unit": "node --test src/utils/version.test.mjs src/utils/pagePersistence.test.mjs src/features/settings/settingsRelease.test.mjs src/features/accounts/tests/accountConfig.test.mjs src/features/accounts/tests/accountFilters.test.mjs src/features/accounts/tests/accountSelectors.test.mjs src/features/accounts/tests/accountSelection.test.mjs src/features/accounts/tests/accountTransfer.test.mjs src/features/accounts/tests/accountPresentation.test.mjs src/features/accounts/tests/accountOAuth.test.mjs src/features/accounts/tests/accountCardInteractions.test.mjs src/features/accounts/tests/accountRotation.test.mjs src/features/accounts/tests/accountUsage.test.mjs src/components/biz/accountDetailClipboard.test.mjs"
1212
},
1313
"dependencies": {
1414
"react": "^18.3.1",

frontend/package.json.md5

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
35baeb6e8a4ffa5af73d4f0f83dc681c
1+
e5044a3b8f400e4e0913d71aa2b6f601

frontend/src/components/biz/AccountDetailModal.tsx

Lines changed: 58 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,18 @@ import { useDebug } from '../../context/DebugContext';
44
import { useI18n } from '../../context/I18nContext';
55
import type { AuthFile, AuthModel } from '../../types';
66
import { toErrorMessage } from '../../utils/error';
7+
import type { AccountUsageSummary } from '../../features/accounts/model/accountUsage';
8+
import AccountHealthBar from '../../features/accounts/components/AccountHealthBar';
79
import { canCopyRawContent, copyRawContent, RAW_CONTENT_COPY_RESET_MS } from './accountDetailClipboard';
810

911
interface AccountDetailModalProps {
1012
account: AuthFile;
13+
usageSummary?: AccountUsageSummary;
1114
canStartReauth?: boolean;
1215
isReauthing?: boolean;
1316
onClose: () => void;
1417
onStartReauth?: () => void;
18+
onCancelReauth?: () => void;
1519
}
1620

1721
type DetailField = readonly [string, string];
@@ -39,10 +43,12 @@ function getModelLabel(model: AuthModel): string {
3943

4044
export default function AccountDetailModal({
4145
account,
46+
usageSummary,
4247
canStartReauth = false,
4348
isReauthing = false,
4449
onClose,
4550
onStartReauth,
51+
onCancelReauth,
4652
}: AccountDetailModalProps) {
4753
const { t } = useI18n();
4854
const { trackRequest } = useDebug();
@@ -55,8 +61,6 @@ export default function AccountDetailModal({
5561
const [sanitizeState, setSanitizeState] = useState<'idle' | 'success' | 'error'>('idle');
5662
const [viewMode, setViewMode] = useState<'raw' | 'sanitized'>('raw');
5763
const [sanitizing, setSanitizing] = useState(false);
58-
const [verifyResult, setVerifyResult] = useState('');
59-
const [verifying, setVerifying] = useState(false);
6064

6165
const detailFields = useMemo<DetailField[]>(
6266
() => [
@@ -65,11 +69,29 @@ export default function AccountDetailModal({
6569
[t('accounts.size'), account.size ? `${account.size} B` : '—'],
6670
[t('common.status'), account.status || '—'],
6771
[t('common.enable'), account.disabled ? 'NO' : 'YES'],
68-
['REFRESH', formatRefreshValue(account.lastRefresh)],
72+
[t('accounts.last_refresh'), formatRefreshValue(account.lastRefresh)],
6973
],
7074
[account, t]
7175
);
7276

77+
const statisticsFields = useMemo<DetailField[]>(
78+
() => [
79+
[
80+
t('accounts.success_rate'),
81+
usageSummary?.successRate !== null && usageSummary?.successRate !== undefined
82+
? `${Math.round(usageSummary.successRate)}%`
83+
: t('accounts.no_recent_activity'),
84+
],
85+
[t('accounts.recent_success'), String(usageSummary?.success ?? 0)],
86+
[t('accounts.recent_failure'), String(usageSummary?.failure ?? 0)],
87+
[
88+
t('accounts.average_latency'),
89+
usageSummary?.averageLatencyMs ? `${usageSummary.averageLatencyMs} ms` : '—',
90+
],
91+
],
92+
[t, usageSummary]
93+
);
94+
7395
useEffect(() => {
7496
let mounted = true;
7597

@@ -193,30 +215,14 @@ export default function AccountDetailModal({
193215
}
194216
}
195217

196-
async function verify() {
197-
setVerifying(true);
198-
setVerifyResult('VERIFYING...');
199-
try {
200-
await trackRequest('GetAuthFileModels', { name: account.name, mode: 'verify' }, () =>
201-
GetAuthFileModels(account.name)
202-
);
203-
setVerifyResult('✓ VALID');
204-
} catch (error) {
205-
console.error(error);
206-
setVerifyResult('✗ FAILED');
207-
} finally {
208-
setVerifying(false);
209-
}
210-
}
211-
212218
return (
213219
<div
214220
className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-8 backdrop-blur-sm"
215221
data-collaboration-id="MODAL_ACCOUNT_DETAIL"
216222
onClick={onClose}
217223
>
218224
<div
219-
className="flex max-h-[90vh] w-full max-w-2xl flex-col border-2 border-[var(--border-color)] bg-[var(--bg-main)] shadow-hard shadow-[var(--shadow-color)]"
225+
className="flex max-h-[90vh] w-full max-w-2xl flex-col border-2 border-[var(--border-color)] bg-[var(--bg-main)] shadow-hard shadow-[var(--shadow-color)]"
220226
onClick={(event: ClickEventLike) => event.stopPropagation()}
221227
>
222228
<header className="flex items-center justify-between border-b-2 border-[var(--border-color)] bg-[var(--bg-main)] px-6 py-4">
@@ -231,11 +237,10 @@ export default function AccountDetailModal({
231237
<div className="flex items-center gap-3">
232238
{canStartReauth ? (
233239
<button
234-
onClick={onStartReauth}
235-
disabled={isReauthing}
240+
onClick={isReauthing ? onCancelReauth : onStartReauth}
236241
className="btn-swiss !px-3 !py-1 !text-[9px]"
237242
>
238-
{isReauthing ? t('accounts.reauth_pending') : t('accounts.reauth')}
243+
{isReauthing ? t('common.cancel') : t('accounts.reauth')}
239244
</button>
240245
) : null}
241246
<button onClick={onClose} className="btn-swiss !p-1 !shadow-none hover:bg-[var(--bg-surface)]">
@@ -256,29 +261,30 @@ export default function AccountDetailModal({
256261
))}
257262
</div>
258263

259-
{canStartReauth ? (
260-
<section className="space-y-4 border-b-2 border-dashed border-[var(--border-color)] pb-8">
261-
<div className="flex items-center gap-2 text-[9px] font-black uppercase tracking-widest text-[var(--text-muted)]">
262-
<span className="h-2 w-2 bg-[var(--border-color)]"></span>
263-
ACCOUNT_ACTIONS
264+
<section className="space-y-4 border-b-2 border-dashed border-[var(--border-color)] pb-8">
265+
<div className="flex items-center justify-between gap-4">
266+
<div className="text-[9px] font-black uppercase tracking-[0.2em] text-[var(--text-muted)]">
267+
{t('accounts.recent_health')}
264268
</div>
265-
<div className="flex items-center justify-between gap-4 border-2 border-[var(--border-color)] bg-[var(--bg-surface)] p-4">
266-
<div className="space-y-1">
267-
<div className="text-[11px] font-black uppercase text-[var(--text-primary)]">{t('accounts.reauth')}</div>
268-
<div className="text-[10px] font-bold uppercase tracking-wide text-[var(--text-muted)]">
269-
{t('accounts.reauth_detail_hint')}
270-
</div>
271-
</div>
272-
<button
273-
onClick={onStartReauth}
274-
disabled={isReauthing}
275-
className="btn-swiss shrink-0 !px-3 !py-2 !text-[9px]"
276-
>
277-
{isReauthing ? t('accounts.reauth_pending') : t('accounts.reauth')}
278-
</button>
269+
<div className="text-[9px] font-black uppercase tracking-[0.16em] text-[var(--text-primary)]">
270+
{usageSummary?.hasData ? t('accounts.stability_signal_synced') : t('accounts.no_recent_activity')}
279271
</div>
280-
</section>
281-
) : null}
272+
</div>
273+
274+
{usageSummary?.hasData ? <AccountHealthBar summary={usageSummary} /> : null}
275+
276+
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
277+
{statisticsFields.map(([label, value]) => (
278+
<div
279+
key={label}
280+
className="space-y-1 border-2 border-dashed border-[var(--border-color)] bg-[var(--bg-surface)] px-3 py-3"
281+
>
282+
<div className="text-[8px] font-black uppercase tracking-[0.2em] text-[var(--text-muted)]">{label}</div>
283+
<div className="text-[12px] font-black uppercase tracking-[0.06em] text-[var(--text-primary)]">{value}</div>
284+
</div>
285+
))}
286+
</div>
287+
</section>
282288

283289
<section className="space-y-4">
284290
<div className="flex items-center justify-between">
@@ -310,13 +316,13 @@ export default function AccountDetailModal({
310316
<span className="h-2 w-2 bg-[var(--border-color)]"></span>
311317
{viewMode === 'sanitized' ? 'SANITIZED_SOURCE_DATA' : 'RAW_SOURCE_DATA'}
312318
</div>
313-
<div className="flex items-center gap-3">
314-
{copyState !== 'idle' || sanitizeState !== 'idle' ? (
315-
<span className="text-[9px] font-black uppercase tracking-[0.14em] text-[var(--text-muted)]">
316-
{copyState === 'success' || sanitizeState === 'success'
317-
? t('accounts.copy_done')
318-
: t('accounts.copy_failed')}
319-
</span>
319+
<div className="flex items-center gap-3">
320+
{copyState !== 'idle' || sanitizeState !== 'idle' ? (
321+
<span className="text-[9px] font-black uppercase tracking-[0.14em] text-[var(--border-color)]">
322+
{copyState === 'success' || sanitizeState === 'success'
323+
? t('accounts.copy_done')
324+
: t('accounts.copy_failed')}
325+
</span>
320326
) : null}
321327
{loadingRaw ? (
322328
<span className="animate-pulse text-[9px] font-black text-[var(--text-muted)]">FETCHING_FS...</span>
@@ -367,25 +373,7 @@ export default function AccountDetailModal({
367373
</section>
368374
</div>
369375

370-
<footer className="flex items-center justify-between border-t-2 border-[var(--border-color)] bg-[var(--bg-surface)] px-6 py-4">
371-
<div className="flex items-center gap-4">
372-
<button
373-
onClick={verify}
374-
disabled={verifying}
375-
className="btn-swiss bg-[var(--border-color)] !text-[var(--bg-main)]"
376-
>
377-
{verifying ? 'VERIFYING...' : 'VERIFY_ACCOUNT'}
378-
</button>
379-
{verifyResult ? (
380-
<span
381-
className={`text-[10px] font-black italic ${
382-
verifyResult.includes('✓') ? 'text-green-600' : 'text-red-600'
383-
}`}
384-
>
385-
{verifyResult}
386-
</span>
387-
) : null}
388-
</div>
376+
<footer className="flex items-center justify-end border-t-2 border-[var(--border-color)] bg-[var(--bg-surface)] px-6 py-4">
389377
<button onClick={onClose} className="btn-swiss">
390378
{t('common.close')}
391379
</button>

frontend/src/features/accounts/AccountsFeature.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export default function AccountsFeature({ sidecarStatus }: AccountsFeatureProps)
4747
pasteContent,
4848
pasteError,
4949
codexQuotaByName,
50+
accountUsageByID,
5051
isSelectionMode,
5152
selectedAccountIDs,
5253
isHeaderActionsMenuOpen,
@@ -57,6 +58,7 @@ export default function AccountsFeature({ sidecarStatus }: AccountsFeatureProps)
5758
allFilteredSelected,
5859
loadAccounts,
5960
startCodexOAuth,
61+
cancelCodexOAuth,
6062
openOAuthDialogInBrowser,
6163
refreshCodexQuota,
6264
setSearchTerm,
@@ -201,6 +203,7 @@ export default function AccountsFeature({ sidecarStatus }: AccountsFeatureProps)
201203
group={group}
202204
groupCardHeight={groupCardHeights[group.id]}
203205
codexQuotaByName={codexQuotaByName}
206+
accountUsageByID={accountUsageByID}
204207
ready={ready}
205208
isSelectionMode={isSelectionMode}
206209
selectedAccountIDSet={selectedAccountIDSet}
@@ -226,20 +229,22 @@ export default function AccountsFeature({ sidecarStatus }: AccountsFeatureProps)
226229
{selectedAccount?.credentialSource === 'auth-file' && selectedAccount.rawAuthFile ? (
227230
<AccountDetailModal
228231
account={selectedAccount.rawAuthFile}
232+
usageSummary={accountUsageByID[selectedAccount.id]}
229233
canStartReauth={isCodexAuthFile(selectedAccount)}
230234
isReauthing={oauthPendingAccountID === selectedAccount.id}
231235
onClose={() => setSelectedAccount(null)}
232236
onStartReauth={() => {
233237
const targetAccount = selectedAccount;
234-
setSelectedAccount(null);
235238
void startCodexOAuth(targetAccount);
236239
}}
240+
onCancelReauth={cancelCodexOAuth}
237241
/>
238242
) : null}
239243

240244
{selectedAccount?.credentialSource === 'api-key' ? (
241245
<ApiKeyDetailModal
242246
account={selectedAccount}
247+
usageSummary={accountUsageByID[selectedAccount.id]}
243248
onClose={() => setSelectedAccount(null)}
244249
onRename={renameSelectedApiKey}
245250
onSavePriority={(priority) => void updateSelectedApiKeyPriority(priority)}
@@ -292,7 +297,7 @@ export default function AccountsFeature({ sidecarStatus }: AccountsFeatureProps)
292297
t={t}
293298
existingName={oauthDialog.existingName}
294299
url={oauthDialog.url}
295-
onClose={() => setOAuthDialog(null)}
300+
onClose={cancelCodexOAuth}
296301
onOpenInBrowser={openOAuthDialogInBrowser}
297302
/>
298303
) : null}

0 commit comments

Comments
 (0)