fix(auth-files): sync quota priority display and credential ordering

Read OAuth priorities in quota cards and sort credentials by descending
priority followed by filename order across OAuth and quota pages.
This commit is contained in:
lzt404 committed 2026-10-05 16:10:37 +08:00
1 parent 46a9d3d449
commit a08f38df7f
5 files changed
+54 -4

No files matched your search

+2 -1
View File
@@ -69,6 +69,7 @@ import {
parseAuthFilePriority,
resetAuthFileCooldown,
setOAuthCredentialFileDisabled,
sortAuthFilesByPriority,
} from '../services/authFiles';
import { authFileHealth, normalizeAuthFileCooldowns } from '../services/authFileHealth';
import { codexMetadataFor } from '../services/quotaMetadata';
@@ -175,7 +176,7 @@ export function AuthFileManagementPage() {
try {
const payload = await managementApi.get('/credentials');
if (!mountedRef.current || requestId !== fileRequestRef.current) return;
const nextFiles = dedupeAuthFiles(responseList(payload, 'files'));
const nextFiles = sortAuthFilesByPriority(dedupeAuthFiles(responseList(payload, 'files')));
setFileSnapshot({ files: nextFiles, receivedAtMs: Date.now(), observedAt: readString(payload, 'observed_at') });
const validQuotaKeys = new Set(nextFiles.map(quotaKey));
pruneQuotaCache(validQuotaKeys);
+4 -3
View File
@@ -34,7 +34,7 @@ import {
updateQuotaCache,
useQuotaCache,
} from '../services/quotaCache';
import { dedupeAuthFiles } from '../services/authFiles';
import { dedupeAuthFiles, parseAuthFilePriority, sortAuthFilesByPriority } from '../services/authFiles';
import { useI18n } from '../i18n';
import './QuotaPage.css';
@@ -65,7 +65,7 @@ export function QuotaPage() {
setError('');
try {
const payload = await managementApi.get('/credentials');
const allFiles = dedupeAuthFiles(responseList(payload, 'files'));
const allFiles = sortAuthFilesByPriority(dedupeAuthFiles(responseList(payload, 'files')));
const nextFiles = allFiles.filter((file) => !readBoolean(file, 'disabled') && providerForFile(file));
setFiles(nextFiles);
const validQuotaKeys = new Set(allFiles.map(quotaKey));
@@ -188,6 +188,7 @@ export function QuotaCard({ file, quota, onRefresh, onReset }: { file: AuthFile;
const provider = providerForFile(file);
const name = fileName(file);
const disabled = readBoolean(file, 'disabled');
const priority = parseAuthFilePriority(file.priority) ?? 0;
return (
<article className="panel real-quota-card">
<div className="real-quota-card-header">
@@ -196,7 +197,7 @@ export function QuotaCard({ file, quota, onRefresh, onReset }: { file: AuthFile;
<div><strong title={name}>{name}</strong><span>{provider ? providerMeta[provider].label : t('quota.unknownProvider')}</span></div>
</div>
<div className="quota-plan"><strong>{quota.plan || '—'}</strong><span>{quota.subscriptionActiveUntil ? t('quota.expiresAt', { time: formatQuotaTimestamp(quota.subscriptionActiveUntil, locale) }) : ''}</span></div>
<div className="quota-status"><span className={quota.status === 'success' ? 'quota-status-badge ready' : quota.status === 'error' ? 'quota-status-badge error' : 'quota-status-badge'}>{quota.status === 'success' ? t('common.enabled') : quota.status === 'loading' ? t('quota.querying') : quota.status === 'error' ? t('common.unavailable') : t('quota.notFetched')}</span><small>{t('authFiles.settings.priority')} 0</small></div>
<div className="quota-status"><span className={quota.status === 'success' ? 'quota-status-badge ready' : quota.status === 'error' ? 'quota-status-badge error' : 'quota-status-badge'}>{quota.status === 'success' ? t('common.enabled') : quota.status === 'loading' ? t('quota.querying') : quota.status === 'error' ? t('common.unavailable') : t('quota.notFetched')}</span><small>{t('authFiles.settings.priority')} {priority}</small></div>
<div className="quota-card-actions">
<button type="button" className="icon-button quiet" onClick={onRefresh} disabled={disabled || quota.status === 'loading'} title={disabled ? t('quota.fileDisabled') : t('quota.refresh')} aria-label={disabled ? t('quota.fileDisabled') : t('quota.refresh')}><RefreshCw size={16} className={quota.status === 'loading' ? 'spin' : ''} aria-hidden="true" /></button>
</div>
+13
View File
@@ -37,6 +37,19 @@ export const resetAuthFileCooldown = async (
export const authFileName = (file: AuthFileRecord) =>
readString(file, 'name') || translate(getCurrentLocale(), 'authFiles.unnamed');
/**
* Present credentials in the same order used by routing: higher explicit
* priority first, then a deterministic case-insensitive filename order.
* Return a new array so callers do not mutate the API response or snapshots.
*/
export const sortAuthFilesByPriority = <File extends AuthFileRecord>(files: File[]): File[] =>
[...files].sort((left, right) => {
const priorityDelta = (parseAuthFilePriority(right.priority) ?? 0)
- (parseAuthFilePriority(left.priority) ?? 0);
if (priorityDelta !== 0) return priorityDelta;
return authFileName(left).localeCompare(authFileName(right), undefined, { sensitivity: 'base' });
});
export const isRuntimeOnlyAuthFile = (file: AuthFileRecord) =>
readBoolean(file, 'runtime_only', 'runtimeOnly');
+22
View File
@@ -25,6 +25,28 @@ it('shows every reset expiry in chronological order, including duplicates, on bo
}
});
it('shows the OAuth credential priority on the quota card', () => {
const html = renderToStaticMarkup(<I18nProvider><QuotaCard
file={{ name: 'priority.json', provider: 'codex', priority: 7 }}
quota={{ status: 'success', rows: [] }} onRefresh={() => {}}
/></I18nProvider>);
expect(html).toContain('Priority 7');
expect(html).not.toContain('Priority 0');
});
it('normalizes a string priority and keeps zero as the default', () => {
const stringPriority = renderToStaticMarkup(<I18nProvider><QuotaCard
file={{ name: 'priority.json', provider: 'codex', priority: '3' }}
quota={{ status: 'idle', rows: [] }} onRefresh={() => {}}
/></I18nProvider>);
const missingPriority = renderToStaticMarkup(<I18nProvider><QuotaCard
file={{ name: 'default.json', provider: 'codex' }}
quota={{ status: 'idle', rows: [] }} onRefresh={() => {}}
/></I18nProvider>);
expect(stringPriority).toContain('Priority 3');
expect(missingPriority).toContain('Priority 0');
});
it('renders real quota values with bounded bars and keeps unknown distinct from zero', () => {
const html = render({ status: 'success', rows: [
{ label: 'empty', remainingPercent: 0 }, { label: 'full', remainingPercent: 100 },
+13
View File
@@ -9,6 +9,7 @@ import {
oauthModelProvidersFromAuthFiles,
parseAuthFilePriority,
setOAuthCredentialFileDisabled,
sortAuthFilesByPriority,
snapshotAuthFiles,
} from '../src/services/authFiles';
@@ -181,6 +182,18 @@ describe('authentication file priority', () => {
expect(normalizeAuthFilePriorityInput('1.5')).toBeNull();
});
it('sorts credentials by descending priority, then filename', () => {
expect(sortAuthFilesByPriority([
{ name: 'zeta.json', priority: 2 },
{ name: 'Beta.json', priority: 5 },
{ name: 'alpha.json', priority: 5 },
{ name: 'default.json' },
{ name: 'negative.json', priority: -1 },
]).map((file) => file.name)).toEqual([
'alpha.json', 'Beta.json', 'zeta.json', 'default.json', 'negative.json',
]);
});
it('finds only credentials created or updated by the completed OAuth provider', () => {
const before = snapshotAuthFiles([
{ name: 'codex-old.json', provider: 'codex', modtime: 1, priority: 8 },