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:
@@ -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
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user