Skip to content

Instantly share code, notes, and snippets.

@cyberofficial
Last active March 12, 2026 20:17
Show Gist options
  • Select an option

  • Save cyberofficial/c84072758418acbf2e1d448d8349ca59 to your computer and use it in GitHub Desktop.

Select an option

Save cyberofficial/c84072758418acbf2e1d448d8349ca59 to your computer and use it in GitHub Desktop.
Simple dashboard for LLM cost basis from Vultr Inference

Simple dashboard for LLM cost basis from Vultr Inference

image
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vultr Inference Usage Dashboard</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary: #00bfb3;
--primary-dark: #009d93;
--bg-light: #f8f9fa;
--bg-card-light: #ffffff;
--text-light: #212529;
--text-muted-light: #6c757d;
--border-light: #dee2e6;
--error: #dc3545;
--success: #28a745;
--warning: #ffc107;
--bg-dark: #1a1a2e;
--bg-card-dark: #16213e;
--text-dark: #e4e4e4;
--text-muted-dark: #a0a0a0;
--border-dark: #2d3748;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: var(--bg-light);
color: var(--text-light);
line-height: 1.6;
transition: all 0.3s ease;
min-height: 100vh;
padding: 20px;
}
body.dark-mode {
background: var(--bg-dark);
color: var(--text-dark);
}
.container {
max-width: 1200px;
margin: 0 auto;
}
/* Header */
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding: 20px;
background: var(--bg-card-light);
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
body.dark-mode .header {
background: var(--bg-card-dark);
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
.header h1 {
font-size: 1.8rem;
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.header-controls {
display: flex;
gap: 15px;
align-items: center;
}
/* Buttons */
.btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 191, 179, 0.4);
}
.btn-primary.tracking {
background: linear-gradient(135deg, #f5576c, #f093fb);
}
.btn-primary.tracking:hover {
box-shadow: 0 4px 12px rgba(245, 87, 108, 0.4);
}
.btn-secondary {
background: var(--border-light);
color: var(--text-light);
}
body.dark-mode .btn-secondary {
background: var(--border-dark);
color: var(--text-dark);
}
/* Theme Toggle */
.theme-toggle {
background: none;
border: 2px solid var(--border-light);
width: 40px;
height: 40px;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
transition: all 0.3s ease;
}
body.dark-mode .theme-toggle {
border-color: var(--border-dark);
}
.theme-toggle:hover {
background: var(--border-light);
}
body.dark-mode .theme-toggle:hover {
background: var(--border-dark);
}
/* API Key Input */
.api-key-section {
background: var(--bg-card-light);
padding: 20px;
border-radius: 12px;
margin-bottom: 30px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
body.dark-mode .api-key-section {
background: var(--bg-card-dark);
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
.input-group {
display: flex;
gap: 10px;
align-items: center;
}
.input-wrapper {
position: relative;
flex: 1;
}
.api-input {
width: 100%;
padding: 12px 45px 12px 15px;
border: 2px solid var(--border-light);
border-radius: 8px;
font-size: 0.95rem;
background: var(--bg-light);
color: var(--text-light);
transition: all 0.3s ease;
}
body.dark-mode .api-input {
background: var(--bg-dark);
color: var(--text-dark);
border-color: var(--border-dark);
}
.api-input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(0, 191, 179, 0.1);
}
.toggle-password {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
cursor: pointer;
font-size: 1.1rem;
color: var(--text-muted-light);
}
body.dark-mode .toggle-password {
color: var(--text-muted-dark);
}
/* Refresh Controls */
.refresh-controls {
display: flex;
gap: 15px;
align-items: center;
margin-top: 15px;
flex-wrap: wrap;
}
.interval-selector {
display: flex;
align-items: center;
gap: 10px;
}
.interval-selector select {
padding: 8px 12px;
border: 2px solid var(--border-light);
border-radius: 6px;
background: var(--bg-light);
color: var(--text-light);
cursor: pointer;
}
body.dark-mode .interval-selector select {
background: var(--bg-dark);
color: var(--text-dark);
border-color: var(--border-dark);
}
.auto-refresh-toggle {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.toggle-switch {
width: 50px;
height: 26px;
background: var(--border-light);
border-radius: 13px;
position: relative;
transition: all 0.3s ease;
}
body.dark-mode .toggle-switch {
background: var(--border-dark);
}
.toggle-switch.active {
background: var(--primary);
}
.toggle-switch::after {
content: '';
position: absolute;
width: 22px;
height: 22px;
background: white;
border-radius: 50%;
top: 2px;
left: 2px;
transition: all 0.3s ease;
}
.toggle-switch.active::after {
left: 26px;
}
.countdown {
font-size: 0.9rem;
color: var(--text-muted-light);
font-weight: 600;
}
body.dark-mode .countdown {
color: var(--text-muted-dark);
}
/* Status Indicator */
.status-bar {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
padding: 12px 20px;
background: var(--bg-card-light);
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
body.dark-mode .status-bar {
background: var(--bg-card-dark);
}
.status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--success);
animation: pulse 2s infinite;
}
.status-dot.loading {
background: var(--warning);
animation: blink 1s infinite;
}
.status-dot.error {
background: var(--error);
animation: none;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
.status-text {
font-weight: 600;
font-size: 0.95rem;
}
.last-updated {
margin-left: auto;
color: var(--text-muted-light);
font-size: 0.85rem;
}
body.dark-mode .last-updated {
color: var(--text-muted-dark);
}
/* Grid */
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.card {
background: var(--bg-card-light);
padding: 24px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
body.dark-mode .card {
background: var(--bg-card-dark);
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
}
.card-title {
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-muted-light);
margin-bottom: 8px;
font-weight: 700;
}
body.dark-mode .card-title {
color: var(--text-muted-dark);
}
.card-value {
font-size: 2rem;
font-weight: 700;
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.card-subtitle {
font-size: 0.8rem;
color: var(--text-muted-light);
margin-top: 4px;
}
body.dark-mode .card-subtitle {
color: var(--text-muted-dark);
}
/* Sections */
.section {
background: var(--bg-card-light);
padding: 30px;
border-radius: 12px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
body.dark-mode .section {
background: var(--bg-card-dark);
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.section-title {
font-size: 1.3rem;
font-weight: 700;
}
/* Usage Items */
.usage-item {
margin-bottom: 20px;
}
.usage-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.usage-label {
font-weight: 600;
font-size: 0.95rem;
}
.usage-value {
font-weight: 700;
font-size: 1.1rem;
}
.usage-bar-bg {
width: 100%;
height: 8px;
background: var(--border-light);
border-radius: 4px;
overflow: hidden;
}
body.dark-mode .usage-bar-bg {
background: var(--border-dark);
}
.usage-bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--primary), var(--primary-dark));
border-radius: 4px;
transition: width 0.5s ease;
}
.usage-bar-fill.chat {
background: linear-gradient(90deg, #667eea, #764ba2);
}
.usage-bar-fill.tts {
background: linear-gradient(90deg, #f093fb, #f5576c);
}
.usage-bar-fill.image {
background: linear-gradient(90deg, #4facfe, #00f2fe);
}
.usage-details {
display: flex;
justify-content: space-between;
margin-top: 6px;
font-size: 0.8rem;
color: var(--text-muted-light);
}
body.dark-mode .usage-details {
color: var(--text-muted-dark);
}
/* Cost Section */
.cost-breakdown {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-top: 20px;
}
.cost-item {
padding: 15px;
background: var(--bg-light);
border-radius: 8px;
border-left: 4px solid var(--primary);
}
body.dark-mode .cost-item {
background: var(--bg-dark);
}
.cost-label {
font-size: 0.85rem;
color: var(--text-muted-light);
margin-bottom: 4px;
}
body.dark-mode .cost-label {
color: var(--text-muted-dark);
}
.cost-value {
font-size: 1.3rem;
font-weight: 700;
}
.cost-value.dollars {
color: var(--success);
}
/* Error Display */
.error-message {
background: rgba(220, 53, 69, 0.1);
border: 1px solid var(--error);
color: var(--error);
padding: 15px 20px;
border-radius: 8px;
margin-bottom: 20px;
display: none;
}
.error-message.show {
display: block;
}
/* Responsive */
@media (max-width: 768px) {
.header {
flex-direction: column;
gap: 15px;
text-align: center;
}
.header-controls {
flex-wrap: wrap;
justify-content: center;
}
.refresh-controls {
justify-content: center;
}
.input-group {
flex-direction: column;
}
.grid {
grid-template-columns: 1fr;
}
}
/* Loading Spinner */
.spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(0, 191, 179, 0.3);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<div class="header">
<h1>🚀 Vultr Inference Usage Dashboard</h1>
<div class="header-controls">
<button class="theme-toggle" id="themeToggle" title="Toggle Dark Mode">🌙</button>
</div>
</div>
<!-- API Key Section -->
<div class="api-key-section">
<div class="input-group">
<div class="input-wrapper">
<input
type="password"
id="apiKeyInput"
class="api-input"
placeholder="Enter your Vultr API Key..."
autocomplete="off"
>
<button class="toggle-password" id="togglePassword" title="Show/Hide API Key">👁️</button>
</div>
<button class="btn btn-primary" id="saveKeyBtn">
💾 Save Key
</button>
<button class="btn btn-secondary" id="refreshBtn">
🔄 Refresh
</button>
</div>
<div class="refresh-controls">
<div class="interval-selector">
<label for="intervalSelect">Refresh every:</label>
<select id="intervalSelect">
<option value="5">5 seconds</option>
<option value="10">10 seconds</option>
<option value="30">30 seconds</option>
<option value="60">1 minute</option>
</select>
</div>
<div class="auto-refresh-toggle" id="autoRefreshToggle">
<div class="toggle-switch" id="toggleSwitch"></div>
<span>Auto-refresh</span>
</div>
<span class="countdown" id="countdown"></span>
</div>
</div>
<!-- Error Message -->
<div class="error-message" id="errorMessage"></div>
<!-- Status Bar -->
<div class="status-bar">
<div class="status-dot" id="statusDot"></div>
<span class="status-text" id="statusText">Ready - Enter API Key to begin</span>
<span class="last-updated" id="lastUpdated"></span>
</div>
<!-- Session Tracking -->
<div class="section" style="background: linear-gradient(135deg, rgba(0, 191, 179, 0.1), rgba(0, 157, 147, 0.1)); border: 2px solid var(--primary);">
<div class="section-header">
<h2 class="section-title">⏱️ Session Tracking</h2>
<button class="btn btn-primary" id="startTrackingBtn">
▶️ Start Tracking
</button>
</div>
<div class="grid" style="margin-bottom: 0;">
<div class="card">
<div class="card-title">Chat Completion Tokens</div>
<div class="card-value" id="sessionCompletionTokens">0</div>
<div class="card-subtitle" id="sessionCompletionCost">$0.00 cost</div>
</div>
<div class="card">
<div class="card-title">Chat Input Tokens</div>
<div class="card-value" id="sessionInputTokens">0</div>
<div class="card-subtitle" id="sessionInputCost">$0.00 cost</div>
</div>
<div class="card">
<div class="card-title">Total Session Tokens</div>
<div class="card-value" id="sessionTokens">0</div>
<div class="card-subtitle">Completion + Input</div>
</div>
<div class="card">
<div class="card-title">Total Session Cost</div>
<div class="card-value" id="sessionCost">$0.00</div>
<div class="card-subtitle">Completion + Input</div>
</div>
<div class="card">
<div class="card-title">Session Duration</div>
<div class="card-value" id="sessionDuration">00:00:00</div>
<div class="card-subtitle">Time tracking</div>
</div>
</div>
</div>
<!-- Summary Cards -->
<div class="grid">
<div class="card">
<div class="card-title">Current Month Chat Tokens</div>
<div class="card-value" id="currentTokens">0</div>
<div class="card-subtitle">Total (input + completion)</div>
</div>
<div class="card">
<div class="card-title">Current Month Cost</div>
<div class="card-value" id="currentCost">$0.00</div>
<div class="card-subtitle">Estimated based on usage</div>
</div>
<div class="card">
<div class="card-title">TTS Characters</div>
<div class="card-value" id="ttsChars">0</div>
<div class="card-subtitle">Text-to-speech this month</div>
</div>
<div class="card">
<div class="card-title">Images Generated</div>
<div class="card-value" id="imagePixels">0</div>
<div class="card-subtitle">Megapixels this month</div>
</div>
</div>
<!-- Current Month Section -->
<div class="section">
<div class="section-header">
<h2 class="section-title">📊 Current Month Usage</h2>
</div>
<div class="usage-item">
<div class="usage-header">
<span class="usage-label">💬 Chat Completion Tokens</span>
<span class="usage-value" id="currentChat">0 tokens</span>
</div>
<div class="usage-bar-bg">
<div class="usage-bar-fill chat" id="currentChatBar" style="width: 0%"></div>
</div>
<div class="usage-details">
<span>$2.75 per 1M tokens</span>
<span id="currentChatCost">$0.00</span>
</div>
</div>
<div class="usage-item">
<div class="usage-header">
<span class="usage-label">📥 Chat Input Tokens</span>
<span class="usage-value" id="currentChatInput">0 tokens</span>
</div>
<div class="usage-bar-bg">
<div class="usage-bar-fill chat" id="currentChatInputBar" style="width: 0%"></div>
</div>
<div class="usage-details">
<span>$0.55 per 1M tokens</span>
<span id="currentChatInputCost">$0.00</span>
</div>
</div>
<div class="usage-item">
<div class="usage-header">
<span class="usage-label">🔊 TTS (HD Model)</span>
<span class="usage-value" id="currentTts">0 characters</span>
</div>
<div class="usage-bar-bg">
<div class="usage-bar-fill tts" id="currentTtsBar" style="width: 0%"></div>
</div>
<div class="usage-details">
<span>Standard HD model</span>
</div>
</div>
<div class="usage-item">
<div class="usage-header">
<span class="usage-label">🔉 TTS (Basic Model)</span>
<span class="usage-value" id="currentTtsSm">0 characters</span>
</div>
<div class="usage-bar-bg">
<div class="usage-bar-fill tts" id="currentTtsSmBar" style="width: 0%"></div>
</div>
<div class="usage-details">
<span>Basic model</span>
</div>
</div>
<div class="usage-item">
<div class="usage-header">
<span class="usage-label">🖼️ Images (Standard)</span>
<span class="usage-value" id="currentImage">0 MP</span>
</div>
<div class="usage-bar-bg">
<div class="usage-bar-fill image" id="currentImageBar" style="width: 0%"></div>
</div>
<div class="usage-details">
<span>Standard model megapixels</span>
</div>
</div>
<div class="usage-item">
<div class="usage-header">
<span class="usage-label">🖼️ Images (Small)</span>
<span class="usage-value" id="currentImageSm">0 MP</span>
</div>
<div class="usage-bar-bg">
<div class="usage-bar-fill image" id="currentImageSmBar" style="width: 0%"></div>
</div>
<div class="usage-details">
<span>Small model megapixels</span>
</div>
</div>
<!-- Cost Breakdown -->
<div class="cost-breakdown">
<div class="cost-item">
<div class="cost-label">Completion Cost</div>
<div class="cost-value dollars" id="completionCost">$0.00</div>
</div>
<div class="cost-item">
<div class="cost-label">Input Cost</div>
<div class="cost-value dollars" id="inputCost">$0.00</div>
</div>
<div class="cost-item">
<div class="cost-label">Total Chat Cost</div>
<div class="cost-value dollars" id="totalChatCost">$0.00</div>
</div>
</div>
</div>
<!-- Previous Month Section -->
<div class="section">
<div class="section-header">
<h2 class="section-title">📈 Previous Month Usage</h2>
</div>
<div class="usage-item">
<div class="usage-header">
<span class="usage-label">💬 Chat Completion Tokens</span>
<span class="usage-value" id="prevChat">0 tokens</span>
</div>
<div class="usage-bar-bg">
<div class="usage-bar-fill chat" id="prevChatBar" style="width: 0%"></div>
</div>
<div class="usage-details">
<span id="prevChatCost">$0.00</span>
</div>
</div>
<div class="usage-item">
<div class="usage-header">
<span class="usage-label">📥 Chat Input Tokens</span>
<span class="usage-value" id="prevChatInput">0 tokens</span>
</div>
<div class="usage-bar-bg">
<div class="usage-bar-fill chat" id="prevChatInputBar" style="width: 0%"></div>
</div>
<div class="usage-details">
<span id="prevChatInputCost">$0.00</span>
</div>
</div>
<div class="cost-breakdown">
<div class="cost-item">
<div class="cost-label">Total Month Cost</div>
<div class="cost-value dollars" id="prevTotalCost">$0.00</div>
</div>
<div class="cost-item">
<div class="cost-label">Total Tokens</div>
<div class="cost-value" id="prevTotalTokens">0</div>
</div>
</div>
</div>
</div>
<script>
// Pricing constants
const PRICING = {
completion_per_million: 2.75,
input_per_million: 0.55
};
// State
let state = {
apiKey: localStorage.getItem('vultr_api_key') || '',
autoRefresh: false,
refreshInterval: 5,
countdownValue: 5,
intervalId: null,
sessionTracking: false,
sessionStart: null,
sessionStartData: null,
sessionIntervalId: null
};
// DOM Elements
const elements = {
apiKeyInput: document.getElementById('apiKeyInput'),
startTrackingBtn: document.getElementById('startTrackingBtn'),
sessionTokens: document.getElementById('sessionTokens'),
sessionCompletionTokens: document.getElementById('sessionCompletionTokens'),
sessionInputTokens: document.getElementById('sessionInputTokens'),
sessionCompletionCost: document.getElementById('sessionCompletionCost'),
sessionInputCost: document.getElementById('sessionInputCost'),
sessionCost: document.getElementById('sessionCost'),
sessionDuration: document.getElementById('sessionDuration'),
togglePassword: document.getElementById('togglePassword'),
togglePassword: document.getElementById('togglePassword'),
saveKeyBtn: document.getElementById('saveKeyBtn'),
refreshBtn: document.getElementById('refreshBtn'),
intervalSelect: document.getElementById('intervalSelect'),
autoRefreshToggle: document.getElementById('autoRefreshToggle'),
toggleSwitch: document.getElementById('toggleSwitch'),
countdown: document.getElementById('countdown'),
themeToggle: document.getElementById('themeToggle'),
statusDot: document.getElementById('statusDot'),
statusText: document.getElementById('statusText'),
lastUpdated: document.getElementById('lastUpdated'),
errorMessage: document.getElementById('errorMessage'),
// Current month
currentTokens: document.getElementById('currentTokens'),
currentCost: document.getElementById('currentCost'),
ttsChars: document.getElementById('ttsChars'),
imagePixels: document.getElementById('imagePixels'),
currentChat: document.getElementById('currentChat'),
currentChatBar: document.getElementById('currentChatBar'),
currentChatCost: document.getElementById('currentChatCost'),
currentChatInput: document.getElementById('currentChatInput'),
currentChatInputBar: document.getElementById('currentChatInputBar'),
currentChatInputCost: document.getElementById('currentChatInputCost'),
currentTts: document.getElementById('currentTts'),
currentTtsBar: document.getElementById('currentTtsBar'),
currentTtsSm: document.getElementById('currentTtsSm'),
currentTtsSmBar: document.getElementById('currentTtsSmBar'),
currentImage: document.getElementById('currentImage'),
currentImageBar: document.getElementById('currentImageBar'),
currentImageSm: document.getElementById('currentImageSm'),
currentImageSmBar: document.getElementById('currentImageSmBar'),
completionCost: document.getElementById('completionCost'),
inputCost: document.getElementById('inputCost'),
totalChatCost: document.getElementById('totalChatCost'),
// Previous month
prevChat: document.getElementById('prevChat'),
prevChatBar: document.getElementById('prevChatBar'),
prevChatCost: document.getElementById('prevChatCost'),
prevChatInput: document.getElementById('prevChatInput'),
prevChatInputBar: document.getElementById('prevChatInputBar'),
prevChatInputCost: document.getElementById('prevChatInputCost'),
prevTotalCost: document.getElementById('prevTotalCost'),
prevTotalTokens: document.getElementById('prevTotalTokens')
};
// Format numbers with commas
function formatNumber(num) {
return num.toLocaleString('en-US');
}
// Format currency
function formatCurrency(amount) {
return '$' + amount.toFixed(2);
}
// Calculate cost
function calculateCost(tokens, pricePerMillion) {
return (tokens / 1_000_000) * pricePerMillion;
}
// Update UI with data
function updateUI(data) {
// Handle wrapped response structure
const usageData = data.usage || data;
const current = usageData.current_month || {};
const previous = usageData.previous_month || {};
// Current month
const currentTotalTokens = (current.chat || 0) + (current.chat_input || 0);
const currentCompletionCost = calculateCost(current.chat || 0, PRICING.completion_per_million);
const currentInputCost = calculateCost(current.chat_input || 0, PRICING.input_per_million);
const currentTotalCost = currentCompletionCost + currentInputCost;
elements.currentTokens.textContent = formatNumber(currentTotalTokens);
elements.currentCost.textContent = formatCurrency(currentTotalCost);
elements.ttsChars.textContent = formatNumber((current.tts || 0) + (current.tts_sm || 0));
elements.imagePixels.textContent = ((current.image || 0) + (current.image_sm || 0)).toFixed(1) + ' MP';
elements.currentChat.textContent = formatNumber(current.chat || 0) + ' tokens';
elements.currentChatBar.style.width = Math.min((current.chat || 0) / 10000, 100) + '%';
elements.currentChatCost.textContent = formatCurrency(currentCompletionCost);
elements.currentChatInput.textContent = formatNumber(current.chat_input || 0) + ' tokens';
elements.currentChatInputBar.style.width = Math.min((current.chat_input || 0) / 10000, 100) + '%';
elements.currentChatInputCost.textContent = formatCurrency(currentInputCost);
elements.currentTts.textContent = formatNumber(current.tts || 0) + ' characters';
elements.currentTtsBar.style.width = Math.min((current.tts || 0) / 10000, 100) + '%';
elements.currentTtsSm.textContent = formatNumber(current.tts_sm || 0) + ' characters';
elements.currentTtsSmBar.style.width = Math.min((current.tts_sm || 0) / 10000, 100) + '%';
elements.currentImage.textContent = (current.image || 0).toFixed(1) + ' MP';
elements.currentImageBar.style.width = Math.min((current.image || 0) * 10, 100) + '%';
elements.currentImageSm.textContent = (current.image_sm || 0).toFixed(1) + ' MP';
elements.currentImageSmBar.style.width = Math.min((current.image_sm || 0) * 10, 100) + '%';
elements.completionCost.textContent = formatCurrency(currentCompletionCost);
elements.inputCost.textContent = formatCurrency(currentInputCost);
elements.totalChatCost.textContent = formatCurrency(currentTotalCost);
// Previous month
const prevTotalTokens = (previous.chat || 0) + (previous.chat_input || 0);
const prevCompletionCost = calculateCost(previous.chat || 0, PRICING.completion_per_million);
const prevInputCost = calculateCost(previous.chat_input || 0, PRICING.input_per_million);
const prevTotalCost = prevCompletionCost + prevInputCost;
elements.prevChat.textContent = formatNumber(previous.chat || 0) + ' tokens';
elements.prevChatBar.style.width = Math.min((previous.chat || 0) / 10000, 100) + '%';
elements.prevChatCost.textContent = formatCurrency(prevCompletionCost);
elements.prevChatInput.textContent = formatNumber(previous.chat_input || 0) + ' tokens';
elements.prevChatInputBar.style.width = Math.min((previous.chat_input || 0) / 10000, 100) + '%';
elements.prevChatInputCost.textContent = formatCurrency(prevInputCost);
elements.prevTotalCost.textContent = formatCurrency(prevTotalCost);
elements.prevTotalTokens.textContent = formatNumber(prevTotalTokens);
// Update session display if tracking
updateSessionDisplay();
}
// Show error
function showError(message) {
elements.errorMessage.textContent = '❌ ' + message;
elements.errorMessage.classList.add('show');
elements.statusDot.className = 'status-dot error';
elements.statusText.textContent = 'Error: ' + message;
}
// Hide error
function hideError() {
elements.errorMessage.classList.remove('show');
}
// Set loading state
function setLoading(isLoading) {
if (isLoading) {
elements.statusDot.className = 'status-dot loading';
elements.statusText.textContent = 'Fetching usage data...';
hideError();
} else {
elements.statusDot.className = 'status-dot';
}
}
// Fetch usage data
async function fetchUsage() {
if (!state.apiKey) {
showError('Please enter your Vultr API Key first');
return;
}
setLoading(true);
try {
const response = await fetch('https://api.vultrinference.com/v1/usage', {
method: 'GET',
headers: {
'Authorization': `Bearer ${state.apiKey}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
if (response.status === 401) {
throw new Error('Invalid API Key');
} else if (response.status === 403) {
throw new Error('Forbidden - Check API permissions');
} else {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
}
const data = await response.json();
state.currentData = data; // Store for session tracking
updateUI(data);
elements.statusText.textContent = '✅ Data updated successfully';
elements.lastUpdated.textContent = 'Last updated: ' + new Date().toLocaleTimeString();
hideError();
} catch (error) {
showError(error.message || 'Failed to fetch usage data');
} finally {
setLoading(false);
}
}
// Start countdown
function startCountdown() {
if (!state.autoRefresh) return;
state.countdownValue = state.refreshInterval;
updateCountdownDisplay();
state.intervalId = setInterval(() => {
state.countdownValue--;
updateCountdownDisplay();
if (state.countdownValue <= 0) {
fetchUsage();
state.countdownValue = state.refreshInterval;
}
}, 1000);
}
// Stop countdown
function stopCountdown() {
if (state.intervalId) {
clearInterval(state.intervalId);
state.intervalId = null;
}
elements.countdown.textContent = '';
}
// Update countdown display
function updateCountdownDisplay() {
elements.countdown.textContent = `Refreshing in ${state.countdownValue}s...`;
}
// Toggle auto-refresh
function toggleAutoRefresh() {
state.autoRefresh = !state.autoRefresh;
elements.toggleSwitch.classList.toggle('active', state.autoRefresh);
if (state.autoRefresh) {
startCountdown();
} else {
stopCountdown();
}
}
// Format duration
function formatDuration(ms) {
const seconds = Math.floor(ms / 1000);
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
// Update session display
function updateSessionDisplay() {
if (!state.sessionTracking) {
return;
}
if (!state.sessionStartData || !state.currentData) {
console.log('Session tracking: waiting for data...');
return;
}
// Handle wrapped response structure
const currentUsage = state.currentData.usage || state.currentData;
const startUsage = state.sessionStartData.usage || state.sessionStartData;
const current = currentUsage.current_month || {};
const start = startUsage.current_month || {};
// Calculate session usage
const sessionCompletionTokens = Math.max(0, (current.chat || 0) - (start.chat || 0));
const sessionInputTokens = Math.max(0, (current.chat_input || 0) - (start.chat_input || 0));
const sessionTotalTokens = sessionCompletionTokens + sessionInputTokens;
const sessionCompletionCost = calculateCost(sessionCompletionTokens, PRICING.completion_per_million);
const sessionInputCost = calculateCost(sessionInputTokens, PRICING.input_per_million);
const sessionTotalCost = sessionCompletionCost + sessionInputCost;
console.log('Session update:', {
sessionCompletionTokens,
sessionInputTokens,
sessionTotalTokens,
sessionTotalCost
});
// Update all session tracking fields
elements.sessionCompletionTokens.textContent = formatNumber(sessionCompletionTokens);
elements.sessionInputTokens.textContent = formatNumber(sessionInputTokens);
elements.sessionTokens.textContent = formatNumber(sessionTotalTokens);
elements.sessionCompletionCost.textContent = '$' + sessionCompletionCost.toFixed(2) + ' cost';
elements.sessionInputCost.textContent = '$' + sessionInputCost.toFixed(2) + ' cost';
elements.sessionCost.textContent = formatCurrency(sessionTotalCost);
}
// Update session duration
function updateSessionDuration() {
if (!state.sessionTracking || !state.sessionStart) {
return;
}
const elapsed = Date.now() - state.sessionStart;
elements.sessionDuration.textContent = formatDuration(elapsed);
}
// Start session tracking
function startSessionTracking() {
if (state.sessionTracking) {
// Stop tracking
state.sessionTracking = false;
state.sessionStart = null;
state.sessionStartData = null;
if (state.sessionIntervalId) {
clearInterval(state.sessionIntervalId);
state.sessionIntervalId = null;
}
elements.startTrackingBtn.innerHTML = '▶️ Start Tracking';
elements.startTrackingBtn.classList.remove('tracking');
// Reset all session tracking fields
elements.sessionCompletionTokens.textContent = '0';
elements.sessionInputTokens.textContent = '0';
elements.sessionTokens.textContent = '0';
elements.sessionCompletionCost.textContent = '$0.00 cost';
elements.sessionInputCost.textContent = '$0.00 cost';
elements.sessionCost.textContent = '$0.00';
elements.sessionDuration.textContent = '00:00:00';
} else {
// Start tracking
console.log('Starting session tracking');
console.log('Current data available:', !!state.currentData);
if (!state.currentData) {
showError('Please fetch usage data first - click the Refresh button');
return;
}
state.sessionTracking = true;
state.sessionStart = Date.now();
state.sessionStartData = JSON.parse(JSON.stringify(state.currentData));
console.log('Session start data:', state.sessionStartData);
elements.startTrackingBtn.innerHTML = '⏹️ Stop Tracking';
elements.startTrackingBtn.classList.add('tracking');
// Initial update
updateSessionDisplay();
updateSessionDuration();
state.sessionIntervalId = setInterval(() => {
updateSessionDuration();
updateSessionDisplay();
}, 1000);
}
}
// Event Listeners
elements.apiKeyInput.value = state.apiKey;
elements.saveKeyBtn.addEventListener('click', () => {
state.apiKey = elements.apiKeyInput.value.trim();
localStorage.setItem('vultr_api_key', state.apiKey);
if (state.apiKey) {
fetchUsage();
}
});
elements.togglePassword.addEventListener('click', () => {
const type = elements.apiKeyInput.type === 'password' ? 'text' : 'password';
elements.apiKeyInput.type = type;
elements.togglePassword.textContent = type === 'password' ? '👁️' : '🙈';
});
elements.refreshBtn.addEventListener('click', fetchUsage);
elements.intervalSelect.addEventListener('change', (e) => {
state.refreshInterval = parseInt(e.target.value);
if (state.autoRefresh) {
stopCountdown();
startCountdown();
}
});
elements.autoRefreshToggle.addEventListener('click', toggleAutoRefresh);
elements.startTrackingBtn.addEventListener('click', startSessionTracking);
elements.themeToggle.addEventListener('click', () => {
document.body.classList.toggle('dark-mode');
const isDark = document.body.classList.contains('dark-mode');
elements.themeToggle.textContent = isDark ? '☀️' : '🌙';
localStorage.setItem('darkMode', isDark);
});
// Load dark mode preference
if (localStorage.getItem('darkMode') === 'true') {
document.body.classList.add('dark-mode');
elements.themeToggle.textContent = '☀️';
}
// Auto-fetch if API key exists
if (state.apiKey) {
fetchUsage();
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment