Add `DEVIN_MODE` configuration supporting `enterprise` (default) and `organization_sessions` modes. Organization sessions mode uses `/v3/organizations/{org_id}/sessions` API with `ViewOrgSessions` permission, suitable for non-Enterprise organizations. Sums session ACUs consumed within configurable rolling window (1-365 days via `DEVIN_SESSION_WINDOW_DAYS`). Mode does not show quota, percentage, credit, or reset fields as these are unav
93 lines
7.1 KiB
JavaScript
93 lines
7.1 KiB
JavaScript
(() => {
|
|
const state = { last: { codex: null, devin: null }, refresh: 10, countdowns: new Map() };
|
|
const byId = (id) => document.getElementById(id);
|
|
const text = (node, value) => { node.textContent = value; return node; };
|
|
const fmt = (value) => Number.isInteger(value) ? String(value) : Number(value).toFixed(1).replace(/\.0$/, '');
|
|
const clockValue = (date) => date.toLocaleTimeString([], { hour12: false });
|
|
const remainingClass = (value) => value < 20 ? 'red' : value <= 50 ? 'amber' : '';
|
|
const amount = (limit) => {
|
|
if (limit.used != null && limit.limit != null) return `${fmt(limit.used)} / ${fmt(limit.limit)} ${limit.unit || ''}`.trim();
|
|
if (limit.used != null) return `${fmt(limit.used)} ${limit.unit || ''}`.trim();
|
|
if (limit.remaining != null) return `${fmt(limit.remaining)} ${limit.unit || ''}`.trim();
|
|
return '';
|
|
};
|
|
const countdown = (date) => {
|
|
const delta = new Date(date).getTime() - Date.now();
|
|
if (!Number.isFinite(delta) || delta <= 0) return 'Reset due';
|
|
let seconds = Math.floor(delta / 1000);
|
|
const days = Math.floor(seconds / 86400); seconds %= 86400;
|
|
const hours = Math.floor(seconds / 3600); seconds %= 3600;
|
|
const minutes = Math.floor(seconds / 60); seconds %= 60;
|
|
if (days) return `Reset in ${days}d ${String(hours).padStart(2, '0')}h ${String(minutes).padStart(2, '0')}m`;
|
|
return `Reset in ${String(hours).padStart(2, '0')}h ${String(minutes).padStart(2, '0')}m ${String(seconds).padStart(2, '0')}s`;
|
|
};
|
|
const updateClock = () => text(byId('clock'), clockValue(new Date()));
|
|
const updateCountdowns = () => state.countdowns.forEach((date, node) => text(node, countdown(date)));
|
|
const renderLimit = (limit, prior) => {
|
|
const item = document.createElement('section'); item.className = 'limit';
|
|
const top = document.createElement('div'); top.className = 'limit-top';
|
|
const name = document.createElement('span'); name.className = 'limit-name'; text(name, limit.name || 'Usage limit'); top.append(name);
|
|
if (limit.remaining_percent != null) {
|
|
const remaining = document.createElement('strong'); remaining.className = `remaining ${remainingClass(limit.remaining_percent)}`; text(remaining, `${fmt(limit.remaining_percent)}%`); top.append(remaining);
|
|
}
|
|
item.append(top);
|
|
if (limit.remaining_percent != null) {
|
|
const target = Math.max(0, Math.min(100, limit.remaining_percent));
|
|
const bar = document.createElement('div'); bar.className = 'bar'; bar.setAttribute('role', 'progressbar'); bar.setAttribute('aria-valuemin', '0'); bar.setAttribute('aria-valuemax', '100'); bar.setAttribute('aria-valuenow', String(limit.remaining_percent));
|
|
const fill = document.createElement('div'); fill.className = `bar-fill ${remainingClass(limit.remaining_percent)}`;
|
|
if (prior && Number.isFinite(prior.remaining_percent) && prior.remaining_percent !== limit.remaining_percent) {
|
|
fill.style.width = `${Math.max(0, Math.min(100, prior.remaining_percent))}%`;
|
|
requestAnimationFrame(() => { fill.style.width = `${target}%`; });
|
|
} else {
|
|
fill.style.width = `${target}%`;
|
|
}
|
|
bar.append(fill); item.append(bar);
|
|
}
|
|
const meta = document.createElement('div'); meta.className = 'meta';
|
|
if (limit.remaining_percent == null && limit.used_percent == null) meta.classList.add('standalone');
|
|
if (limit.used_percent != null) { const used = document.createElement('span'); text(used, `${fmt(limit.used_percent)}% used`); meta.append(used); }
|
|
const value = amount(limit); if (value) { const numeric = document.createElement('span'); numeric.className = 'amount'; text(numeric, value); meta.append(numeric); }
|
|
if (meta.childNodes.length) item.append(meta);
|
|
if (limit.reset_at) { const reset = document.createElement('div'); reset.className = 'reset'; text(reset, countdown(limit.reset_at)); state.countdowns.set(reset, limit.reset_at); item.append(reset); }
|
|
return item;
|
|
};
|
|
const clearContent = (content) => { while (content.firstChild) content.removeChild(content.firstChild); };
|
|
const renderProvider = (name, provider) => {
|
|
const content = byId(`${name}-content`); const status = byId(`${name}-status`);
|
|
status.className = `status-pill ${provider.status}`; text(status, provider.status === 'ok' ? 'LIVE' : provider.status === 'disabled' ? 'DISABLED' : 'ERROR');
|
|
if (provider.status === 'ok') {
|
|
const prior = state.last[name];
|
|
state.last[name] = provider;
|
|
state.countdowns.forEach((_, node) => { if (content.contains(node)) state.countdowns.delete(node); });
|
|
clearContent(content);
|
|
provider.limits.forEach((limit) => {
|
|
const priorLimit = prior && prior.limits.find((entry) => entry.id === limit.id);
|
|
content.append(renderLimit(limit, priorLimit));
|
|
});
|
|
return;
|
|
}
|
|
if (provider.status === 'error' && state.last[name]) {
|
|
if (!content.querySelector('.overlay')) {
|
|
const overlay = document.createElement('div'); overlay.className = 'overlay'; const strong = document.createElement('strong'); text(strong, 'Data currently unavailable'); overlay.append(strong);
|
|
const small = document.createElement('small'); const lastUpdate = provider.last_successful_update || state.last[name].last_successful_update; text(small, lastUpdate ? `Last successful update: ${clockValue(new Date(lastUpdate))}` : 'No successful update yet'); overlay.append(small); content.append(overlay);
|
|
}
|
|
return;
|
|
}
|
|
state.countdowns.forEach((_, node) => { if (content.contains(node)) state.countdowns.delete(node); });
|
|
clearContent(content);
|
|
const notice = document.createElement('div'); notice.className = 'notice'; const strong = document.createElement('strong'); text(strong, provider.status === 'disabled' ? 'Provider disabled' : 'Data currently unavailable'); notice.append(strong); const detail = document.createElement('span'); text(detail, provider.status === 'disabled' ? 'Enable this provider in configuration to view usage.' : 'Waiting for a successful update.'); notice.append(detail); content.append(notice);
|
|
};
|
|
const setConnection = (online) => { text(byId('connection-state'), online ? 'LIVE' : 'Connection error'); byId('connection-dot').classList.toggle('offline', !online); };
|
|
const fetchUsage = async () => {
|
|
try {
|
|
const response = await fetch('/api/usage', { cache: 'no-store' });
|
|
if (!response.ok) throw new Error('request failed');
|
|
const data = await response.json();
|
|
renderProvider('codex', data.codex); renderProvider('devin', data.devin); state.refresh = Math.max(1, Number(data.refresh_interval) || 10);
|
|
byId('demo-badge').hidden = !data.demo_mode; setConnection(true); text(byId('last-update'), `Last update: ${clockValue(new Date(data.server_time))}`);
|
|
window.clearInterval(state.networkTimer); state.networkTimer = window.setInterval(fetchUsage, state.refresh * 1000);
|
|
} catch (_) { setConnection(false); window.clearInterval(state.networkTimer); state.networkTimer = window.setInterval(fetchUsage, state.refresh * 1000); }
|
|
};
|
|
state.networkTimer = null; updateClock(); setInterval(() => { updateClock(); updateCountdowns(); }, 1000); fetchUsage();
|
|
})();
|