343 lines
14 KiB
JavaScript
343 lines
14 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 => {
|
|
const providerId = quota.provider.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
updateRow(providerId, quota);
|
|
});
|
|
} catch (error) {
|
|
console.error('Error fetching quotas:', error);
|
|
}
|
|
}
|
|
|
|
// Formats a timestamp as "Heute 14:30", "Morgen 09:00" or "Mo. 14:30"
|
|
function formatResetTime(ms) {
|
|
if (!ms) return '';
|
|
const d = new Date(ms);
|
|
const now = new Date();
|
|
const todayStr = now.toDateString();
|
|
const tomorrowStr = new Date(now.getTime() + 86400000).toDateString();
|
|
const timeStr = d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
|
if (d.toDateString() === todayStr) return `Heute ${timeStr}`;
|
|
if (d.toDateString() === tomorrowStr) return `Morgen ${timeStr}`;
|
|
const dayStr = d.toLocaleDateString('de-DE', { weekday: 'short' });
|
|
return `${dayStr} ${timeStr}`;
|
|
}
|
|
|
|
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
|
|
const detailsId = `details-${providerId}`;
|
|
const detailsEl = document.getElementById(detailsId);
|
|
const resetTimeEl = document.getElementById(`reset-time-${providerId}`);
|
|
|
|
if (quota.provider.startsWith('MiniMax') && quota.meta) {
|
|
statusText.innerText = `${Math.round(quota.used)}% / 100%`;
|
|
const resetTs = quota.meta.reset;
|
|
const targetMs = (typeof resetTs === 'number' && resetTs < 9999999999) ? resetTs * 1000 : resetTs;
|
|
const countdownId = `countdown-${detailsId}`;
|
|
|
|
if (resetTimeEl) resetTimeEl.innerText = formatResetTime(targetMs);
|
|
|
|
if (detailsEl) {
|
|
if (quota.provider.includes('5h')) {
|
|
detailsEl.innerHTML = `<div><span>Reset in:</span> <strong id="${countdownId}" style="color: var(--success)">--:--:--</strong></div>`;
|
|
if (targetMs) startCountdown(countdownId, targetMs);
|
|
} else {
|
|
if (targetMs && targetMs > Date.now()) {
|
|
detailsEl.innerHTML = `<div><span>Reset in:</span> <strong id="${countdownId}" style="color: var(--success)">--:--:--</strong></div>`;
|
|
startCountdown(countdownId, targetMs);
|
|
} else {
|
|
const resetStr = targetMs ? new Date(targetMs).toLocaleString() : '--';
|
|
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 resetTs = quota.meta.resetTime;
|
|
const targetMs = resetTs ? new Date(resetTs).getTime() : null;
|
|
const countdownId = `countdown-${detailsId}`;
|
|
|
|
if (resetTimeEl) resetTimeEl.innerText = formatResetTime(targetMs);
|
|
|
|
if (detailsEl) {
|
|
if (targetMs && targetMs > Date.now()) {
|
|
detailsEl.innerHTML = `<div><span>Reset in:</span> <strong id="${countdownId}" style="color: var(--success)">--:--:--</strong></div>`;
|
|
startCountdown(countdownId, targetMs);
|
|
} else if (targetMs) {
|
|
detailsEl.innerHTML = `<div><span>Reset:</span> <strong style="color: var(--success)">${new Date(targetMs).toLocaleString()}</strong></div>`;
|
|
} else {
|
|
detailsEl.innerHTML = `<div><span>Reset:</span> <strong style="color: var(--text-muted)">--</strong></div>`;
|
|
}
|
|
}
|
|
} else if (quota.provider.toLowerCase() === 'poe' && quota.meta) {
|
|
statusText.innerText = `${(quota.meta.available).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 countdownIntervals = {};
|
|
function startCountdown(elementId, targetMs) {
|
|
if (countdownIntervals[elementId]) clearInterval(countdownIntervals[elementId]);
|
|
|
|
const updateCountdown = () => {
|
|
let diffMs = targetMs - Date.now();
|
|
if (diffMs < 0) 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(elementId);
|
|
if (el) {
|
|
el.innerText = `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
|
} else {
|
|
clearInterval(countdownIntervals[elementId]);
|
|
delete countdownIntervals[elementId];
|
|
}
|
|
};
|
|
|
|
updateCountdown();
|
|
countdownIntervals[elementId] = setInterval(updateCountdown, 1000);
|
|
}
|
|
|
|
async function fetchHistory() {
|
|
try {
|
|
const response = await fetch('/api/history');
|
|
const history = await response.json();
|
|
|
|
// Group by 5-minute intervals to align points from the same fetch batch
|
|
const snapshots = {};
|
|
history.forEach(item => {
|
|
const time = new Date(item.timestamp);
|
|
const coeff = 1000 * 60 * 5;
|
|
const roundedTime = new Date(Math.round(time.getTime() / coeff) * coeff);
|
|
const timeKey = roundedTime.getTime();
|
|
|
|
if (!snapshots[timeKey]) {
|
|
snapshots[timeKey] = {
|
|
label: roundedTime.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}),
|
|
data: {}
|
|
};
|
|
}
|
|
snapshots[timeKey].data[item.provider] = item.percentage;
|
|
});
|
|
|
|
const sortedKeys = Object.keys(snapshots).sort((a, b) => a - b);
|
|
const labels = sortedKeys.map(k => snapshots[k].label);
|
|
|
|
const datasetsConfig = [
|
|
{ label: 'AG Gemini (5h)', provider: 'AG Gemini (5h)', color: '#58a6ff' },
|
|
{ label: 'AG Claude (5h)', provider: 'AG Claude (5h)', color: '#8957e5' },
|
|
{ label: 'POE', provider: 'POE', color: '#3fb950' },
|
|
{ label: 'MiniMax (5h)', provider: 'MiniMax (5h)', color: '#d29922' }
|
|
];
|
|
|
|
const datasets = datasetsConfig.map(config => ({
|
|
label: config.label,
|
|
data: sortedKeys.length ? sortedKeys.map(k => snapshots[k].data[config.provider] || 0) : [0, 0, 0, 0],
|
|
borderColor: config.color,
|
|
backgroundColor: config.color + '1a', // will be overridden by theme
|
|
borderWidth: 2,
|
|
tension: 0.3,
|
|
fill: true
|
|
}));
|
|
|
|
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: datasets
|
|
},
|
|
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', '#8957e5', '#3fb950', '#d29922']; // Default
|
|
|
|
switch (themeClass) {
|
|
case 'style-pacman':
|
|
colors = ['#f1c40f', '#f39c12', '#e67e22', '#d35400'];
|
|
break;
|
|
case 'style-nyancat':
|
|
colors = ['#ff0000', '#ff9900', '#33ff00', '#0099ff'];
|
|
break;
|
|
case 'style-matrix':
|
|
colors = ['#0f0', '#0c0', '#0a0', '#050'];
|
|
break;
|
|
case 'style-lava':
|
|
colors = ['#ff4e00', '#ff7a00', '#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();
|
|
}
|
|
|