Fix database connection, add postgres healthcheck, update deployment templates
Build and Push Docker Image / build-and-push (push) Canceled after 0s
Build and Push Docker Image / build-and-push (push) Canceled after 0s
This commit is contained in:
1 parent
516a498f75
commit
b312ac38ef
9 files changed
+450
-131
No files matched your search
@@ -0,0 +1,18 @@
|
||||
PORT=3000
|
||||
|
||||
# Database Configuration
|
||||
DB_USER=postgres
|
||||
DB_HOST=postgres
|
||||
DB_NAME=tokenwatch
|
||||
DB_PASSWORD=(H-?T5cNdSzQjOcA0wu(XjhBFuXh)6_2k0rCM,}v5c
|
||||
DB_PORT=5432
|
||||
|
||||
# ntfy Configuration
|
||||
NTFY_URL=https://ntfy.orfel.de/Token-Watch
|
||||
ALERT_THRESHOLD_PERCENT=80
|
||||
|
||||
# Provider API Keys
|
||||
ANTIGRAVITY_API_KEY=your_antigravity_key
|
||||
ANTIGRAVITY_ENDPOINT=https://api.antigravity.google/v1/quota
|
||||
POE_API_KEY=sk-poe-kYNW4JAHC5vPDPXEKoQZBRIh9kzjpI1aStcBfrG8NLw
|
||||
MINIMAX_API_KEY=sk-api-_uWU8wuYW49aO5UwxYojPxBx0XPVlnwPzD2UybaBFgwsy9nxU9FsqeFn9ihIsXWOerUvTz3X4bpQs9cN7BfLBmcedrGhI5t5bpX6rEfpeARg_8YNl8ePGJI
|
||||
@@ -8,7 +8,7 @@ services:
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
# TokenWatch liest DATABASE_URL und NTFY_URL aus der .env
|
||||
# TokenWatch liest DB_USER, DB_PASSWORD, etc. und NTFY_URL aus der .env
|
||||
NODE_ENV: production
|
||||
networks:
|
||||
- jannik-cloud-net
|
||||
|
||||
@@ -15,18 +15,14 @@ cat > "${SCRIPT_DIR}/.env" <<EOF
|
||||
DB_USER=${DB_USER}
|
||||
DB_PASSWORD=${DB_PASSWORD}
|
||||
DB_NAME=${DB_NAME}
|
||||
DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@postgres:5432/${DB_NAME}?sslmode=disable
|
||||
DB_HOST=postgres
|
||||
|
||||
NTFY_URL=https://ntfy.orfel.de/Token-Watch
|
||||
EOF
|
||||
|
||||
chmod 600 "${SCRIPT_DIR}/.env"
|
||||
|
||||
cat > "${SCRIPT_DIR}/tokenwatch.caddy" <<'EOF'
|
||||
tokenwatch.orfel.de {
|
||||
reverse_proxy 127.0.0.1:3500
|
||||
}
|
||||
EOF
|
||||
|
||||
|
||||
if [[ -f "${AGE_PUB_KEY}" ]]; then
|
||||
age -r "$(cat "${AGE_PUB_KEY}")" -o "${SCRIPT_DIR}/.env.age" "${SCRIPT_DIR}/.env"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
tokenwatch.orfel.de {
|
||||
reverse_proxy 127.0.0.1:3500
|
||||
}
|
||||
+7
-3
@@ -1,5 +1,3 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
tokenwatch:
|
||||
build: .
|
||||
@@ -18,7 +16,8 @@ services:
|
||||
- POE_API_KEY=${POE_API_KEY}
|
||||
- MINIMAX_API_KEY=${MINIMAX_API_KEY}
|
||||
depends_on:
|
||||
- postgres
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
@@ -27,6 +26,11 @@ services:
|
||||
POSTGRES_USER: ${DB_USER:-postgres}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-your_db_password}
|
||||
POSTGRES_DB: ${DB_NAME:-tokenwatch}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
|
||||
|
||||
+113
-35
@@ -1,60 +1,75 @@
|
||||
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 data = await fetch('/api/history'); // Using history endpoint as mockup for latest if DB is empty? Wait, no, quotas endpoint should return latest.
|
||||
const quotas = await response.json();
|
||||
|
||||
// If DB is empty, mock data for UI testing if quotas is empty
|
||||
// Mock data if DB is empty
|
||||
const activeQuotas = quotas.length > 0 ? quotas : [
|
||||
{ provider: 'Antigravity', used: 730, total: 1000, percentage: 73 },
|
||||
{ provider: 'Antigravity', used: 68240, total: 100000, percentage: 68.24 },
|
||||
{ provider: 'POE', used: 33750, total: 75000, percentage: 45 },
|
||||
{ provider: 'MiniMax', used: 60, total: 100, percentage: 60 }
|
||||
];
|
||||
|
||||
activeQuotas.forEach(quota => {
|
||||
updateCard(quota.provider.toLowerCase(), quota);
|
||||
updateRow(quota.provider.toLowerCase(), quota);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching quotas:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function updateCard(providerId, quota) {
|
||||
function updateRow(providerId, quota) {
|
||||
const fill = document.getElementById(`fill-${providerId}`);
|
||||
const statusText = document.getElementById(`status-${providerId}`);
|
||||
const meme = document.getElementById(`meme-${providerId}`);
|
||||
const percentText = document.getElementById(`percentage-${providerId}`);
|
||||
|
||||
if (!fill || !statusText) return;
|
||||
if (!fill || !statusText || !percentText) return;
|
||||
|
||||
const percentage = quota.percentage;
|
||||
fill.style.width = `${percentage}%`;
|
||||
|
||||
// Set color gradient position based on percentage
|
||||
fill.style.backgroundPosition = `${100 - percentage}% 0`;
|
||||
// Update percentage text
|
||||
percentText.innerText = `${Math.round(percentage)}%`;
|
||||
|
||||
// Meme reactions based on percentage
|
||||
// Status coloring
|
||||
percentText.className = 'percentage'; // reset
|
||||
if (percentage > 90) {
|
||||
meme.style.transform = 'scale(1.2) rotate(5deg)';
|
||||
statusText.style.color = 'var(--danger)';
|
||||
} else if (percentage < 20) {
|
||||
statusText.style.color = 'var(--success)';
|
||||
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.toLowerCase() === 'minimax' && quota.meta) {
|
||||
const reset5h = new Date(quota.meta.intervalReset).toLocaleTimeString();
|
||||
const resetWeekly = new Date(quota.meta.weeklyReset).toLocaleDateString();
|
||||
statusText.innerHTML = `5h Limit: ${quota.meta.intervalUsed}% (Resets: ${reset5h})<br>Weekly: ${quota.meta.weeklyUsed}% (Resets: ${resetWeekly})`;
|
||||
statusText.innerHTML = `5h: ${quota.meta.intervalUsed}% (Resets: ${reset5h})`;
|
||||
} else if (quota.provider.toLowerCase() === 'antigravity' && quota.meta) {
|
||||
statusText.innerHTML = `5h Limit: ${quota.meta.intervalUsed}% (Resets: ${quota.meta.intervalResetStr})<br>Weekly: ${quota.meta.weeklyUsed}% (Resets: ${quota.meta.weeklyResetStr})`;
|
||||
} else if (quota.provider.toLowerCase() === 'poe') {
|
||||
statusText.innerText = `${percentage.toFixed(1)}% Used (${(quota.used).toLocaleString()} / ${(quota.total).toLocaleString()})`;
|
||||
statusText.innerHTML = `5h: ${quota.meta.intervalUsed}% (Resets: ${quota.meta.intervalResetStr})`;
|
||||
} else {
|
||||
statusText.innerText = `${percentage.toFixed(1)}% Used (${quota.used} / ${quota.total})`;
|
||||
statusText.innerText = `${(quota.used).toLocaleString()} / ${(quota.total).toLocaleString()}`;
|
||||
}
|
||||
|
||||
if (percentage >= 100) {
|
||||
@@ -63,7 +78,8 @@ function updateCard(providerId, quota) {
|
||||
spread: 70,
|
||||
origin: { y: 0.6 }
|
||||
});
|
||||
statusText.innerText = 'LIMIT REACHED! 😱';
|
||||
statusText.innerText = 'LIMIT REACHED!';
|
||||
statusText.style.color = 'var(--danger)';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +88,6 @@ async function fetchHistory() {
|
||||
const response = await fetch('/api/history');
|
||||
const history = await response.json();
|
||||
|
||||
// Process data for Chart.js
|
||||
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);
|
||||
@@ -80,42 +95,105 @@ async function fetchHistory() {
|
||||
const minimaxData = history.filter(item => item.provider === 'MiniMax').map(item => item.percentage);
|
||||
|
||||
const ctx = document.getElementById('historyChart').getContext('2d');
|
||||
new Chart(ctx, {
|
||||
|
||||
historyChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: labels.length ? labels : ['10:00', '11:00', '12:00', '13:00'], // Fallback labels
|
||||
labels: labels.length ? labels : ['10:00', '11:00', '12:00', '13:00'],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Antigravity Usage (%)',
|
||||
data: antigravityData.length ? antigravityData : [10, 30, 50, 73],
|
||||
borderColor: '#bb86fc',
|
||||
tension: 0.1
|
||||
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 Usage (%)',
|
||||
label: 'POE (%)',
|
||||
data: poeData.length ? poeData : [5, 10, 20, 45],
|
||||
borderColor: '#03dac6',
|
||||
tension: 0.1
|
||||
borderColor: '#3fb950',
|
||||
backgroundColor: 'rgba(63, 185, 80, 0.1)',
|
||||
borderWidth: 2,
|
||||
tension: 0.3,
|
||||
fill: true
|
||||
},
|
||||
{
|
||||
label: 'MiniMax Usage (%)',
|
||||
label: 'MiniMax (%)',
|
||||
data: minimaxData.length ? minimaxData : [0, 0, 30, 60],
|
||||
borderColor: '#f1c40f',
|
||||
tension: 0.1
|
||||
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
|
||||
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();
|
||||
}
|
||||
|
||||
+58
-25
@@ -8,45 +8,78 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.6.0/dist/confetti.browser.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<body class="style-standard">
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>TokenWatch 📈</h1>
|
||||
<p>API Limit & Quota Tracker</p>
|
||||
<div class="header-content">
|
||||
<div>
|
||||
<h1>TokenWatch</h1>
|
||||
<p>API Limit & Quota Tracker</p>
|
||||
</div>
|
||||
<div class="style-selector">
|
||||
<label for="theme-dropdown">Progress Bar Style:</label>
|
||||
<select id="theme-dropdown">
|
||||
<option value="style-standard">1. Standard</option>
|
||||
<option value="style-pacman">2. Pac-Man</option>
|
||||
<option value="style-battery">3. Battery</option>
|
||||
<option value="style-nyancat">4. Nyan Cat</option>
|
||||
<option value="style-matrix">5. Matrix</option>
|
||||
<option value="style-lava">6. Lava</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="dashboard">
|
||||
<div class="card" id="card-antigravity">
|
||||
<h2>Antigravity (Gemini)</h2>
|
||||
<div class="meme-container">
|
||||
<img id="meme-antigravity" class="meme" src="https://i.imgflip.com/1g8my4.jpg" alt="Antigravity Meme">
|
||||
<section class="dashboard-list">
|
||||
<!-- Antigravity Row -->
|
||||
<div class="provider-row" id="row-antigravity">
|
||||
<div class="provider-info">
|
||||
<span class="provider-icon">🤖</span>
|
||||
<span class="provider-name">Antigravity (Gemini)</span>
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="fill-antigravity"></div>
|
||||
<div class="progress-container">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="fill-antigravity"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="provider-stats">
|
||||
<span class="percentage" id="percentage-antigravity">0%</span>
|
||||
<span class="raw-value" id="status-antigravity">Loading...</span>
|
||||
</div>
|
||||
<p class="status-text" id="status-antigravity">Loading...</p>
|
||||
</div>
|
||||
|
||||
<div class="card" id="card-poe">
|
||||
<h2>POE</h2>
|
||||
<div class="meme-container">
|
||||
<img id="meme-poe" class="meme" src="https://i.kym-cdn.com/entries/icons/original/000/028/021/work.jpg" alt="POE Meme">
|
||||
<!-- POE Row -->
|
||||
<div class="provider-row" id="row-poe">
|
||||
<div class="provider-info">
|
||||
<span class="provider-icon">🧠</span>
|
||||
<span class="provider-name">POE</span>
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="fill-poe"></div>
|
||||
<div class="progress-container">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="fill-poe"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="provider-stats">
|
||||
<span class="percentage" id="percentage-poe">0%</span>
|
||||
<span class="raw-value" id="status-poe">Loading...</span>
|
||||
</div>
|
||||
<p class="status-text" id="status-poe">Loading...</p>
|
||||
</div>
|
||||
|
||||
<div class="card" id="card-minimax">
|
||||
<h2>MiniMax (M3)</h2>
|
||||
<div class="meme-container">
|
||||
<img id="meme-minimax" class="meme" src="https://i.kym-cdn.com/entries/icons/original/000/018/012/this_is_fine.jpeg" alt="MiniMax Meme">
|
||||
<!-- MiniMax Row -->
|
||||
<div class="provider-row" id="row-minimax">
|
||||
<div class="provider-info">
|
||||
<span class="provider-icon">⚡</span>
|
||||
<span class="provider-name">MiniMax (M3)</span>
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="fill-minimax"></div>
|
||||
<div class="progress-container">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="fill-minimax"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="provider-stats">
|
||||
<span class="percentage" id="percentage-minimax">0%</span>
|
||||
<span class="raw-value" id="status-minimax">Loading...</span>
|
||||
</div>
|
||||
<p class="status-text" id="status-minimax">Loading...</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
+245
-59
@@ -1,107 +1,293 @@
|
||||
:root {
|
||||
--bg-color: #121212;
|
||||
--card-bg: #1e1e1e;
|
||||
--text-color: #ffffff;
|
||||
--primary: #bb86fc;
|
||||
--danger: #cf6679;
|
||||
--success: #03dac6;
|
||||
--warning: #f1c40f;
|
||||
--bg-color: #0d1117;
|
||||
--row-bg: #161b22;
|
||||
--row-border: #30363d;
|
||||
--text-color: #c9d1d9;
|
||||
--text-muted: #8b949e;
|
||||
--primary: #58a6ff;
|
||||
--danger: #f85149;
|
||||
--success: #3fb950;
|
||||
--warning: #d29922;
|
||||
|
||||
--bar-bg: #21262d;
|
||||
--bar-height: 24px;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
margin-bottom: 30px;
|
||||
border-bottom: 1px solid var(--row-border);
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 3em;
|
||||
margin-bottom: 10px;
|
||||
font-size: 2em;
|
||||
margin: 0 0 5px 0;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.dashboard {
|
||||
header p {
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.style-selector label {
|
||||
margin-right: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.style-selector select {
|
||||
background: var(--row-bg);
|
||||
color: var(--text-color);
|
||||
border: 1px solid var(--row-border);
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 1em;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.dashboard-list {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 50px;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--card-bg);
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.meme-container {
|
||||
height: 150px;
|
||||
.provider-row {
|
||||
background-color: var(--row-bg);
|
||||
border: 1px solid var(--row-border);
|
||||
border-radius: 8px;
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 20px 0;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
gap: 20px;
|
||||
transition: transform 0.2s, border-color 0.2s;
|
||||
}
|
||||
|
||||
.meme {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
object-fit: contain;
|
||||
transition: transform 0.5s;
|
||||
.provider-row:hover {
|
||||
border-color: var(--text-muted);
|
||||
}
|
||||
|
||||
.provider-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 0 0 200px;
|
||||
}
|
||||
|
||||
.provider-icon {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.provider-name {
|
||||
font-weight: 600;
|
||||
font-size: 1.1em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
background-color: #333;
|
||||
height: 30px;
|
||||
border-radius: 15px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 15px;
|
||||
background-color: var(--bar-bg);
|
||||
height: var(--bar-height);
|
||||
width: 100%;
|
||||
border-radius: 12px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
background: linear-gradient(90deg, var(--success), var(--warning), var(--danger));
|
||||
background-size: 200% 100%;
|
||||
transition: width 1s ease-in-out, background-position 0.5s;
|
||||
border-radius: 15px;
|
||||
transition: width 0.8s ease-out;
|
||||
border-radius: 12px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 1.1em;
|
||||
.provider-stats {
|
||||
flex: 0 0 150px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.percentage {
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.raw-value {
|
||||
font-size: 0.85em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.charts {
|
||||
background-color: var(--card-bg);
|
||||
background-color: var(--row-bg);
|
||||
border: 1px solid var(--row-border);
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
canvas {
|
||||
max-height: 400px;
|
||||
max-height: 350px;
|
||||
}
|
||||
|
||||
|
||||
/* =========================================
|
||||
STYLE 1: STANDARD
|
||||
========================================= */
|
||||
.style-standard .progress-fill {
|
||||
background: linear-gradient(90deg, #58a6ff, #bb86fc);
|
||||
}
|
||||
|
||||
/* =========================================
|
||||
STYLE 2: PAC-MAN
|
||||
========================================= */
|
||||
.style-pacman .progress-bar {
|
||||
background-image: radial-gradient(#fff 15%, transparent 16%);
|
||||
background-size: 20px 20px;
|
||||
background-position: 0 center;
|
||||
}
|
||||
.style-pacman .progress-fill {
|
||||
background-color: var(--bg-color);
|
||||
border-radius: 0;
|
||||
}
|
||||
.style-pacman .progress-fill::after {
|
||||
content: "ᗧ";
|
||||
color: #f1c40f;
|
||||
font-size: 24px;
|
||||
line-height: var(--bar-height);
|
||||
position: absolute;
|
||||
right: -10px;
|
||||
top: -2px;
|
||||
text-shadow: 0 0 5px rgba(241,196,15,0.5);
|
||||
animation: chomp 0.5s infinite;
|
||||
}
|
||||
@keyframes chomp {
|
||||
0%, 100% { transform: scaleY(1); }
|
||||
50% { transform: scaleY(0.6); }
|
||||
}
|
||||
|
||||
/* =========================================
|
||||
STYLE 3: BATTERY
|
||||
========================================= */
|
||||
.style-battery .progress-bar {
|
||||
border-radius: 4px;
|
||||
border: 2px solid #555;
|
||||
padding: 2px;
|
||||
background: transparent;
|
||||
height: calc(var(--bar-height) - 4px);
|
||||
overflow: visible;
|
||||
}
|
||||
.style-battery .progress-bar::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -6px;
|
||||
top: 5px;
|
||||
width: 4px;
|
||||
height: calc(100% - 10px);
|
||||
background: #555;
|
||||
border-radius: 0 2px 2px 0;
|
||||
}
|
||||
.style-battery .progress-fill {
|
||||
background-color: #3fb950;
|
||||
border-radius: 2px;
|
||||
background-image: linear-gradient(90deg,
|
||||
transparent 0%, transparent 90%,
|
||||
var(--bg-color) 90%, var(--bg-color) 100%
|
||||
);
|
||||
background-size: 20px 100%;
|
||||
}
|
||||
|
||||
/* =========================================
|
||||
STYLE 4: NYAN CAT
|
||||
========================================= */
|
||||
.style-nyancat .progress-fill {
|
||||
background: linear-gradient(to bottom,
|
||||
#ff0000 16%, #ff9900 16% 32%, #ffff00 32% 48%,
|
||||
#33ff00 48% 64%, #0099ff 64% 80%, #6633ff 80%
|
||||
);
|
||||
border-radius: 0;
|
||||
}
|
||||
.style-nyancat .progress-fill::after {
|
||||
content: "🐈";
|
||||
font-size: 20px;
|
||||
position: absolute;
|
||||
right: -15px;
|
||||
top: -2px;
|
||||
}
|
||||
|
||||
/* =========================================
|
||||
STYLE 5: MATRIX
|
||||
========================================= */
|
||||
.style-matrix .progress-bar {
|
||||
background-color: #000;
|
||||
border-radius: 0;
|
||||
border: 1px solid #0f0;
|
||||
}
|
||||
.style-matrix .progress-fill {
|
||||
background-color: #000;
|
||||
border-radius: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.style-matrix .progress-fill::before {
|
||||
content: '0101100101011101000101110010101010110010101110100010111001010';
|
||||
color: #0f0;
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
letter-spacing: 2px;
|
||||
white-space: nowrap;
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 0;
|
||||
text-shadow: 0 0 5px #0f0;
|
||||
}
|
||||
|
||||
/* =========================================
|
||||
STYLE 6: LAVA
|
||||
========================================= */
|
||||
.style-lava .progress-bar {
|
||||
background-color: #2b110a;
|
||||
border: 2px solid #5a2c1f;
|
||||
}
|
||||
.style-lava .progress-fill {
|
||||
background: linear-gradient(90deg, #ff4e00, #ec9f05, #ff4e00);
|
||||
background-size: 200% 100%;
|
||||
animation: lavaFlow 2s linear infinite;
|
||||
box-shadow: 0 0 10px #ff4e00 inset;
|
||||
}
|
||||
@keyframes lavaFlow {
|
||||
0% { background-position: 100% 0; }
|
||||
100% { background-position: 0 0; }
|
||||
}
|
||||
|
||||
/* Colors for percentages based on status */
|
||||
.status-good { color: var(--success); }
|
||||
.status-warn { color: var(--warning); }
|
||||
.status-danger { color: var(--danger); }
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
import express from 'express';
|
||||
import path from 'path';
|
||||
import { getQuotaHistory } from './services/db';
|
||||
import { getQuotaHistory, initDb } from './services/db';
|
||||
import { fetchAllQuotas } from './services/providers';
|
||||
import './cron'; // Start the cron jobs
|
||||
|
||||
@@ -41,6 +41,7 @@ app.post('/api/webhook/antigravity', (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
app.listen(PORT, async () => {
|
||||
await initDb();
|
||||
console.log(`TokenWatch server running on port ${PORT}`);
|
||||
});
|
||||
Reference in new issue
Block a user