From e94cf8f503193fdfdae3f342cbffe2f69daa7e6f Mon Sep 17 00:00:00 2001 From: DJP Date: Tue, 17 Mar 2026 23:11:02 -0400 Subject: [PATCH] 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) --- public/css/style.css | 39 +++++++++++++++++++++++++++++++++++++++ public/index.html | 38 +++++++++++++++++++++++++++++++++++++- public/js/app.js | 16 ++++++++++++++++ routes/api.js | 10 ++++++++++ services/balance.js | 26 ++++++++++++++++++++++++++ 5 files changed, 128 insertions(+), 1 deletion(-) diff --git a/public/css/style.css b/public/css/style.css index 2dd81c4..36a4883 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -648,6 +648,45 @@ tr:hover td { line-height: 1.5; } +/* Filter Bar */ +.filter-bar { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} + +.filter-label { + font-size: 13px; + font-weight: 600; + color: var(--text-muted); + margin-right: 4px; +} + +.filter-btn { + background: var(--input-bg); + border: 1px solid var(--card-border); + color: var(--text-muted); + padding: 10px 20px; + border-radius: 10px; + cursor: pointer; + font-family: 'Montserrat', sans-serif; + font-size: 13px; + font-weight: 600; + transition: all 0.2s; +} + +.filter-btn:hover { + border-color: rgba(255, 196, 7, 0.3); + color: var(--text-main); +} + +.filter-btn.active { + background: rgba(255, 196, 7, 0.1); + border-color: var(--accent-primary); + color: var(--accent-primary); +} + /* Responsive */ @media (max-width: 768px) { .sidebar { diff --git a/public/index.html b/public/index.html index 843ab00..24fe058 100644 --- a/public/index.html +++ b/public/index.html @@ -29,6 +29,10 @@ Find User + -
+
@@ -160,6 +164,38 @@
+ + + diff --git a/public/js/app.js b/public/js/app.js index c79665a..ba8fe27 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -80,6 +80,7 @@ const app = { if (view === 'home') this.loadHome(); if (view === 'all') this.loadAll(); + if (view === 'low') this.loadLow(1000000); if (view === 'search') document.getElementById('search-input').focus(); }, @@ -132,6 +133,21 @@ const app = { this.renderPagination(data); }, + async loadLow(threshold) { + document.querySelectorAll('.filter-btn').forEach(btn => { + btn.classList.toggle('active', parseInt(btn.dataset.threshold) === threshold); + }); + const data = await this.api(`/balances/low?threshold=${threshold}`); + if (!data) return; + const tbody = document.getElementById('low-tbody'); + document.getElementById('low-count').textContent = `${data.users.length} User${data.users.length !== 1 ? 's' : ''} under ${(threshold / 1000000)}M`; + if (data.users.length === 0) { + tbody.innerHTML = 'No users below this threshold'; + } else { + tbody.innerHTML = data.users.map(u => this.userRow(u)).join(''); + } + }, + userRow(u) { const credits = u.tokenCredits || 0; const balanceClass = credits > 0 ? 'balance-positive' : 'balance-zero'; diff --git a/routes/api.js b/routes/api.js index 305c233..6454521 100644 --- a/routes/api.js +++ b/routes/api.js @@ -22,6 +22,16 @@ router.get('/balances', async (req, res) => { } }); +router.get('/balances/low', async (req, res) => { + try { + const { threshold = 1000000 } = req.query; + const users = await balanceService.getLowBalances(req.db, parseInt(threshold)); + res.json({ users }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + router.get('/balances/search', async (req, res) => { try { const { q } = req.query; diff --git a/services/balance.js b/services/balance.js index 21bb99a..aaefe78 100644 --- a/services/balance.js +++ b/services/balance.js @@ -82,6 +82,31 @@ async function getAllBalances(db, page, limit) { }; } +async function getLowBalances(db, threshold) { + return db.collection('balances').aggregate([ + { $match: { tokenCredits: { $lt: threshold } } }, + { + $lookup: { + from: 'users', + localField: 'user', + foreignField: '_id', + as: 'userInfo', + }, + }, + { $unwind: { path: '$userInfo', preserveNullAndEmptyArrays: true } }, + { + $project: { + userId: '$user', + tokenCredits: 1, + email: '$userInfo.email', + name: '$userInfo.name', + username: '$userInfo.username', + }, + }, + { $sort: { tokenCredits: 1 } }, + ]).toArray(); +} + async function searchUsers(db, query) { const regex = new RegExp(query, 'i'); @@ -181,6 +206,7 @@ async function setAll(db, amount) { module.exports = { getStats, getAllBalances, + getLowBalances, searchUsers, getUserBalance, setBalance,