Files
antigravity-claude-proxy/public/views/accounts.html
jgor20 a43d2332ca feat: per-account quota threshold protection (#212)
feat: per-account quota threshold protection

Resolves #135

- Adds configurable quota protection with three-tier threshold resolution (per-model → per-account → global)
- New global Minimum Quota Level slider in Settings
- Per-account threshold settings via Account Settings modal
- Draggable per-account threshold markers on model quota bars
- Backend: PATCH /api/accounts/:email endpoint, globalQuotaThreshold config
- i18n: quota protection keys for all 5 languages
2026-02-01 17:15:46 +05:30

524 lines
36 KiB
HTML

<div x-data="accountManager" class="view-container">
<!-- Compact Header -->
<div class="flex items-center justify-between gap-4 mb-6">
<!-- Title with inline subtitle -->
<div class="flex flex-wrap items-center gap-4">
<h1 class="text-2xl font-bold text-white tracking-tight" x-text="$store.global.t('accountManagement')">
Account Management
</h1>
<div class="flex items-center h-6 px-3 rounded-full bg-space-800/80 border border-space-border/50 shadow-sm backdrop-blur-sm">
<span class="text-[10px] font-mono text-gray-400 uppercase tracking-wider"
x-text="$store.global.t('manageTokens')">
Manage Google Account tokens and authorization states
</span>
</div>
</div>
<!-- Action Buttons -->
<div class="flex items-center gap-2">
<!-- Search -->
<div class="relative" x-show="$store.data.accounts.length > 0">
<input type="text"
x-model="searchQuery"
:placeholder="$store.global.t('searchAccounts')"
:aria-label="$store.global.t('searchAccounts')"
class="input-search-sm w-48 pl-9 h-8"
@keydown.escape="searchQuery = ''">
<svg class="w-4 h-4 absolute left-3 top-2 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<!-- Export Button -->
<button class="btn btn-xs btn-outline border-space-border text-gray-400 hover:text-white transition-all gap-2 h-8"
@click="exportAccounts()"
x-show="$store.data.accounts.length > 0"
:title="$store.global.t('exportAccounts') || 'Export Accounts'">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
</svg>
<span x-text="$store.global.t('export') || 'Export'">Export</span>
</button>
<!-- Import Button -->
<label class="btn btn-xs btn-outline border-space-border text-gray-400 hover:text-white transition-all gap-2 h-8 cursor-pointer"
:title="$store.global.t('importAccounts') || 'Import Accounts'">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
<span x-text="$store.global.t('import') || 'Import'">Import</span>
<input type="file" accept=".json" class="hidden" @change="importAccounts($event)">
</label>
<button class="btn btn-xs btn-outline border-space-border text-gray-400 hover:text-white transition-all gap-2 h-8"
@click="reloadAccounts()"
:disabled="reloading">
<svg xmlns="http://www.w3.org/2000/svg"
class="w-3.5 h-3.5 transition-transform"
:class="{ 'animate-spin': reloading }"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<span x-text="$store.global.t('reload')">Reload</span>
</button>
<!-- Account Count Indicator -->
<div class="flex items-center h-6 px-2 rounded bg-space-800/80 border border-space-border/50"
:class="{ 'border-yellow-500/50': $store.data.accounts.length >= $store.data.maxAccounts }">
<span class="text-[11px] font-mono"
:class="$store.data.accounts.length >= $store.data.maxAccounts ? 'text-yellow-400' : 'text-gray-400'"
x-text="$store.data.accounts.length + '/' + $store.data.maxAccounts"></span>
</div>
<button class="btn bg-neon-purple hover:bg-purple-600 border-none text-white btn-xs gap-2 shadow-lg shadow-neon-purple/20 h-8"
:class="{ 'opacity-50 cursor-not-allowed': $store.data.accounts.length >= $store.data.maxAccounts }"
:disabled="$store.data.accounts.length >= $store.data.maxAccounts"
@click="$store.data.accounts.length < $store.data.maxAccounts && document.getElementById('add_account_modal').showModal()">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
<span x-text="$store.global.t('addAccount')">Add Account</span>
</button>
</div>
</div>
<!-- Table Card -->
<div class="view-card !p-0">
<table class="w-full">
<thead x-show="$store.data.accounts.length > 0">
<tr class="bg-space-900/50 border-b border-space-border/50">
<th class="pl-6 py-3 text-left text-[10px] font-bold text-gray-500 uppercase tracking-wider w-16" x-text="$store.global.t('enabled')">Enabled</th>
<th class="py-3 text-left text-[10px] font-bold text-gray-500 uppercase tracking-wider flex-1 min-w-[200px]" x-text="$store.global.t('accountEmail')">Account (Email)</th>
<th class="py-3 text-left text-[10px] font-bold text-gray-500 uppercase tracking-wider w-20" x-text="$store.global.t('source')">Source</th>
<th class="py-3 text-left text-[10px] font-bold text-gray-500 uppercase tracking-wider w-16" x-text="$store.global.t('tier')">Tier</th>
<th class="py-3 text-left text-[10px] font-bold text-gray-500 uppercase tracking-wider w-32" x-text="$store.global.t('quota')">Quota</th>
<th class="py-3 text-left text-[10px] font-bold text-gray-500 uppercase tracking-wider w-24" x-text="$store.global.t('health')">Health</th>
<th class="py-3 pr-6 text-right text-[10px] font-bold text-gray-500 uppercase tracking-wider w-32" x-text="$store.global.t('operations')">Operations</th>
</tr>
</thead>
<tbody>
<!-- Empty State -->
<template x-if="$store.data.accounts.length === 0">
<tr>
<td colspan="6" class="py-16 text-center">
<div class="flex flex-col items-center gap-4 max-w-lg mx-auto">
<svg class="w-20 h-20 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
<h3 class="text-xl font-semibold text-gray-400" x-text="$store.global.t('noAccountsYet')">
No Accounts Yet
</h3>
<p class="text-sm text-gray-600 max-w-md leading-relaxed" x-text="$store.global.t('noAccountsDesc')">
Get started by adding a Google account via OAuth, or use the CLI command to import credentials.
</p>
<div class="flex items-center gap-4 mt-2">
<button class="btn bg-neon-purple hover:bg-purple-600 border-none text-white btn-sm gap-2 shadow-lg shadow-neon-purple/20"
onclick="document.getElementById('add_account_modal').showModal()">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
<span x-text="$store.global.t('addFirstAccount')">Add Your First Account</span>
</button>
<span class="text-xs text-gray-600" x-text="$store.global.t('or')">or</span>
<div class="text-xs font-mono bg-space-800 px-3 py-2 rounded border border-space-border text-gray-400">
npm run accounts:add
</div>
</div>
</div>
</td>
</tr>
</template>
<!-- Account Rows -->
<template x-for="acc in filteredAccounts" :key="acc.email">
<tr class="border-b border-space-border/30 last:border-0 hover:bg-white/5 transition-colors group">
<td class="pl-6 py-4">
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" class="sr-only peer"
:checked="acc.enabled !== false"
@change="toggleAccount(acc.email, $el.checked)">
<div class="w-9 h-5 bg-space-800 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-gray-600 after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-neon-green peer-checked:after:bg-white"></div>
</label>
</td>
<td class="py-4">
<div class="tooltip tooltip-right" :data-tip="acc.email">
<span class="font-mono text-sm text-gray-300 truncate max-w-[320px] inline-block cursor-help group-hover:text-white transition-colors"
x-text="formatEmail(acc.email)">
</span>
</div>
</td>
<td class="py-4">
<span :class="acc.source === 'oauth' ? 'status-pill-purple' : 'status-pill-free'"
x-text="acc.source || 'oauth'">
</span>
</td>
<td class="py-4">
<span :class="{
'status-pill-ultra': acc.subscription?.tier === 'ultra',
'status-pill-pro': acc.subscription?.tier === 'pro',
'status-pill-free': !acc.subscription || acc.subscription.tier === 'free' || acc.subscription.tier === 'unknown'
}"
x-text="(acc.subscription?.tier || 'free').toUpperCase()">
</span>
</td>
<td class="py-4 cursor-pointer" @click="openQuotaModal(acc)">
<div x-data="{ quota: getMainModelQuota(acc) }">
<template x-if="quota.percent !== null">
<div class="flex items-center gap-2">
<div class="w-16 bg-gray-700 rounded-full h-2 overflow-hidden">
<div class="h-full rounded-full transition-all"
:class="{
'bg-neon-green': quota.percent > 50,
'bg-yellow-500': quota.percent > 20 && quota.percent <= 50,
'bg-red-500': quota.percent <= 20
}"
:style="`width: ${quota.percent}%`">
</div>
</div>
<span class="text-xs font-mono text-gray-400" x-text="quota.percent + '%'"></span>
</div>
</template>
<template x-if="quota.percent === null">
<span class="text-xs text-gray-600">-</span>
</template>
<div x-show="acc.quotaThreshold !== undefined"
class="text-[10px] font-mono text-neon-yellow/60 mt-0.5"
x-text="'min: ' + getEffectiveThreshold(acc)"></div>
</div>
</td>
<td class="py-4">
<div class="flex items-center gap-2">
<div class="w-2 h-2 rounded-full flex-shrink-0"
:class="acc.status === 'ok' ?
'bg-neon-green shadow-[0_0_8px_rgba(34,197,94,0.6)] animate-pulse' :
'bg-red-500 shadow-[0_0_8px_rgba(239,68,68,0.6)]'">
</div>
<span class="text-xs font-mono font-semibold"
:class="acc.status === 'ok' ? 'text-neon-green' : 'text-red-400'"
x-text="acc.status.toUpperCase()">
</span>
</div>
</td>
<td class="py-4 pr-6">
<div class="flex justify-end gap-2">
<!-- Fix Button -->
<button x-show="acc.status === 'invalid'"
class="px-3 py-1 text-[10px] font-bold font-mono uppercase tracking-wider rounded bg-yellow-500/10 text-yellow-400 hover:bg-yellow-500/20 border border-yellow-500/30 hover:border-yellow-500/50 transition-all"
@click="fixAccount(acc.email)"
x-text="$store.global.t('fix')">
FIX
</button>
<!-- Settings Button (threshold) -->
<button class="p-2 rounded hover:bg-white/10 text-gray-500 hover:text-neon-yellow transition-colors"
@click="openThresholdModal(acc)"
title="Account Settings">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</button>
<button class="p-2 rounded hover:bg-white/10 text-gray-500 hover:text-white transition-colors"
@click="refreshAccount(acc.email)"
:disabled="refreshing"
:title="$store.global.t('refreshData')">
<svg class="w-4 h-4 transition-transform"
:class="{ 'animate-spin': refreshing }"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
<button
class="p-2 rounded transition-colors"
:class="acc.source === 'database' ? 'text-gray-700 cursor-not-allowed' : 'hover:bg-red-500/10 text-gray-500 hover:text-red-400'"
:disabled="acc.source === 'database'"
@click="acc.source !== 'database' && confirmDeleteAccount(acc.email)"
:title="acc.source === 'database' ? $store.global.t('cannotDeleteDatabase') : $store.global.t('delete')">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</td>
</tr>
</template>
<!-- No Search Results -->
<template x-if="$store.data.accounts.length > 0 && filteredAccounts.length === 0">
<tr>
<td colspan="6" class="py-12 text-center">
<div class="flex flex-col items-center gap-3">
<svg class="w-12 h-12 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<p class="text-sm text-gray-600" x-text="$store.global.t('noSearchResults')">No accounts match your search</p>
<button class="btn-action-ghost !text-gray-500" @click="searchQuery = ''" x-text="$store.global.t('clearSearch')">Clear Search</button>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
<!-- Account Status Info -->
<div x-show="$store.data.accounts.length > 0" class="mt-4 px-6 py-3 bg-space-900/20 rounded-lg border border-space-border/20">
<p class="text-xs text-gray-600 flex items-center gap-2">
<svg class="w-4 h-4 text-gray-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span x-html="$store.global.t('disabledAccountsNote')"></span>
</p>
</div>
<!-- Delete Confirmation Modal -->
<dialog id="delete_account_modal" class="modal">
<div class="modal-box bg-space-900 border-2 border-red-500/50">
<h3 class="font-bold text-lg text-red-400 flex items-center gap-2">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<span x-text="$store.global.t('dangerousOperation')">Dangerous Operation</span>
</h3>
<p class="py-4 text-gray-300">
<span x-text="$store.global.t('confirmDeletePrompt')">Are you sure you want to delete account</span>
<strong class="text-white font-mono" x-text="deleteTarget"></strong>?
</p>
<div class="bg-red-500/10 border border-red-500/30 rounded p-3 mb-4">
<p class="text-sm text-red-300 flex items-start gap-2">
<svg class="w-5 h-5 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<span x-text="$store.global.t('deleteWarning')">This action cannot be undone. All configuration and historical records will be permanently deleted.</span>
</p>
</div>
<div class="modal-action">
<button class="btn btn-ghost text-gray-400" onclick="document.getElementById('delete_account_modal').close()" x-text="$store.global.t('cancel')">
Cancel
</button>
<button class="btn bg-red-500 hover:bg-red-600 border-none text-white"
@click="executeDelete()"
:disabled="deleting"
:class="{ 'loading': deleting }">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" x-show="!deleting">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
<span x-text="$store.global.t('confirmDelete')">Confirm Delete</span>
</button>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button x-text="$store.global.t('close')">close</button>
</form>
</dialog>
<!-- Quota Distribution Modal -->
<dialog id="quota_modal" class="modal backdrop-blur-sm">
<div class="modal-box max-w-2xl w-full bg-space-900 border border-space-border text-gray-300 shadow-2xl p-6">
<h3 class="font-bold text-xl text-white mb-2 flex items-center gap-2">
<svg class="w-6 h-6 text-neon-cyan" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
<span x-text="$store.global.t('quotaDistribution')">Quota Distribution</span>
</h3>
<p class="text-sm text-gray-500 font-mono mb-6" x-text="selectedAccountEmail"></p>
<div class="grid grid-cols-1 gap-4 overflow-y-auto max-h-[60vh] pr-2 custom-scrollbar">
<template x-for="(limit, modelId) in selectedAccountLimits" :key="modelId">
<div class="p-3 bg-space-800/50 border border-space-border/30 rounded-lg">
<div class="flex justify-between items-center mb-2">
<span class="text-sm font-semibold text-gray-200" x-text="modelId"></span>
<div class="flex items-center gap-2">
<span class="text-xs font-mono"
:class="limit.remainingFraction > 0.5 ? 'text-neon-green' : (limit.remainingFraction > 0.2 ? 'text-yellow-500' : 'text-red-500')"
x-text="Math.round(limit.remainingFraction * 100) + '%'"></span>
</div>
</div>
<div class="w-full bg-gray-700 rounded-full h-2.5 mb-2 overflow-hidden">
<div class="h-full rounded-full transition-all duration-500"
:class="limit.remainingFraction > 0.5 ? 'bg-neon-green' : (limit.remainingFraction > 0.2 ? 'bg-yellow-500' : 'bg-red-500')"
:style="`width: ${limit.remainingFraction * 100}%`">
</div>
</div>
<div class="flex justify-between items-center">
<span class="text-[10px] text-gray-500 uppercase font-bold" x-text="$store.data.getModelFamily(modelId)"></span>
<template x-if="limit.resetTime">
<span class="text-[10px] text-yellow-500/80 font-mono italic"
x-text="$store.global.t('resetsIn', { time: window.utils.formatTimeUntil(limit.resetTime) })"></span>
</template>
</div>
</div>
</template>
<template x-if="Object.keys(selectedAccountLimits).length === 0">
<div class="text-center py-8 text-gray-500" x-text="$store.global.t('noQuotaData')">
No quota data available for this account yet.
</div>
</template>
</div>
<div class="modal-action">
<form method="dialog">
<button class="btn btn-ghost hover:bg-white/10" x-text="$store.global.t('close')">Close</button>
</form>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button x-text="$store.global.t('close')">close</button>
</form>
</dialog>
<!-- Threshold Settings Modal -->
<dialog id="threshold_modal" class="modal backdrop-blur-sm">
<div class="modal-box max-w-md w-full bg-space-900 border border-space-border text-gray-300 shadow-2xl p-6">
<h3 class="font-bold text-xl text-white mb-2 flex items-center gap-2">
<svg class="w-6 h-6 text-neon-yellow" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<span>Account Settings</span>
</h3>
<p class="text-sm text-gray-500 font-mono mb-6" x-text="thresholdDialog.email"></p>
<div class="space-y-4">
<!-- Info Alert -->
<div class="bg-space-800/50 border border-space-border/30 rounded-lg p-3">
<p class="text-xs text-gray-400 leading-relaxed">
Set a minimum quota level for this account. When the account's quota falls below this threshold,
the proxy will switch to another account with more quota remaining.
</p>
</div>
<!-- Threshold Input -->
<div class="form-control">
<label class="label">
<span class="label-text text-gray-400">Minimum Quota</span>
<span class="label-text-alt font-mono text-neon-yellow"
x-text="thresholdDialog.quotaThreshold !== null ? thresholdDialog.quotaThreshold + '%' : 'Default'"></span>
</label>
<div class="flex gap-3 items-center">
<input type="range" min="0" max="99" step="1"
class="custom-range custom-range-yellow flex-1"
:value="thresholdDialog.quotaThreshold || 0"
:style="`background-size: ${thresholdDialog.quotaThreshold || 0}% 100%`"
@input="thresholdDialog.quotaThreshold = parseInt($event.target.value)"
aria-label="Minimum quota threshold slider">
<div class="flex items-center gap-1">
<input type="number" min="0" max="99" step="1"
class="input input-sm input-bordered w-16 bg-space-800 border-space-border text-white font-mono text-center"
:value="thresholdDialog.quotaThreshold ?? ''"
placeholder="-"
@input="thresholdDialog.quotaThreshold = $event.target.value === '' ? null : parseInt($event.target.value)"
aria-label="Minimum quota threshold value">
<span class="text-gray-500 text-xs">%</span>
</div>
</div>
<p class="text-[10px] text-gray-600 mt-1">
Leave empty to use the global threshold from Settings.
</p>
</div>
<!-- Clear Button -->
<button class="btn btn-sm btn-ghost text-gray-500 hover:text-gray-300 w-full"
@click="clearAccountThreshold()"
x-show="thresholdDialog.quotaThreshold !== null">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
<span>Reset to Default</span>
</button>
<!-- Per-Model Thresholds -->
<div class="pt-4 border-t border-space-border/30">
<div class="flex items-center justify-between mb-3">
<span class="text-xs text-gray-400 font-semibold">Per-Model Overrides</span>
<button class="btn btn-xs btn-ghost text-gray-500 hover:text-neon-yellow gap-1"
@click="addModelThreshold()">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
Add
</button>
</div>
<!-- Model threshold list -->
<div class="space-y-2 max-h-40 overflow-y-auto custom-scrollbar">
<template x-for="(threshold, modelId) in thresholdDialog.modelQuotaThresholds" :key="modelId">
<div class="flex items-center gap-2 bg-space-800/50 rounded px-2 py-1.5">
<span class="text-xs text-gray-300 flex-1 truncate font-mono" x-text="modelId"></span>
<div class="flex items-center gap-1">
<input type="number" min="0" max="99" step="1"
class="input input-xs input-bordered w-14 bg-space-900 border-space-border text-white font-mono text-center"
:value="threshold"
@input="updateModelThreshold(modelId, $event.target.value)"
aria-label="Model threshold">
<span class="text-gray-500 text-[10px]">%</span>
</div>
<button class="btn btn-xs btn-ghost text-gray-500 hover:text-red-400 px-1"
@click="removeModelThreshold(modelId)">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</template>
<template x-if="Object.keys(thresholdDialog.modelQuotaThresholds).length === 0">
<p class="text-[10px] text-gray-600 text-center py-2">
No per-model overrides configured.
</p>
</template>
</div>
</div>
<!-- Add Model Dialog (inline) -->
<div x-show="thresholdDialog.addingModel" class="pt-3 border-t border-space-border/30" x-cloak>
<div class="flex gap-2">
<select class="select select-sm select-bordered flex-1 bg-space-800 border-space-border text-white text-xs"
x-model="thresholdDialog.newModelId">
<option value="">Select model...</option>
<template x-for="model in getAvailableModelsForThreshold()" :key="model">
<option :value="model" x-text="model"></option>
</template>
</select>
<input type="number" min="0" max="99" step="1"
class="input input-sm input-bordered w-16 bg-space-800 border-space-border text-white font-mono text-center"
x-model="thresholdDialog.newModelThreshold"
placeholder="10">
<span class="text-gray-500 text-xs self-center">%</span>
</div>
<div class="flex gap-2 mt-2">
<button class="btn btn-xs btn-ghost text-gray-500 flex-1"
@click="thresholdDialog.addingModel = false">
Cancel
</button>
<button class="btn btn-xs btn-warning text-black flex-1"
@click="confirmAddModelThreshold()"
:disabled="!thresholdDialog.newModelId">
Add Override
</button>
</div>
</div>
</div>
<div class="modal-action">
<form method="dialog">
<button class="btn btn-ghost hover:bg-white/10">Cancel</button>
</form>
<button class="btn btn-warning border-none text-black font-semibold"
@click="saveAccountThreshold()"
:disabled="thresholdDialog.saving"
:class="{ 'loading': thresholdDialog.saving }">
<span>Save</span>
</button>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
</div>