mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-27 07:51:52 +00:00
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 <[email protected]>
This commit is contained in:
1 parent
24ee371b6a
commit
41f3775ea9
3 files changed
+161
-6
No files matched your search
+103
-6
@@ -97,6 +97,43 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Pagination Controls -->
|
||||
<div id="paginationControls" class="flex items-center justify-between border-t border-border pt-4 mb-8 hidden">
|
||||
<div class="flex flex-1 justify-between sm:hidden">
|
||||
<button id="mobilePrevBtn" class="pagination-btn rounded-md">Previous</button>
|
||||
<button id="mobileNextBtn" class="pagination-btn rounded-md ml-auto">Next</button>
|
||||
</div>
|
||||
<div class="sm:flex sm:flex-1 sm:items-center sm:justify-between hidden">
|
||||
<div>
|
||||
<p class="text-sm text-muted">
|
||||
Showing
|
||||
<span class="font-medium text-text" id="pageStart">1</span>
|
||||
to
|
||||
<span class="font-medium text-text" id="pageEnd">10</span>
|
||||
of
|
||||
<span class="font-medium text-text" id="totalItems">97</span>
|
||||
results
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<nav class="isolate inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
|
||||
<button id="prevBtn" class="pagination-btn rounded-l-md px-2 py-2">
|
||||
<span class="sr-only">Previous</span>
|
||||
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
<button id="nextBtn" class="pagination-btn rounded-r-md px-2 py-2">
|
||||
<span class="sr-only">Next</span>
|
||||
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Raw event log -->
|
||||
<details class="rounded-lg border border-border">
|
||||
<summary
|
||||
@@ -277,6 +314,20 @@
|
||||
const statusFilter = document.getElementById('statusFilter');
|
||||
const refreshBtn = document.getElementById('refreshBtn');
|
||||
|
||||
// Pagination DOM refs
|
||||
const paginationControls = document.getElementById('paginationControls');
|
||||
const mobilePrevBtn = document.getElementById('mobilePrevBtn');
|
||||
const mobileNextBtn = document.getElementById('mobileNextBtn');
|
||||
const prevBtn = document.getElementById('prevBtn');
|
||||
const nextBtn = document.getElementById('nextBtn');
|
||||
const pageStartEl = document.getElementById('pageStart');
|
||||
const pageEndEl = document.getElementById('pageEnd');
|
||||
const totalItemsEl = document.getElementById('totalItems');
|
||||
|
||||
// Pagination State
|
||||
let currentPage = 1;
|
||||
const itemsPerPage = 20;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Cleanup check
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -543,24 +594,59 @@
|
||||
function renderLocks() {
|
||||
if (!isAlive()) return;
|
||||
|
||||
const locks = getFilteredLocks();
|
||||
const allLocks = getFilteredLocks();
|
||||
const now = nowUnix();
|
||||
|
||||
eventCount.textContent = `${lockStore.size} events`;
|
||||
renderSummary(allLocks);
|
||||
|
||||
renderSummary(locks);
|
||||
const totalItems = allLocks.length;
|
||||
const totalPages = Math.ceil(totalItems / itemsPerPage);
|
||||
|
||||
if (locks.length === 0) {
|
||||
// Adjust currentPage if out of bounds
|
||||
if (currentPage > 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) {
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user