feat(webui): add subscription tier and quota visualization
Backend:
This commit is contained in:
@@ -39,6 +39,7 @@ document.addEventListener('alpine:init', () => {
|
||||
},
|
||||
|
||||
refreshTimer: null,
|
||||
isTabVisible: true,
|
||||
|
||||
fetchData() {
|
||||
Alpine.store('data').fetchData();
|
||||
@@ -46,9 +47,39 @@ document.addEventListener('alpine:init', () => {
|
||||
|
||||
startAutoRefresh() {
|
||||
if (this.refreshTimer) clearInterval(this.refreshTimer);
|
||||
const interval = parseInt(Alpine.store('settings').refreshInterval);
|
||||
if (interval > 0) {
|
||||
this.refreshTimer = setInterval(() => Alpine.store('data').fetchData(), interval * 1000);
|
||||
const baseInterval = parseInt(Alpine.store('settings').refreshInterval);
|
||||
if (baseInterval > 0) {
|
||||
// Setup visibility change listener (only once)
|
||||
if (!this._visibilitySetup) {
|
||||
this._visibilitySetup = true;
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
this.isTabVisible = !document.hidden;
|
||||
if (this.isTabVisible) {
|
||||
// Tab became visible - fetch immediately and restart timer
|
||||
Alpine.store('data').fetchData();
|
||||
this.startAutoRefresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Schedule next refresh with jitter
|
||||
const scheduleNext = () => {
|
||||
// Add ±20% random jitter to prevent synchronized requests
|
||||
const jitter = (Math.random() - 0.5) * 0.4; // -0.2 to +0.2
|
||||
const interval = baseInterval * (1 + jitter);
|
||||
|
||||
// Slow down when tab is hidden (reduce frequency by 3x)
|
||||
const actualInterval = this.isTabVisible
|
||||
? interval
|
||||
: interval * 3;
|
||||
|
||||
this.refreshTimer = setTimeout(() => {
|
||||
Alpine.store('data').fetchData();
|
||||
scheduleNext(); // Reschedule with new jitter
|
||||
}, actualInterval * 1000);
|
||||
};
|
||||
|
||||
scheduleNext();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -163,5 +163,40 @@ window.Components.accountManager = () => ({
|
||||
} catch (e) {
|
||||
store.showToast(store.t('reloadFailed') + ': ' + e.message, 'error');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get main model quota for display
|
||||
* Prioritizes flagship models (Opus > Sonnet > Flash)
|
||||
* @param {Object} account - Account object with limits
|
||||
* @returns {Object} { percent: number|null, model: string }
|
||||
*/
|
||||
getMainModelQuota(account) {
|
||||
const limits = account.limits || {};
|
||||
const modelIds = Object.keys(limits);
|
||||
|
||||
if (modelIds.length === 0) {
|
||||
return { percent: null, model: '-' };
|
||||
}
|
||||
|
||||
// Priority: opus > sonnet > flash > others
|
||||
const priorityModels = [
|
||||
modelIds.find(m => m.toLowerCase().includes('opus')),
|
||||
modelIds.find(m => m.toLowerCase().includes('sonnet')),
|
||||
modelIds.find(m => m.toLowerCase().includes('flash')),
|
||||
modelIds[0] // Fallback to first model
|
||||
];
|
||||
|
||||
const selectedModel = priorityModels.find(m => m) || modelIds[0];
|
||||
const quota = limits[selectedModel];
|
||||
|
||||
if (!quota || quota.remainingFraction === null) {
|
||||
return { percent: null, model: selectedModel };
|
||||
}
|
||||
|
||||
return {
|
||||
percent: Math.round(quota.remainingFraction * 100),
|
||||
model: selectedModel
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -40,4 +40,18 @@ window.DashboardStats.updateStats = function(component) {
|
||||
component.stats.total = enabledAccounts.length;
|
||||
component.stats.active = active;
|
||||
component.stats.limited = limited;
|
||||
|
||||
// Calculate subscription tier distribution
|
||||
const subscription = { ultra: 0, pro: 0, free: 0 };
|
||||
enabledAccounts.forEach(acc => {
|
||||
const tier = acc.subscription?.tier || 'free';
|
||||
if (tier === 'ultra') {
|
||||
subscription.ultra++;
|
||||
} else if (tier === 'pro') {
|
||||
subscription.pro++;
|
||||
} else {
|
||||
subscription.free++;
|
||||
}
|
||||
});
|
||||
component.stats.subscription = subscription;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user