From 41f3775ea9c8ae8e372d635e4b51fe4086609552 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Sun, 15 Feb 2026 16:55:58 +0000
Subject: [PATCH] feat: add pagination to dashboard and fix mjs mime type
- Implemented client-side pagination in `dashboard/index.html` (20 items per page).
- Added pagination controls and CSS utilities in `dashboard/styles.css`.
- Fixed `src/dashboard.mjs` to serve `.mjs` files with `text/javascript` MIME type.
- Verified `npx torch-lock init` functionality (no changes needed to landing page).
Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com>
---
dashboard/index.html | 109 ++++++++++++++++++++++++++++++++++++++++---
dashboard/styles.css | 57 ++++++++++++++++++++++
src/dashboard.mjs | 1 +
3 files changed, 161 insertions(+), 6 deletions(-)
diff --git a/dashboard/index.html b/dashboard/index.html
index 894d7dd..9802b24 100644
--- a/dashboard/index.html
+++ b/dashboard/index.html
@@ -97,6 +97,43 @@
+
+
+
totalPages && totalPages > 0) {
+ currentPage = totalPages;
+ }
+ if (currentPage < 1) currentPage = 1;
+
+ if (totalItems === 0) {
lockGrid.replaceChildren(emptyState);
emptyState.textContent =
lockStore.size === 0
? 'Waiting for lock events from relays\u2026'
: 'No locks match the current filters.';
+ paginationControls.classList.add('hidden');
return;
}
- const cards = locks.map((lock) => createLockCard(lock, now));
+ // Slice for pagination
+ const startIdx = (currentPage - 1) * itemsPerPage;
+ const endIdx = Math.min(startIdx + itemsPerPage, totalItems);
+ const pageLocks = allLocks.slice(startIdx, endIdx);
+
+ const cards = pageLocks.map((lock) => createLockCard(lock, now));
lockGrid.replaceChildren(...cards);
+
+ // Update Pagination Controls
+ if (totalItems > itemsPerPage) {
+ paginationControls.classList.remove('hidden');
+
+ // Update text
+ pageStartEl.textContent = startIdx + 1;
+ pageEndEl.textContent = endIdx;
+ totalItemsEl.textContent = totalItems;
+
+ // Update buttons state
+ const isFirst = currentPage === 1;
+ const isLast = currentPage === totalPages;
+
+ prevBtn.disabled = isFirst;
+ nextBtn.disabled = isLast;
+ mobilePrevBtn.disabled = isFirst;
+ mobileNextBtn.disabled = isLast;
+ } else {
+ paginationControls.classList.add('hidden');
+ }
}
// -----------------------------------------------------------------------
@@ -704,8 +790,19 @@
// -----------------------------------------------------------------------
// Event handlers
// -----------------------------------------------------------------------
- if (cadenceFilter) cadenceFilter.addEventListener('change', renderLocks);
- if (statusFilter) statusFilter.addEventListener('change', renderLocks);
+ function setPage(p) {
+ currentPage = p;
+ renderLocks();
+ }
+
+ if (cadenceFilter) cadenceFilter.addEventListener('change', () => { currentPage = 1; renderLocks(); });
+ if (statusFilter) statusFilter.addEventListener('change', () => { currentPage = 1; renderLocks(); });
+
+ if (prevBtn) prevBtn.addEventListener('click', () => setPage(currentPage - 1));
+ if (nextBtn) nextBtn.addEventListener('click', () => setPage(currentPage + 1));
+ if (mobilePrevBtn) mobilePrevBtn.addEventListener('click', () => setPage(currentPage - 1));
+ if (mobileNextBtn) mobileNextBtn.addEventListener('click', () => setPage(currentPage + 1));
+
if (refreshBtn) refreshBtn.addEventListener('click', () => {
// Close existing connections and reconnect
for (const ws of sockets) {
diff --git a/dashboard/styles.css b/dashboard/styles.css
index ab28511..23a310f 100644
--- a/dashboard/styles.css
+++ b/dashboard/styles.css
@@ -178,3 +178,60 @@ a { color: inherit; }
@media (min-width: 1024px) {
.lg\:px-8 { padding-left: 2rem; padding-right: 2rem; }
}
+
+/* Pagination Utilities */
+.hidden { display: none; }
+.block { display: block; }
+.relative { position: relative; }
+.justify-center { justify-content: center; }
+
+.pagination-nav {
+ display: inline-flex;
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+ border-radius: 0.375rem;
+}
+
+.pagination-btn {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ padding: 0.5rem 0.75rem;
+ color: var(--muted);
+ background-color: transparent;
+ border: 1px solid var(--border);
+ font-size: 0.875rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: background-color 0.15s ease;
+}
+
+.pagination-btn:hover:not(:disabled) {
+ background-color: var(--surface-alt);
+}
+
+.pagination-btn:first-child {
+ border-top-left-radius: 0.375rem;
+ border-bottom-left-radius: 0.375rem;
+}
+
+.pagination-btn:last-child {
+ border-top-right-radius: 0.375rem;
+ border-bottom-right-radius: 0.375rem;
+}
+
+.pagination-btn + .pagination-btn {
+ margin-left: -1px;
+}
+
+.pagination-btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+@media (min-width: 640px) {
+ .sm\:hidden { display: none; }
+ .sm\:flex { display: flex; }
+ .sm\:flex-1 { flex: 1 1 0%; }
+ .sm\:items-center { align-items: center; }
+ .sm\:justify-between { justify-content: space-between; }
+}
diff --git a/src/dashboard.mjs b/src/dashboard.mjs
index 247c16b..3474029 100644
--- a/src/dashboard.mjs
+++ b/src/dashboard.mjs
@@ -68,6 +68,7 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT) {
switch (extname) {
case '.html': contentType = 'text/html'; break;
case '.js': contentType = 'text/javascript'; break;
+ case '.mjs': contentType = 'text/javascript'; break;
case '.css': contentType = 'text/css'; break;
case '.json': contentType = 'application/json'; break;
case '.png': contentType = 'image/png'; break;