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
This commit is contained in:
jgor20
2026-02-01 11:45:46 +00:00
committed by GitHub
parent 33584d31bb
commit a43d2332ca
23 changed files with 806 additions and 31 deletions

View File

@@ -190,9 +190,41 @@
<span class="text-gray-500 text-[10px]"
x-text="row.quotaInfo.filter(q => q.pct > 0).length + '/' + row.quotaInfo.length"></span>
</div>
<progress class="progress w-full h-1 bg-space-800"
:class="row.avgQuota > 50 ? 'progress-gradient-success' : (row.avgQuota > 0 ? 'progress-gradient-warning' : 'progress-gradient-error')"
:value="row.avgQuota" max="100"></progress>
<div class="relative">
<progress class="progress w-full h-1.5 bg-space-800"
:class="row.avgQuota > 50 ? 'progress-gradient-success' : (row.avgQuota > 0 ? 'progress-gradient-warning' : 'progress-gradient-error')"
:value="row.avgQuota" max="100"></progress>
<!-- Per-account draggable quota threshold markers -->
<template x-for="(q, qIdx) in row.quotaInfo.filter(q => q.thresholdPct > 0 || isDragging(q, row))" :key="q.fullEmail + '-threshold'">
<div class="absolute -top-1 -bottom-1 group/marker"
:class="isDragging(q, row) ? 'cursor-grabbing z-30' : 'cursor-grab'"
:style="'left: calc(' + getMarkerPct(q, row) + '% - 4px); width: 9px; transform: translateX(' + (isDragging(q, row) ? '0px' : getMarkerOffset(q, row, qIdx)) + ');'"
:title="q.email + ' min: ' + getMarkerPct(q, row) + '%'"
@mousedown.prevent="startDrag($event, q, row)"
@touchstart.prevent="startDrag($event, q, row)">
<!-- Visible marker line -->
<div class="absolute top-1 bottom-1 left-1 w-[3px] rounded-full"
:class="isDragging(q, row) ? 'scale-y-150 brightness-150' : 'transition-all group-hover/marker:scale-y-150 group-hover/marker:brightness-125'"
:style="'background-color: ' + getThresholdColor(qIdx).bg + '; box-shadow: 0 0 ' + (isDragging(q, row) ? '10px' : '6px') + ' ' + getThresholdColor(qIdx).shadow"></div>
<!-- Tooltip popup (visible on hover or during drag) -->
<div class="absolute -top-8 left-1/2 -translate-x-1/2 items-center gap-1.5
whitespace-nowrap text-xs font-mono font-semibold px-2.5 py-1 rounded-md bg-space-900 border border-space-border shadow-xl shadow-black/40 z-20"
:class="isDragging(q, row) ? 'flex' : 'hidden group-hover/marker:flex'"
:style="'color: ' + getThresholdColor(qIdx).bg + '; border-color: ' + getThresholdColor(qIdx).bg + '40'">
<span class="w-2 h-2 rounded-full flex-shrink-0" :style="'background-color: ' + getThresholdColor(qIdx).bg"></span>
<span x-text="q.email"></span>
<span class="text-gray-500">min</span>
<span x-text="getMarkerPct(q, row) + '%'"></span>
</div>
</div>
</template>
</div>
<!-- Threshold text under bar -->
<div x-show="row.effectiveThresholdPct > 0"
class="text-[10px] font-mono text-gray-500 -mt-0.5"
x-text="row.hasVariedThresholds
? 'min: ' + Math.min(...row.quotaInfo.map(q => q.thresholdPct).filter(t => t > 0)) + '' + row.effectiveThresholdPct + '%'
: 'min: ' + row.effectiveThresholdPct + '%'"></div>
</div>
</td>
<td class="font-mono text-xs">
@@ -219,7 +251,8 @@
<div class="flex flex-wrap gap-1 justify-start">
<!-- Visible accounts (limited to maxVisible) -->
<template x-for="(q, idx) in row.quotaInfo.slice(0, maxVisible)" :key="q.fullEmail">
<div class="tooltip tooltip-left" :data-tip="`${q.fullEmail} (${q.pct}%)`">
<div class="tooltip tooltip-left"
:data-tip="`${q.fullEmail} (${q.pct}%)` + (q.thresholdPct > 0 ? ` · min: ${q.thresholdPct}%` : '')">
<div class="w-3 h-3 rounded-[2px] transition-all hover:scale-125 cursor-help"
:class="q.pct > 50 ? 'bg-neon-green opacity-80' : (q.pct > 0 ? 'bg-yellow-500 opacity-80' : 'bg-red-900 opacity-50')">
</div>
@@ -228,7 +261,7 @@
<!-- Overflow indicator -->
<template x-if="row.quotaInfo.length > maxVisible">
<div class="tooltip tooltip-left"
:data-tip="row.quotaInfo.slice(maxVisible).map(q => `${q.fullEmail} (${q.pct}%)`).join('\n')">
:data-tip="row.quotaInfo.slice(maxVisible).map(q => `${q.fullEmail} (${q.pct}%)` + (q.thresholdPct > 0 ? ` · min: ${q.thresholdPct}%` : '')).join('\n')">
<div class="w-3 h-3 rounded-[2px] bg-gray-700/50 border border-gray-600 flex items-center justify-center cursor-help hover:bg-gray-600/70 transition-colors">
<span class="text-[8px] text-gray-400 font-bold leading-none" x-text="`+${row.quotaInfo.length - maxVisible}`"></span>
</div>