Skip to content

Commit 8c284a0

Browse files
Merge pull request #22 from JavaScriptSolidServer/issue-21-relay-filter-tabs
Relays page: filter tabs (All/Online/Writable/Paid/Auth) with per-category counts
2 parents 4a25649 + 9294b47 commit 8c284a0

3 files changed

Lines changed: 62 additions & 25 deletions

File tree

public/index.html

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,12 @@
117117
padding:.4rem .85rem;border-radius:999px;cursor:pointer;transition:border-color .15s,color .15s,background .15s}
118118
.toggle:hover{border-color:var(--accent);color:var(--accent)}
119119
.toggle.on{background:var(--accent-soft);border-color:#dcd8f5;color:var(--accent)}
120+
.tabs{display:flex;flex-wrap:wrap;gap:.4rem;margin:.2rem 0 1rem}
121+
.tab{font-size:.84rem;font-weight:600;border:1px solid var(--line);background:#fff;color:var(--muted);padding:.42rem .85rem;border-radius:999px;cursor:pointer;display:inline-flex;align-items:center;gap:.45rem;transition:border-color .15s,color .15s,background .15s}
122+
.tab:hover{border-color:var(--accent);color:var(--accent)}
123+
.tab.on{background:var(--accent-soft);border-color:#dcd8f5;color:var(--accent)}
124+
.tab .tabn{font-family:var(--mono);font-size:.74rem;font-weight:500;color:var(--faint)}
125+
.tab.on .tabn{color:var(--accent)}
120126
.caveat{font-size:.8rem;color:var(--faint);background:#fff;border:1px solid var(--line);border-radius:10px;padding:.55rem .8rem;margin-bottom:1rem}
121127
.rtable{width:100%;border-collapse:collapse;background:var(--surface);border:1px solid var(--line);border-radius:14px;overflow:hidden;font-size:.86rem}
122128
.rtable thead th{text-align:left;font-size:.7rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--faint);
@@ -349,7 +355,14 @@ <h2>${esc(c.name || short(pk))}</h2>
349355
}
350356

351357
// ---- relay directory (firehose health data) ----
352-
const relaysState = { online: true, sortKey: 'lastChecked', dir: -1, rows: [], meta: {} };
358+
const relaysState = { filter: 'online', sortKey: 'lastChecked', dir: -1, rows: [], meta: {} };
359+
const RTABS = [
360+
{ key: 'all', label: 'All' },
361+
{ key: 'online', label: 'Online' },
362+
{ key: 'writable', label: 'Writable' },
363+
{ key: 'paid', label: 'Paid' },
364+
{ key: 'auth', label: 'Auth' },
365+
];
353366
const RCOLS = [
354367
{ key: 'relay', label: 'Relay', num: false },
355368
{ key: 'online', label: 'Status', num: false },
@@ -394,20 +407,22 @@ <h2>${esc(c.name || short(pk))}</h2>
394407
}
395408

396409
function renderRelays() {
397-
const { rows, meta, sortKey, dir, online } = relaysState;
410+
const { rows, meta, sortKey, dir, filter } = relaysState;
411+
const counts = meta.counts || {};
398412
const sorted = [...rows].sort((a, b) => cmpRelay(a, b, sortKey, dir));
399413
const head = RCOLS.map((c) => `<th class="${c.num ? 'num' : ''}" data-key="${c.key}">${c.label}${sortKey === c.key ? ` <span class="arrow">${dir < 0 ? '▼' : '▲'}</span>` : ''}</th>`).join('');
414+
const tabs = RTABS.map((t) => `<button class="tab ${filter === t.key ? 'on' : ''}" data-filter="${t.key}">${t.label} <span class="tabn">${fmt(counts[t.key] || 0)}</span></button>`).join('');
415+
const label = filter === 'all' ? 'relays' : `${filter} relays`;
400416
view.innerHTML = `<div class="detail">
401417
<div class="back" id="back">← all identities</div>
402-
<div class="relays-head"><h2>Relay directory</h2>
403-
<span class="sub"><strong>${fmt(meta.online)}</strong> online of <strong>${fmt(meta.total)}</strong> known relays</span></div>
404-
<div class="caveat">Health snapshot · relays last checked <strong>${esc(meta.lastChecked ? ageStr(meta.lastChecked) + ' ago' : 'unknown')}</strong>. Live monitoring is paused, so these figures reflect that snapshot.</div>
405-
<div class="relays-controls"><button class="toggle ${online ? 'on' : ''}" id="r-online">Online only</button></div>
418+
<div class="relays-head"><h2>Relay directory</h2></div>
419+
<div class="tabs">${tabs}</div>
420+
<div class="caveat">Health checked live · relays last checked <strong>${esc(meta.lastChecked ? ageStr(meta.lastChecked) + ' ago' : 'unknown')}</strong>.</div>
406421
<div style="overflow-x:auto"><table class="rtable"><thead><tr>${head}</tr></thead><tbody>${sorted.map(relayRow).join('')}</tbody></table></div>
407-
<div class="caveat" style="margin-top:1rem">Showing ${sorted.length} relays. Click a column to sort.</div>
422+
<div class="caveat" style="margin-top:1rem">Showing ${sorted.length} ${label}. Click a column to sort.</div>
408423
</div>`;
409424
$('#back').onclick = () => navigate('/');
410-
$('#r-online').onclick = () => { relaysState.online = !relaysState.online; relaysView(); };
425+
view.querySelectorAll('.tabs .tab').forEach((b) => { b.onclick = () => { relaysState.filter = b.dataset.filter; relaysView(); }; });
411426
view.querySelectorAll('.rtable thead th').forEach((th) => { th.onclick = () => {
412427
const k = th.dataset.key;
413428
if (relaysState.sortKey === k) relaysState.dir = -relaysState.dir;
@@ -420,7 +435,7 @@ <h2>${esc(c.name || short(pk))}</h2>
420435
if ($('#q')) $('#q').value = '';
421436
$('#stat').innerHTML = '';
422437
view.innerHTML = '<div class="loading">loading relay directory…</div>';
423-
const data = await fetch(`/api/relays-directory?limit=2000${relaysState.online ? '&online=1' : ''}`).then((r) => r.json()).catch(() => null);
438+
const data = await fetch(`/api/relays-directory?filter=${encodeURIComponent(relaysState.filter)}&limit=2000`).then((r) => r.json()).catch(() => null);
424439
if (!data || !Array.isArray(data.relays)) { view.innerHTML = '<div class="loading">Could not load the relay directory.</div>'; return; }
425440
relaysState.rows = data.relays; relaysState.meta = data;
426441
renderRelays();

src/db.js

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -116,24 +116,46 @@ const RELAY_FIELDS = {
116116
requiresAuth: 1, requiresPayment: 1, lastChecked: 1, checksOnline: 1, checksTotal: 1,
117117
};
118118

119+
// Tab filters → the Mongo match for each. `all` is the whole directory.
120+
const RELAY_FILTERS = {
121+
all: {},
122+
online: { online: true },
123+
writable: { acceptsEvents: true },
124+
paid: { requiresPayment: true },
125+
auth: { requiresAuth: true },
126+
};
127+
119128
/**
120-
* Read the relay-health directory. Returns a summary (for the freshness caveat)
121-
* plus the matching rows. `online` filters to live relays; `sort` is one of
122-
* 'quality' (uptime desc, latency asc), 'latency', or 'recent' (last checked).
129+
* Read the relay-health directory in one round-trip ($facet): per-tab `counts`
130+
* (all/online/writable/paid/auth), the freshest `lastChecked` (freshness
131+
* caveat), and the filtered+sorted page. `filter` is a tab key (default
132+
* 'online'); `sort` is 'recent' (default), 'quality', or 'latency'.
123133
*/
124-
export async function relaysDirectory({ online = false, sort = 'quality', limit = 300 } = {}) {
134+
export async function relaysDirectory({ filter = 'online', sort = 'recent', limit = 1000 } = {}) {
125135
const col = (await connect()).collection(RELAY_DIRECTORY);
126-
const filter = online ? { online: true } : {};
127-
const sortSpec = sort === 'recent' ? { lastChecked: -1 }
136+
const match = RELAY_FILTERS[filter] || RELAY_FILTERS.online;
137+
const sortSpec = sort === 'quality' ? { uptime: -1, responseTime: 1 }
128138
: sort === 'latency' ? { responseTime: 1 }
129-
: { uptime: -1, responseTime: 1 }; // 'quality'
130-
const [total, onlineCount, newest, relays] = await Promise.all([
131-
col.estimatedDocumentCount(),
132-
col.countDocuments({ online: true }),
133-
col.find({}, { projection: { _id: 0, lastChecked: 1 } }).sort({ lastChecked: -1 }).limit(1).toArray(),
134-
col.find(filter, { projection: RELAY_FIELDS }).sort(sortSpec).limit(Math.min(Number(limit) || 300, 2000)).toArray(),
135-
]);
136-
return { total, online: onlineCount, lastChecked: newest[0]?.lastChecked || null, relays };
139+
: { lastChecked: -1 }; // 'recent'
140+
const lim = Math.min(Number(limit) || 1000, 2000);
141+
const count = (m) => (m && Object.keys(m).length ? [{ $match: m }, { $count: 'n' }] : [{ $count: 'n' }]);
142+
const [res] = await col.aggregate([{
143+
$facet: {
144+
all: count(RELAY_FILTERS.all),
145+
online: count(RELAY_FILTERS.online),
146+
writable: count(RELAY_FILTERS.writable),
147+
paid: count(RELAY_FILTERS.paid),
148+
auth: count(RELAY_FILTERS.auth),
149+
newest: [{ $sort: { lastChecked: -1 } }, { $limit: 1 }, { $project: { _id: 0, lastChecked: 1 } }],
150+
page: [{ $match: match }, { $sort: sortSpec }, { $limit: lim }, { $project: RELAY_FIELDS }],
151+
},
152+
}]).toArray();
153+
const n = (k) => res?.[k]?.[0]?.n || 0;
154+
return {
155+
counts: { all: n('all'), online: n('online'), writable: n('writable'), paid: n('paid'), auth: n('auth') },
156+
lastChecked: res?.newest?.[0]?.lastChecked || null,
157+
relays: res?.page || [],
158+
};
137159
}
138160

139161
// ---- graph metrics ---------------------------------------------------------

src/server.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,11 @@ export async function startServer(port = process.env.PORT || 3000) {
127127
try { res.json(await getRelays(req.params.pubkey)); } catch (e) { next(e); }
128128
});
129129

130-
// relay-health directory (firehose data): { total, online, lastChecked, relays }
130+
// relay-health directory (firehose data): { counts, lastChecked, relays }
131131
app.get('/api/relays-directory', async (req, res, next) => {
132132
try {
133133
res.json(await relaysDirectory({
134-
online: req.query.online === '1' || req.query.online === 'true',
134+
filter: req.query.filter,
135135
sort: req.query.sort,
136136
limit: req.query.limit,
137137
}));

0 commit comments

Comments
 (0)