diff --git a/static/js/components/pager-directory.js b/static/js/components/pager-directory.js
new file mode 100644
index 0000000..c12ceb4
--- /dev/null
+++ b/static/js/components/pager-directory.js
@@ -0,0 +1,197 @@
+const PagerDirectory = (function () {
+ 'use strict';
+
+ const STORAGE_KEY = 'pagerView';
+
+ // Map
+ const addresses = new Map();
+ let highlighted = null;
+
+ // ---- Helpers ----
+
+ function esc(str) {
+ return String(str)
+ .replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
+ }
+
+ function formatAge(ts) {
+ const s = Math.floor((Date.now() - ts) / 1000);
+ if (s < 10) return 'just now';
+ if (s < 60) return `${s}s ago`;
+ return `${Math.floor(s / 60)}m ago`;
+ }
+
+ // ---- Directory rendering ----
+
+ function renderDirectory() {
+ const entriesEl = document.getElementById('pagerDirEntries');
+ const countEl = document.getElementById('pagerDirCount');
+ if (!entriesEl) return;
+
+ const sorted = [...addresses.entries()].sort((a, b) => b[1].count - a[1].count);
+ const maxCount = sorted.length > 0 ? sorted[0][1].count : 1;
+
+ if (countEl) countEl.textContent = sorted.length;
+
+ sorted.forEach(([addr, data]) => {
+ let el = entriesEl.querySelector(`[data-pdir-addr="${CSS.escape(addr)}"]`);
+ const isActive = addr === highlighted;
+ const pct = Math.round((data.count / maxCount) * 100);
+ const isPocsag = data.protocol !== 'flex';
+ const protoClass = isPocsag ? 'pdir-proto--p' : 'pdir-proto--f';
+ const barClass = isPocsag ? '' : 'pdir-bar--flex';
+ const html = `
+
+ ${isPocsag ? 'P' : 'F'}
+ ${esc(addr)}
+
+ ×${data.count}
+
+
+ ${formatAge(data.lastSeen)}
`;
+
+ if (!el) {
+ el = document.createElement('div');
+ el.className = 'pdir-entry';
+ el.dataset.pdirAddr = addr;
+ el.addEventListener('click', () => toggleHighlight(addr));
+ entriesEl.appendChild(el);
+ }
+ el.classList.toggle('pdir-entry--active', isActive);
+ el.innerHTML = html;
+ });
+
+ // Re-order DOM to match sort
+ sorted.forEach(([addr]) => {
+ const el = entriesEl.querySelector(`[data-pdir-addr="${CSS.escape(addr)}"]`);
+ if (el) entriesEl.appendChild(el);
+ });
+ }
+
+ function flashNewDot(addr) {
+ // Find the dot inside this entry after the current render frame
+ setTimeout(() => {
+ const entriesEl = document.getElementById('pagerDirEntries');
+ const entry = entriesEl?.querySelector(`[data-pdir-addr="${CSS.escape(addr)}"]`);
+ const dot = entry?.querySelector('.pdir-new-dot');
+ if (!dot) return;
+ dot.classList.remove('pdir-new-dot--active');
+ void dot.offsetWidth; // force reflow to restart animation
+ dot.classList.add('pdir-new-dot--active');
+ }, 0);
+ }
+
+ // ---- Highlight ----
+
+ function toggleHighlight(addr) {
+ if (highlighted === addr) clearHighlight();
+ else highlight(addr);
+ }
+
+ function highlight(addr) {
+ highlighted = addr;
+ renderDirectory();
+
+ const feedLabel = document.getElementById('pagerFeedLabel');
+ const clearBtn = document.getElementById('pagerClearHighlight');
+ if (feedLabel) feedLabel.textContent = `${addr} highlighted`;
+ if (clearBtn) clearBtn.style.display = 'inline';
+
+ const output = document.getElementById('output');
+ if (!output) return;
+
+ output.querySelectorAll('.signal-card').forEach(card => {
+ card.classList.toggle('pdir-hl', card.dataset.address === addr);
+ });
+
+ const first = output.querySelector(`.signal-card[data-address="${CSS.escape(addr)}"]`);
+ if (first) first.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
+ }
+
+ function clearHighlight() {
+ highlighted = null;
+ renderDirectory();
+
+ const feedLabel = document.getElementById('pagerFeedLabel');
+ const clearBtn = document.getElementById('pagerClearHighlight');
+ if (feedLabel) feedLabel.textContent = 'All messages';
+ if (clearBtn) clearBtn.style.display = 'none';
+
+ document.getElementById('output')
+ ?.querySelectorAll('.pdir-hl')
+ .forEach(c => c.classList.remove('pdir-hl'));
+ }
+
+ // ---- Public: message hook ----
+
+ function addMessage(msg) {
+ const addr = msg.address;
+ if (!addr) return;
+ const proto = (msg.protocol || '').includes('FLEX') ? 'flex' : 'pocsag';
+ const entry = addresses.get(addr);
+ if (entry) {
+ entry.count++;
+ entry.lastSeen = Date.now();
+ entry.protocol = proto;
+ } else {
+ addresses.set(addr, { count: 1, protocol: proto, lastSeen: Date.now() });
+ }
+ renderDirectory();
+ flashNewDot(addr);
+ // Re-apply highlight class to the newly inserted card (caller inserts it after this hook)
+ if (highlighted === addr) {
+ setTimeout(() => {
+ const output = document.getElementById('output');
+ output?.querySelectorAll(`.signal-card[data-address="${CSS.escape(addr)}"]`)
+ .forEach(c => c.classList.add('pdir-hl'));
+ }, 0);
+ }
+ }
+
+ // ---- Show / hide / reset ----
+
+ function applyViewState(mode) {
+ const dirPanel = document.getElementById('pagerDirectoryView');
+ const feedHeader = document.getElementById('pagerFeedHeader');
+
+ if (mode === 'pager') {
+ const saved = localStorage.getItem(STORAGE_KEY) || 'directory';
+ const isDir = saved === 'directory';
+ if (dirPanel) dirPanel.style.display = isDir ? 'flex' : 'none';
+ if (feedHeader) feedHeader.style.display = isDir ? 'flex' : 'none';
+ _updateToggle(isDir);
+ } else {
+ if (dirPanel) dirPanel.style.display = 'none';
+ if (feedHeader) feedHeader.style.display = 'none';
+ clearHighlight();
+ }
+ }
+
+ function show() {
+ localStorage.setItem(STORAGE_KEY, 'directory');
+ applyViewState('pager');
+ }
+
+ function hide() {
+ localStorage.setItem(STORAGE_KEY, 'feed');
+ applyViewState('pager');
+ }
+
+ function _updateToggle(isDir) {
+ document.getElementById('pagerToggleDir')?.classList.toggle('view-toggle-btn--active', isDir);
+ document.getElementById('pagerToggleFeed')?.classList.toggle('view-toggle-btn--active', !isDir);
+ }
+
+ function reset() {
+ addresses.clear();
+ highlighted = null;
+ const entriesEl = document.getElementById('pagerDirEntries');
+ const countEl = document.getElementById('pagerDirCount');
+ if (entriesEl) entriesEl.innerHTML = '';
+ if (countEl) countEl.textContent = '0';
+ clearHighlight();
+ }
+
+ return { addMessage, highlight, clearHighlight, show, hide, reset, applyViewState };
+})();