librechat-balances/public/index.html
DJP e94cf8f503 Add low balance users view with threshold filters
Shows users below 1M/2M/3M/4M/5M tokens with filter buttons.
Sorted lowest first so admins can quickly find and top up users
running low.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 23:11:02 -04:00

211 lines
10 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<base href="./">
<title>Balance Manager - LibreChat</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<aside class="sidebar">
<div class="sidebar-logo">
<div class="icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M19 7V4a1 1 0 0 0-1-1H5a2 2 0 0 0 0 4h15a1 1 0 0 1 1 1v4h-3a2 2 0 0 0 0 4h3a1 1 0 0 0 1-1v-2.5"/><path d="M3 5v14a2 2 0 0 0 2 2h15a1 1 0 0 0 1-1v-4"/></svg>
</div>
<h1>Balance Manager</h1>
</div>
<nav class="nav-items">
<div class="nav-item active" data-view="home">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
Home
</div>
<div class="nav-item" data-view="all">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
All Balances
</div>
<div class="nav-item" data-view="search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
Find User
</div>
<div class="nav-item" data-view="low">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" x2="12" y1="9" y2="13"/><line x1="12" x2="12.01" y1="17" y2="17"/></svg>
Low Balance
</div>
<div class="nav-item" data-view="bulk">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="5" rx="2"/><line x1="2" x2="22" y1="10" y2="10"/></svg>
Bulk Operations
</div>
</nav>
<div class="sidebar-footer">
LibreChat Balance Manager v1.0
</div>
</aside>
<main class="main-content">
<!-- Home View -->
<div id="view-home" class="view">
<div class="page-header">
<h2>Balance Manager</h2>
<p>Manage LibreChat user token balances</p>
</div>
<div id="home-stats" class="stats-grid"></div>
<div class="home-grid">
<div class="home-card" data-nav="all">
<div class="card-icon gold">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
</div>
<h3>View All Balances</h3>
<p>Browse all user balances sorted by token credits. Edit or top up individual users.</p>
</div>
<div class="home-card" data-nav="search">
<div class="card-icon green">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
</div>
<h3>Find a User</h3>
<p>Search by name or email. Type "dave" to find daveporter@... — quick lookup without loading all users.</p>
</div>
<div class="home-card" data-nav="bulk">
<div class="card-icon purple">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="5" rx="2"/><line x1="2" x2="22" y1="10" y2="10"/></svg>
</div>
<h3>Bulk Operations</h3>
<p>Add tokens to all users at once or reset everyone to a specific balance amount.</p>
</div>
<div class="home-card" data-nav="low">
<div class="card-icon red">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" x2="12" y1="9" y2="13"/><line x1="12" x2="12.01" y1="17" y2="17"/></svg>
</div>
<h3>Low Balance Users</h3>
<p>Quickly see who is running low or has hit zero. Keep your team productive.</p>
</div>
</div>
</div>
<!-- All Balances View -->
<div id="view-all" class="view" style="display:none">
<div class="page-header">
<h2>All Balances</h2>
<p>All user balances sorted by token credits</p>
</div>
<div class="table-container">
<div class="table-header">
<h3 id="all-count">Users</h3>
<button class="btn btn-sm btn-secondary" onclick="app.exportCSV()">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="14" height="14"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/></svg>
Export CSV
</button>
</div>
<table>
<thead>
<tr>
<th>Email</th>
<th>Name</th>
<th>Balance</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="all-tbody"></tbody>
</table>
<div class="pagination" id="pagination"></div>
</div>
</div>
<!-- Search View -->
<div id="view-search" class="view" style="display:none">
<div class="page-header">
<h2>Find User</h2>
<p>Search by name or email — minimum 2 characters</p>
</div>
<div class="search-section">
<div class="search-bar">
<input type="text" class="search-input" id="search-input" placeholder="Type a name or email... e.g. dave, simeon, john@..." autocomplete="off">
</div>
</div>
<div class="table-container" id="search-results-container" style="display:none">
<div class="table-header">
<h3 id="search-count">Results</h3>
</div>
<table>
<thead>
<tr>
<th>Email</th>
<th>Name</th>
<th>Balance</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="search-tbody"></tbody>
</table>
</div>
</div>
<!-- Bulk View -->
<div id="view-bulk" class="view" style="display:none">
<div class="page-header">
<h2>Bulk Operations</h2>
<p>Apply balance changes to all users at once</p>
</div>
<div class="home-grid" style="max-width:700px">
<div class="home-card" onclick="app.showBulkModal('add')">
<div class="card-icon green">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
</div>
<h3>Add to All Users</h3>
<p>Increment every user's balance by a specified amount. Does not overwrite existing balances.</p>
</div>
<div class="home-card" onclick="app.showBulkModal('set')">
<div class="card-icon red">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/></svg>
</div>
<h3>Set All Users</h3>
<p>Override every user's balance to an exact amount. Use with caution — this replaces existing values.</p>
</div>
</div>
</div>
<!-- Low Balance View -->
<div id="view-low" class="view" style="display:none">
<div class="page-header">
<h2>Low Balance Users</h2>
<p>Users below a token threshold — sorted lowest first</p>
</div>
<div class="filter-bar">
<span class="filter-label">Show users under:</span>
<button class="filter-btn active" data-threshold="1000000" onclick="app.loadLow(1000000)">1M</button>
<button class="filter-btn" data-threshold="2000000" onclick="app.loadLow(2000000)">2M</button>
<button class="filter-btn" data-threshold="3000000" onclick="app.loadLow(3000000)">3M</button>
<button class="filter-btn" data-threshold="4000000" onclick="app.loadLow(4000000)">4M</button>
<button class="filter-btn" data-threshold="5000000" onclick="app.loadLow(5000000)">5M</button>
</div>
<div class="table-container" style="margin-top:20px">
<div class="table-header">
<h3 id="low-count">Users</h3>
</div>
<table>
<thead>
<tr>
<th>Email</th>
<th>Name</th>
<th>Balance</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="low-tbody"></tbody>
</table>
</div>
</div>
</main>
<!-- Modal -->
<div class="modal-overlay" id="modal-overlay">
<div class="modal" id="modal"></div>
</div>
<!-- Toast Container -->
<div class="toast-container" id="toast-container"></div>
<script src="js/app.js"></script>
</body>
</html>