Files
TokenWatch/public/app.js
T
Jannik a84e62b2e0
Build and Push Docker Image / build-and-push (push) Canceled after 0s
feat: split MiniMax into 5h and Weekly quota bars
2026-08-28 22:41:20 +02:00

320 lines
12 KiB
JavaScript

let historyChart = null;
document.addEventListener('DOMContentLoaded', () => {
fetchQuotas();
fetchHistory();
setupThemeDropdown();
});
function setupThemeDropdown() {
const dropdown = document.getElementById('theme-dropdown');
dropdown.addEventListener('change', (e) => {
// Change body class
document.body.className = e.target.value;
// Update chart colors if chart exists
if (historyChart) {
updateChartTheme(e.target.value);
}
});
}
async function fetchQuotas() {
try {
const response = await fetch('/api/quotas');
const quotas = await response.json();
// Mock data if DB is empty
const activeQuotas = quotas.length > 0 ? quotas : [
{ provider: 'AG Gemini (5h)', used: 73, total: 100, percentage: 73, meta: { resetStr: 'in 01:23' } },
{ provider: 'AG Gemini (Weekly)', used: 73, total: 100, percentage: 73, meta: { resetStr: 'in 01:23' } },
{ provider: 'AG Claude (5h)', used: 0, total: 100, percentage: 0, meta: { resetStr: 'in 03:00' } },
{ provider: 'AG Claude (Weekly)', used: 0, total: 100, percentage: 0, meta: { resetStr: 'in 03:00' } },
{ provider: 'POE', used: 33750, total: 75000, percentage: 45 },
{ provider: 'MiniMax (5h)', used: 60, total: 100, percentage: 60, meta: { reset: 1729000000 } },
{ provider: 'MiniMax (Weekly)', used: 40, total: 100, percentage: 40, meta: { reset: 1729500000 } }
];
activeQuotas.forEach(quota => {
updateRow(quota.provider.toLowerCase(), quota);
});
} catch (error) {
console.error('Error fetching quotas:', error);
}
}
function updateRow(providerId, quota) {
const fill = document.getElementById(`fill-${providerId}`);
const statusText = document.getElementById(`status-${providerId}`);
const percentText = document.getElementById(`percentage-${providerId}`);
if (!fill || !statusText || !percentText) return;
const percentage = quota.percentage;
fill.style.width = `${percentage}%`;
// Update percentage text
percentText.innerText = `${Math.round(percentage)}%`;
// Status coloring
percentText.className = 'percentage'; // reset
if (quota.provider.toLowerCase() === 'poe') {
if (percentage < 10) {
percentText.classList.add('status-danger');
} else if (percentage < 30) {
percentText.classList.add('status-warn');
} else {
percentText.classList.add('status-good');
}
} else {
if (percentage > 90) {
percentText.classList.add('status-danger');
} else if (percentage > 70) {
percentText.classList.add('status-warn');
} else {
percentText.classList.add('status-good');
}
}
// Detail text
if (quota.provider.startsWith('MiniMax') && quota.meta) {
statusText.innerText = `${Math.round(quota.used)}% / 100%`;
const detailsId = `details-${quota.provider.toLowerCase().replace(/[^a-z0-9]/g, '-')}`;
const detailsEl = document.getElementById(detailsId);
if (detailsEl) {
const parseDate = (val) => {
if (!val) return '--';
const ts = (typeof val === 'number' && val < 9999999999) ? val * 1000 : val;
return new Date(ts).toLocaleString();
};
const resetStr = parseDate(quota.meta.reset);
if (quota.provider.includes('5h')) {
detailsEl.innerHTML = `
<div><span>Reset in:</span> <strong id="minimax-countdown" style="color: var(--success)">--:--:--</strong></div>
`;
const targetTs = (typeof quota.meta.reset === 'number' && quota.meta.reset < 9999999999) ? quota.meta.reset * 1000 : quota.meta.reset;
if (targetTs) startMinimaxCountdown(targetTs);
} else {
detailsEl.innerHTML = `
<div><span>Reset:</span> <strong style="color: var(--success)">${resetStr}</strong></div>
`;
}
}
} else if (quota.provider.startsWith('AG ') && quota.meta) {
statusText.innerText = `${Math.round(quota.used)}% / 100%`;
const detailsId = `details-${quota.provider.toLowerCase().replace(/[^a-z0-9]/g, '-')}`;
const detailsEl = document.getElementById(detailsId);
if (detailsEl) {
const resetStr = quota.meta.resetStr || '--';
detailsEl.innerHTML = `
<div><span>Reset:</span> <strong style="color: var(--success)">${resetStr}</strong></div>
`;
}
} else if (quota.provider.toLowerCase() === 'poe' && quota.meta) {
statusText.innerText = `${(quota.used).toLocaleString()} / ${(quota.total).toLocaleString()}`;
const detailsEl = document.getElementById(`details-poe`);
if (detailsEl) {
const { available, dailyRefresh, expiringPoints, daysUntilCap, todayUsed } = quota.meta;
detailsEl.innerHTML = `
<div><span>Übrig:</span> <strong style="color: #fff">${available.toLocaleString()}</strong></div>
<div><span>Heute Verbraucht:</span> <strong style="color: #fff">${(todayUsed || 0).toLocaleString()}</strong></div>
<div><span>Verfällt:</span> <strong style="color: ${expiringPoints > 0 ? 'var(--danger)' : 'var(--text-muted)'}">${expiringPoints.toLocaleString()}</strong></div>
<div><span>Tage bis Cap:</span> <strong style="color: #fff">${daysUntilCap}</strong></div>
<div><span>Refresh in:</span> <strong id="poe-countdown" style="color: var(--success)">--:--:--</strong></div>
`;
startPoeCountdown();
}
} else {
statusText.innerText = `${(quota.used).toLocaleString()} / ${(quota.total).toLocaleString()}`;
}
if (percentage >= 100) {
confetti({
particleCount: 100,
spread: 70,
origin: { y: 0.6 }
});
if (quota.provider.toLowerCase() !== 'poe') {
statusText.innerText = 'LIMIT REACHED!';
statusText.style.color = 'var(--danger)';
}
}
}
let poeCountdownInterval = null;
function startPoeCountdown() {
if (poeCountdownInterval) clearInterval(poeCountdownInterval);
const updateCountdown = () => {
const now = new Date();
const nextMidnightUTC = new Date();
nextMidnightUTC.setUTCHours(24, 0, 0, 0);
let diffMs = nextMidnightUTC.getTime() - now.getTime();
if (diffMs < 0) {
// Already passed, next midnight is +24h (edge case just after midnight)
diffMs += 24 * 60 * 60 * 1000;
}
const h = Math.floor(diffMs / (1000 * 60 * 60));
const m = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
const s = Math.floor((diffMs % (1000 * 60)) / 1000);
const el = document.getElementById('poe-countdown');
if (el) {
el.innerText = `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
}
};
updateCountdown();
poeCountdownInterval = setInterval(updateCountdown, 1000);
}
let minimaxCountdownInterval = null;
function startMinimaxCountdown(targetTs) {
if (minimaxCountdownInterval) clearInterval(minimaxCountdownInterval);
const targetDate = new Date(targetTs);
const updateCountdown = () => {
const now = new Date();
let diffMs = targetDate.getTime() - now.getTime();
if (diffMs < 0) {
// Already passed, show 00:00:00 or let the UI handle refresh
diffMs = 0;
}
const h = Math.floor(diffMs / (1000 * 60 * 60));
const m = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
const s = Math.floor((diffMs % (1000 * 60)) / 1000);
const el = document.getElementById('minimax-countdown');
if (el) {
el.innerText = `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
}
};
updateCountdown();
minimaxCountdownInterval = setInterval(updateCountdown, 1000);
}
async function fetchHistory() {
try {
const response = await fetch('/api/history');
const history = await response.json();
const labels = [...new Set(history.map(item => new Date(item.timestamp).toLocaleTimeString()))].sort();
const antigravityData = history.filter(item => item.provider === 'Antigravity').map(item => item.percentage);
const poeData = history.filter(item => item.provider === 'POE').map(item => item.percentage);
const minimaxData = history.filter(item => item.provider === 'MiniMax').map(item => item.percentage);
const ctx = document.getElementById('historyChart').getContext('2d');
historyChart = new Chart(ctx, {
type: 'line',
data: {
labels: labels.length ? labels : ['10:00', '11:00', '12:00', '13:00'],
datasets: [
{
label: 'Antigravity (%)',
data: antigravityData.length ? antigravityData : [10, 30, 50, 68],
borderColor: '#58a6ff',
backgroundColor: 'rgba(88, 166, 255, 0.1)',
borderWidth: 2,
tension: 0.3,
fill: true
},
{
label: 'POE (%)',
data: poeData.length ? poeData : [5, 10, 20, 45],
borderColor: '#3fb950',
backgroundColor: 'rgba(63, 185, 80, 0.1)',
borderWidth: 2,
tension: 0.3,
fill: true
},
{
label: 'MiniMax (%)',
data: minimaxData.length ? minimaxData : [0, 0, 30, 60],
borderColor: '#d29922',
backgroundColor: 'rgba(210, 153, 34, 0.1)',
borderWidth: 2,
tension: 0.3,
fill: true
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
labels: { color: '#c9d1d9' }
}
},
scales: {
y: {
beginAtZero: true,
max: 100,
grid: { color: '#30363d' },
ticks: { color: '#8b949e' }
},
x: {
grid: { color: '#30363d' },
ticks: { color: '#8b949e' }
}
}
}
});
// Apply initial theme to chart
updateChartTheme(document.body.className);
} catch (error) {
console.error('Error fetching history:', error);
}
}
function updateChartTheme(themeClass) {
if (!historyChart) return;
let colors = ['#58a6ff', '#3fb950', '#d29922']; // Default
switch (themeClass) {
case 'style-pacman':
colors = ['#f1c40f', '#f39c12', '#e67e22'];
break;
case 'style-nyancat':
colors = ['#ff0000', '#33ff00', '#0099ff'];
break;
case 'style-matrix':
colors = ['#0f0', '#0a0', '#050'];
break;
case 'style-lava':
colors = ['#ff4e00', '#ec9f05', '#bf3a00'];
break;
}
historyChart.data.datasets.forEach((dataset, index) => {
dataset.borderColor = colors[index % colors.length];
// Convert hex to rgba for background
let hex = colors[index % colors.length].replace('#', '');
if (hex.length === 3) {
hex = hex.split('').map(char => char + char).join('');
}
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
dataset.backgroundColor = `rgba(${r}, ${g}, ${b}, 0.1)`;
});
historyChart.update();
}