diff --git a/internal/web/static/app.css b/internal/web/static/app.css
index 67d0cf8..b856888 100644
--- a/internal/web/static/app.css
+++ b/internal/web/static/app.css
@@ -202,7 +202,8 @@ body {
.footnote { display: flex; gap: 20px; padding: 10px 18px; font-size: 20.7px; color: var(--muted-fg); border-top: 1px solid var(--border); flex-wrap: wrap; }
/* history */
-.page { max-width: 1200px; margin: 0 auto; padding: 20px; display: flex; flex-direction: column; gap: 16px; }
+.page { max-width: 1200px; margin: 0 auto; padding: 20px 20px 28px; display: flex; flex-direction: column; gap: 16px; min-height: calc(100vh - 76px); }
+.pager { position: sticky; bottom: 0; margin-top: auto; z-index: 5; }
.head-row { display: flex; align-items: baseline; gap: 14px; }
.head-row h1 { font-size: 34.5px; font-weight: 600; }
.search {
diff --git a/internal/web/static/table.js b/internal/web/static/table.js
index 7824080..2eb77ba 100644
--- a/internal/web/static/table.js
+++ b/internal/web/static/table.js
@@ -90,10 +90,31 @@ const PaletteTable = (() => {
if (opts.hasPager && pager && showing) {
const pages = Math.max(1, Math.ceil(state.total / opts.perPage));
if (filtered || state.sortKey) {
- showing.textContent = state.sortKey
- ? 'Sorted by ' + state.sortKey + ' (' + (state.sortDir === 1 ? 'ascending' : 'descending') + ') · ' + items.length.toLocaleString() + ' of ' + state.total.toLocaleString()
- : 'Showing ' + items.length.toLocaleString() + ' matches for "' + state.filter + '"';
- pager.innerHTML = '';
+ // Pagination operates over the FILTERED set (#133): never hide the bar.
+ const filtPages = Math.max(1, Math.ceil(items.length / opts.perPage));
+ if (state.page > filtPages) state.page = filtPages;
+ const fOff = (state.page - 1) * opts.perPage;
+ const view = items.slice(fOff, fOff + opts.perPage);
+ rows.innerHTML = view.length ? view.map(opts.rowHtml).join('') : '';
+ if (!view.length) {
+ empty.style.display = 'block';
+ empty.textContent = filtered ? opts.emptyFiltered : opts.emptyAll;
+ }
+ showing.textContent = `Showing ${view.length === 0 ? 0 : fOff+1}–${fOff+view.length} of ${items.length.toLocaleString()} ${filtered ? 'matches' : 'sorted'} · page ${state.page} of ${filtPages}`;
+ const btns = [];
+ const add = (label, target, o={}) => btns.push(``);
+ add('‹', state.page-1, {dis: state.page===1});
+ const win = new Set([1, 2, state.page-1, state.page, state.page+1, filtPages]);
+ let last = 0;
+ for (let i = 1; i <= filtPages; i++) {
+ if (win.has(i)) {
+ if (last && i - last > 1) btns.push('…');
+ add(String(i), i, {on: i===state.page});
+ last = i;
+ }
+ }
+ add('›', state.page+1, {dis: state.page===filtPages});
+ pager.innerHTML = btns.join('');
} else {
showing.textContent = state.total === 0 ? 'Nothing here yet' :
`Showing ${off+1}–${Math.min(off+opts.perPage, state.total)} of ${state.total.toLocaleString()} · page ${state.page} of ${pages}`;
@@ -126,6 +147,7 @@ const PaletteTable = (() => {
if (!th) return;
const k = th.dataset.sort;
if (state.sortKey === k) { state.sortDir = -state.sortDir; } else { state.sortKey = k; state.sortDir = 1; }
+ state.page = 1;
renderSortIndicators();
load();
});