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

@@ -13,6 +13,7 @@ document.addEventListener('alpine:init', () => {
modelConfig: {}, // Model metadata (hidden, pinned, alias)
quotaRows: [], // Filtered view
usageHistory: {}, // Usage statistics history (from /account-limits?includeHistory=true)
globalQuotaThreshold: 0, // Global minimum quota threshold (fraction 0-0.99)
maxAccounts: 10, // Maximum number of accounts allowed (from config)
loading: false,
initialLoad: true, // Track first load for skeleton screen
@@ -116,6 +117,7 @@ document.addEventListener('alpine:init', () => {
this.models = data.models;
}
this.modelConfig = data.modelConfig || {};
this.globalQuotaThreshold = data.globalQuotaThreshold || 0;
// Store usage history if included (for dashboard)
if (data.history) {
@@ -236,6 +238,8 @@ document.addEventListener('alpine:init', () => {
let totalQuotaSum = 0;
let validAccountCount = 0;
let minResetTime = null;
let maxEffectiveThreshold = 0;
const globalThreshold = this.globalQuotaThreshold || 0;
this.accounts.forEach(acc => {
if (acc.enabled === false) return;
@@ -255,11 +259,26 @@ document.addEventListener('alpine:init', () => {
minResetTime = limit.resetTime;
}
// Resolve effective threshold: per-model > per-account > global
const accModelThreshold = acc.modelQuotaThresholds?.[modelId];
const accThreshold = acc.quotaThreshold;
const effective = accModelThreshold ?? accThreshold ?? globalThreshold;
if (effective > maxEffectiveThreshold) {
maxEffectiveThreshold = effective;
}
// Determine threshold source for display
let thresholdSource = 'global';
if (accModelThreshold !== undefined) thresholdSource = 'model';
else if (accThreshold !== undefined) thresholdSource = 'account';
quotaInfo.push({
email: acc.email.split('@')[0],
fullEmail: acc.email,
pct: pct,
resetTime: limit.resetTime
resetTime: limit.resetTime,
thresholdPct: Math.round(effective * 100),
thresholdSource
});
});
@@ -268,6 +287,10 @@ document.addEventListener('alpine:init', () => {
if (!showExhausted && minQuota === 0) return;
// Check if thresholds vary across accounts
const uniqueThresholds = new Set(quotaInfo.map(q => q.thresholdPct));
const hasVariedThresholds = uniqueThresholds.size > 1;
rows.push({
modelId,
displayName: modelId, // Simplified: no longer using alias
@@ -279,7 +302,9 @@ document.addEventListener('alpine:init', () => {
quotaInfo,
pinned: !!config.pinned,
hidden: !!isHidden, // Use computed visibility
activeCount: quotaInfo.filter(q => q.pct > 0).length
activeCount: quotaInfo.filter(q => q.pct > 0).length,
effectiveThresholdPct: Math.round(maxEffectiveThreshold * 100),
hasVariedThresholds
});
});