From ca7a4b6722c2c57044024074a96ca28172cb328c Mon Sep 17 00:00:00 2001 From: Laan Tungir Date: Mon, 3 Aug 2026 19:37:09 -0400 Subject: [PATCH] v2.1.36 - Unified relay table, caching/backfill separation, inbox defaults, profile gating, and UI fixes - Added caching_relays unified table with live_enabled/backfill_enabled columns - Separated --reset-backfill from --start-caching as independent flags - Removed redundant caching_enabled master setting; daemon derives from live/backfill - Set caching_inbox_enabled=true by default; removed Inbox toggle from Backfill page - Set caching_live_strategy=cache_all by default - Fixed outbox relay discovery to store ALL discovered relays, not just covering set - Added store_kind_0_information config (default: true) to gate profile sync trigger - Regenerated pg_schema.h from pg_schema.sql to include caching_relays table - Fixed process toggle button styling to match monochrome aesthetic - Fixed radio button styling to match black/white/red theme - Simplified Backfill page: removed Service Status and Inbox Status sections - Backfill status now respects config setting, not just daemon state - Admin config API now bumps caching_config_generation for caching-related changes - make_and_restart_relay.sh now resets PostgreSQL schema on fresh restart --- admin/api/caching.php | 21 +- admin/api/config.php | 29 +- admin/api/live_subscription.php | 150 + admin/assets/app.js | 446 +- admin/assets/index.css | 199 +- admin/cache/chart_day.txt | 22 +- admin/cache/stats_kinds.json | 2 +- admin/cache/stats_pubkeys.json | 2 +- admin/index.php | 114 +- admin/php_server.log | 31014 ++++++++++++++++ .../cache_all_test_report_20260802_093724.txt | 253 - .../cache_all_test_report_20260802_094007.txt | 207 - .../cache_all_test_report_20260802_095103.txt | 206 - .../cache_all_test_report_20260802_100751.txt | 51 - .../cache_all_test_report_20260802_101256.txt | 39 - .../cache_all_test_report_20260802_101828.txt | 35 - .../cache_all_test_report_20260802_102817.txt | 197 - .../cache_all_test_report_20260802_103042.txt | 257 - .../cache_all_test_report_20260802_112138.txt | 167 - .../cache_all_test_report_20260802_120239.txt | 423 - .../cache_all_test_report_20260802_162333.txt | 646 - .../cache_all_test_report_20260802_163255.txt | 243 - .../cache_all_test_report_20260802_164112.txt | 217 - .../cache_all_test_stats_20260802_093724.json | 1166 - .../cache_all_test_stats_20260802_094007.json | 921 - .../cache_all_test_stats_20260802_095103.json | 916 - .../cache_all_test_stats_20260802_100751.json | 281 - .../cache_all_test_stats_20260802_101256.json | 161 - .../cache_all_test_stats_20260802_101828.json | 121 - .../cache_all_test_stats_20260802_102817.json | 834 - .../cache_all_test_stats_20260802_103042.json | 1156 - .../cache_all_test_stats_20260802_112138.json | 704 - .../cache_all_test_stats_20260802_120239.json | 1344 - .../cache_all_test_stats_20260802_162333.json | 1424 - .../cache_all_test_stats_20260802_163255.json | 998 - .../cache_all_test_stats_20260802_164112.json | 938 - caching/probe_results/analyze_probe.py | 63 - caching/probe_results/damus_60s.err | 0 caching/probe_results/damus_60s.jsonl | 2 - caching/probe_results/nos_lol_15s.err | 1 - .../probe_results/nos_lol_15s_sample.jsonl | 100 - caching/probe_results/nos_lol_60s.err | 0 caching/probe_results/nos_lol_60s.jsonl | 2635 -- caching/probe_results/primal_60s.err | 0 caching/probe_results/primal_60s.jsonl | 1548 - caching/src/backfill.c | 13 +- caching/src/config.h | 9 +- caching/src/live_subscriber.c | 82 +- caching/src/live_subscriber.h | 1 + caching/src/main.c | 73 +- caching/src/pg_config.c | 45 +- caching/src/pg_inbox.c | 231 +- caching/src/pg_inbox.h | 30 +- caching/src/state.h | 5 +- make_and_restart_relay.sh | 41 +- plans/cache_all_feasibility_test_results.md | 161 + plans/caching_backfill_separation_plan.md | 85 + plans/caching_page_redesign_plan.md | 148 + plans/caching_relay_selection_plan.md | 172 + plans/inbox_speed_tuning_plan.md | 21 + plans/profile_fetcher_plan.md | 76 + plans/unified_relay_table_plan.md | 104 + relay.pid | 2 +- src/api.c | 20 +- src/caching_inbox_poller.c | 4 +- src/config.c | 86 +- src/default_config_event.h | 18 +- src/main.c | 67 +- src/main.h | 4 +- src/pg_schema.h | 243 +- src/pg_schema.sql | 27 +- 71 files changed, 33349 insertions(+), 18672 deletions(-) create mode 100644 admin/api/live_subscription.php delete mode 100644 caching/cache_all_test_report_20260802_093724.txt delete mode 100644 caching/cache_all_test_report_20260802_094007.txt delete mode 100644 caching/cache_all_test_report_20260802_095103.txt delete mode 100644 caching/cache_all_test_report_20260802_100751.txt delete mode 100644 caching/cache_all_test_report_20260802_101256.txt delete mode 100644 caching/cache_all_test_report_20260802_101828.txt delete mode 100644 caching/cache_all_test_report_20260802_102817.txt delete mode 100644 caching/cache_all_test_report_20260802_103042.txt delete mode 100644 caching/cache_all_test_report_20260802_112138.txt delete mode 100644 caching/cache_all_test_report_20260802_120239.txt delete mode 100644 caching/cache_all_test_report_20260802_162333.txt delete mode 100644 caching/cache_all_test_report_20260802_163255.txt delete mode 100644 caching/cache_all_test_report_20260802_164112.txt delete mode 100644 caching/cache_all_test_stats_20260802_093724.json delete mode 100644 caching/cache_all_test_stats_20260802_094007.json delete mode 100644 caching/cache_all_test_stats_20260802_095103.json delete mode 100644 caching/cache_all_test_stats_20260802_100751.json delete mode 100644 caching/cache_all_test_stats_20260802_101256.json delete mode 100644 caching/cache_all_test_stats_20260802_101828.json delete mode 100644 caching/cache_all_test_stats_20260802_102817.json delete mode 100644 caching/cache_all_test_stats_20260802_103042.json delete mode 100644 caching/cache_all_test_stats_20260802_112138.json delete mode 100644 caching/cache_all_test_stats_20260802_120239.json delete mode 100644 caching/cache_all_test_stats_20260802_162333.json delete mode 100644 caching/cache_all_test_stats_20260802_163255.json delete mode 100644 caching/cache_all_test_stats_20260802_164112.json delete mode 100644 caching/probe_results/analyze_probe.py delete mode 100644 caching/probe_results/damus_60s.err delete mode 100644 caching/probe_results/damus_60s.jsonl delete mode 100644 caching/probe_results/nos_lol_15s.err delete mode 100644 caching/probe_results/nos_lol_15s_sample.jsonl delete mode 100644 caching/probe_results/nos_lol_60s.err delete mode 100644 caching/probe_results/nos_lol_60s.jsonl delete mode 100644 caching/probe_results/primal_60s.err delete mode 100644 caching/probe_results/primal_60s.jsonl create mode 100644 plans/cache_all_feasibility_test_results.md create mode 100644 plans/caching_backfill_separation_plan.md create mode 100644 plans/caching_page_redesign_plan.md create mode 100644 plans/caching_relay_selection_plan.md create mode 100644 plans/inbox_speed_tuning_plan.md create mode 100644 plans/profile_fetcher_plan.md create mode 100644 plans/unified_relay_table_plan.md diff --git a/admin/api/caching.php b/admin/api/caching.php index 7f7ffc7..6ad199f 100644 --- a/admin/api/caching.php +++ b/admin/api/caching.php @@ -66,10 +66,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { exit; } -// Caching config values (for toggle controls) +// Caching config values (for toggle controls). The external daemon is +// controlled by live/backfill; the inbox poller remains a main-relay setting. $config = []; try { - $cfg_rows = $pdo->query("SELECT key, value FROM config WHERE key IN ('caching_enabled','caching_inbox_enabled','caching_live_enabled','caching_backfill_enabled')")->fetchAll(); + $cfg_rows = $pdo->query("SELECT key, value FROM config WHERE key IN ('caching_inbox_enabled','caching_live_enabled','caching_backfill_enabled')")->fetchAll(); foreach ($cfg_rows as $r) { $config[$r['key']] = $r['value']; } } catch (PDOException $e) {} @@ -87,15 +88,25 @@ try { $inbox = $pdo->query("SELECT COUNT(*) AS pending, COUNT(*) FILTER (WHERE source_class='live') AS live, COUNT(*) FILTER (WHERE source_class='backfill') AS backfill FROM caching_event_inbox")->fetch() ?: $inbox; } catch (PDOException $e) {} -// Upstream relay status (per-relay connection state) +// Upstream relay status (per-relay connection state). The unified +// caching_relays table is authoritative; the old table is only a migration +// fallback so the Backfill page cannot display stale legacy statuses. $upstreamRelays = []; try { $upstreamRelays = $pdo->query(" SELECT relay_url, status_code, status_text, updated_at - FROM caching_upstream_relays + FROM caching_relays ORDER BY relay_url ")->fetchAll(); -} catch (PDOException $e) {} +} catch (PDOException $e) { + try { + $upstreamRelays = $pdo->query(" + SELECT relay_url, status_code, status_text, updated_at + FROM caching_upstream_relays + ORDER BY relay_url + ")->fetchAll(); + } catch (PDOException $ignored) {} +} // Follows (paginated, with names from profiles cache + per-relay progress) $page = max(1, (int)($_GET['page'] ?? 1)); diff --git a/admin/api/config.php b/admin/api/config.php index 423b3d8..b0d7b2a 100644 --- a/admin/api/config.php +++ b/admin/api/config.php @@ -9,10 +9,33 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $value = $input['value'] ?? ''; if (!$key) json_response(['error' => 'Missing key']); try { - $pdo->prepare("UPDATE config SET value = ? WHERE key = ?")->execute([$value, $key]); + $pdo->beginTransaction(); + $updated = $pdo->prepare("UPDATE config SET value = ?, updated_at = EXTRACT(EPOCH FROM NOW())::BIGINT WHERE key = ?") + ->execute([$value, $key]); + if (!$updated) { + throw new RuntimeException("Failed to update $key"); + } + + // The caching daemon polls this generation value and reloads config + // independently of whether the relay's main process is restarted. + if (in_array($key, [ + 'caching_backfill_enabled', + 'caching_inbox_enabled', + 'caching_live_enabled', + 'caching_live_strategy', + 'caching_live_kinds', + 'caching_live_since_seconds', + 'caching_live_limit', + 'caching_backfill_page_size', + 'caching_backfill_tick_interval_ms' + ], true)) { + $pdo->exec("UPDATE config SET value = (COALESCE(value::int, 0) + 1)::text, updated_at = EXTRACT(EPOCH FROM NOW())::BIGINT WHERE key = 'caching_config_generation'"); + } + $pdo->commit(); json_response(['message' => "Updated $key"]); - } catch (PDOException $e) { - json_response(['error' => $e->getMessage()]); + } catch (Throwable $e) { + if ($pdo->inTransaction()) $pdo->rollBack(); + json_response(['error' => $e->getMessage()], 500); } } diff --git a/admin/api/live_subscription.php b/admin/api/live_subscription.php new file mode 100644 index 0000000..c25a3db --- /dev/null +++ b/admin/api/live_subscription.php @@ -0,0 +1,150 @@ +query("SELECT value FROM config WHERE key = 'caching_live_enabled'")->fetchColumn(); + $newVal = ($current === 'true') ? 'false' : 'true'; + $dataType = 'boolean'; + $stmt = $pdo->prepare("INSERT INTO config (key, value, data_type) VALUES ('caching_live_enabled', ?, ?) + ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = EXTRACT(EPOCH FROM NOW())::BIGINT"); + $stmt->execute([$newVal, $dataType]); + // Bump config generation to trigger hot-reload + $pdo->exec("UPDATE config SET value = (COALESCE(value::int, 0) + 1)::text, updated_at = EXTRACT(EPOCH FROM NOW())::BIGINT + WHERE key = 'caching_config_generation'"); + json_response(['ok' => true, 'enabled' => $newVal === 'true']); + } catch (PDOException $e) { + json_response(['ok' => false, 'error' => $e->getMessage()], 500); + } + } elseif ($action === 'save_config') { + $strategy = $input['strategy'] ?? ''; + $kinds = $input['kinds'] ?? ''; + $since = $input['since_seconds'] ?? ''; + $limit = $input['limit'] ?? ''; + + try { + $pdo->beginTransaction(); + if ($strategy === 'whitelist' || $strategy === 'cache_all') { + $stmt = $pdo->prepare("INSERT INTO config (key, value, data_type) VALUES ('caching_live_strategy', ?, 'string') + ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = EXTRACT(EPOCH FROM NOW())::BIGINT"); + $stmt->execute([$strategy]); + } + if ($kinds !== '') { + $stmt = $pdo->prepare("INSERT INTO config (key, value, data_type) VALUES ('caching_live_kinds', ?, 'string') + ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = EXTRACT(EPOCH FROM NOW())::BIGINT"); + $stmt->execute([$kinds]); + } + if ($since !== '') { + $stmt = $pdo->prepare("INSERT INTO config (key, value, data_type) VALUES ('caching_live_since_seconds', ?, 'integer') + ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = EXTRACT(EPOCH FROM NOW())::BIGINT"); + $stmt->execute([(string)(int)$since]); + } + if ($limit !== '') { + $stmt = $pdo->prepare("INSERT INTO config (key, value, data_type) VALUES ('caching_live_limit', ?, 'integer') + ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = EXTRACT(EPOCH FROM NOW())::BIGINT"); + $stmt->execute([(string)(int)$limit]); + } + // Bump config generation to trigger hot-reload + $pdo->exec("UPDATE config SET value = (COALESCE(value::int, 0) + 1)::text, updated_at = EXTRACT(EPOCH FROM NOW())::BIGINT + WHERE key = 'caching_config_generation'"); + $pdo->commit(); + json_response(['ok' => true, 'message' => 'Live subscription config saved.']); + } catch (PDOException $e) { + $pdo->rollBack(); + json_response(['ok' => false, 'error' => $e->getMessage()], 500); + } + } elseif ($action === 'toggle_relay') { + // Toggle live_enabled or backfill_enabled for a relay in caching_relays + $relay_url = $input['relay_url'] ?? ''; + $column = $input['column'] ?? ''; + if ($relay_url === '' || !in_array($column, ['live_enabled', 'backfill_enabled'])) { + json_response(['ok' => false, 'error' => 'Invalid relay_url or column'], 400); + exit; + } + try { + $stmt = $pdo->prepare("UPDATE caching_relays SET $column = NOT $column, updated_at = EXTRACT(EPOCH FROM NOW())::BIGINT WHERE relay_url = ?"); + $stmt->execute([$relay_url]); + // Bump config generation to trigger hot-reload + $pdo->exec("UPDATE config SET value = (COALESCE(value::int, 0) + 1)::text, updated_at = EXTRACT(EPOCH FROM NOW())::BIGINT + WHERE key = 'caching_config_generation'"); + json_response(['ok' => true, 'message' => 'Relay toggled.']); + } catch (PDOException $e) { + json_response(['ok' => false, 'error' => $e->getMessage()], 500); + } + } else { + json_response(['ok' => false, 'error' => 'Unknown action'], 400); + } + exit; +} + +// --- GET: return current live subscription config + status --- +$config = []; +try { + $cfg_rows = $pdo->query("SELECT key, value FROM config WHERE key IN ( + 'caching_live_strategy','caching_live_kinds','caching_live_since_seconds', + 'caching_live_limit','caching_live_enabled', + 'caching_kinds','caching_bootstrap_relays' + )")->fetchAll(); + foreach ($cfg_rows as $r) { $config[$r['key']] = $r['value']; } +} catch (PDOException $e) {} + +// Live subscription status from caching_service_state +$state = []; +try { $state = $pdo->query("SELECT * FROM caching_service_state WHERE id = 1")->fetch() ?: []; } catch (PDOException $e) {} + +// Unified relay list from caching_relays table +$relays = []; +try { + $relays = $pdo->query(" + SELECT relay_url, live_enabled, backfill_enabled, status_code, status_text, follow_count, is_bootstrap + FROM caching_relays + ORDER BY follow_count DESC, relay_url + ")->fetchAll(); +} catch (PDOException $e) {} + +// If caching_relays table is empty (migration not yet run), fall back to old sources +if (empty($relays)) { + // Upstream relay status (old table) + $upstreamRelays = []; + try { + $upstreamRelays = $pdo->query(" + SELECT relay_url, status_code, status_text, updated_at + FROM caching_upstream_relays + ORDER BY relay_url + ")->fetchAll(); + } catch (PDOException $e) {} + + // Discovered relays with follow counts (from backfill progress) + $discoveredRelays = []; + try { + $discoveredRelays = $pdo->query(" + SELECT relay_url, COUNT(DISTINCT author_pubkey) AS follow_count + FROM caching_backfill_relay_progress + GROUP BY relay_url + ORDER BY follow_count DESC, relay_url + ")->fetchAll(); + } catch (PDOException $e) {} + + json_response([ + 'config' => $config, + 'state' => $state, + 'upstreamRelays' => $upstreamRelays, + 'discoveredRelays' => $discoveredRelays, + 'relays' => [], + ]); +} else { + json_response([ + 'config' => $config, + 'state' => $state, + 'relays' => $relays, + ]); +} diff --git a/admin/assets/app.js b/admin/assets/app.js index cc18e0f..fd692f0 100644 --- a/admin/assets/app.js +++ b/admin/assets/app.js @@ -59,7 +59,7 @@ function switchPage(pageName) { const sections = [ 'databaseStatisticsSection', 'subscriptionDetailsSection', 'div_config', 'authRulesSection', 'wotSection', 'ipBansSection', 'relayEventsSection', - 'cachingSection', 'nip17DMSection', 'cleanupSection', 'sqlQuerySection' + 'backfillSection', 'cachingSection', 'nip17DMSection', 'cleanupSection', 'sqlQuerySection' ]; sections.forEach(id => { const el = document.getElementById(id); if (el) el.style.display = 'none'; }); @@ -69,6 +69,7 @@ function switchPage(pageName) { 'configuration': 'div_config', 'ip-bans': 'ipBansSection', 'relay-events': 'relayEventsSection', + 'backfill': 'backfillSection', 'caching': 'cachingSection', 'dm': 'nip17DMSection', 'cleanup': 'cleanupSection', @@ -92,6 +93,7 @@ function switchPage(pageName) { 'configuration': loadConfig, 'ip-bans': loadIpBans, 'relay-events': loadEvents, + 'backfill': loadBackfill, 'caching': loadCaching, 'dm': loadDMs, 'cleanup': loadCleanupQueries, @@ -99,15 +101,18 @@ function switchPage(pageName) { if (loaders[pageName]) loaders[pageName](); // Auto-refresh: one interval that calls the right loader for the current page. - // When switching between statistics and caching, the interval keeps running + // When switching between statistics, backfill, or caching, the interval keeps running // but picks the correct loader based on currentPage. - if (pageName === 'statistics' || pageName === 'caching') { - const loader = pageName === 'statistics' ? loadStats : loadCaching; - loader(); // always load immediately on switch + if (pageName === 'statistics' || pageName === 'backfill' || pageName === 'caching') { + if (pageName !== 'statistics') { + // Load immediately on switch for non-statistics pages + if (loaders[pageName]) loaders[pageName](); + } if (!statsInterval) { statsInterval = setInterval(() => { - const l = currentPage === 'statistics' ? loadStats : loadCaching; - l(); + if (currentPage === 'statistics') loadStats(); + else if (currentPage === 'backfill') loadBackfill(); + else if (currentPage === 'caching') loadCaching(); }, REFRESH_MS); console.log('[admin2] auto-refresh started, interval:', REFRESH_MS, 'ms'); } @@ -417,42 +422,40 @@ async function loadEvents() { } // ================================ -// CACHING +// BACKFILL (renamed from CACHING) // ================================ -let cachingPage = 1; +let backfillPage = 1; -async function loadCaching() { +async function loadBackfill() { try { - const res = await fetch('api/caching.php?page=' + cachingPage); + const res = await fetch('api/caching.php?page=' + backfillPage); const d = await res.json(); // Config toggle state if (d.config) { - const enabled = d.config.caching_enabled === 'true'; + const backfillEnabled = d.config.caching_backfill_enabled === 'true'; const inboxEnabled = d.config.caching_inbox_enabled === 'true'; - const enLabel = document.getElementById('caching-enabled-label'); - const enBtn = document.getElementById('caching-toggle-btn'); - const inLabel = document.getElementById('caching-inbox-enabled-label'); - const inBtn = document.getElementById('caching-inbox-toggle-btn'); - if (enLabel) enLabel.textContent = 'Caching: ' + (enabled ? 'ON' : 'OFF'); - if (enBtn) enBtn.textContent = enabled ? 'Turn OFF' : 'Turn ON'; - if (inLabel) inLabel.textContent = 'Inbox: ' + (inboxEnabled ? 'ON' : 'OFF'); - if (inBtn) inBtn.textContent = inboxEnabled ? 'Turn OFF' : 'Turn ON'; + const serviceRunning = d.state?.service_state === 'running'; + const enProcessRunning = backfillEnabled && serviceRunning; + const enBtn = document.getElementById('backfill-toggle-btn'); + if (enBtn) { + enBtn.innerHTML = backfillEnabled + ? (enProcessRunning + ? 'Turn Backfill Off' + : 'Turn Backfill Off') + : 'Turn Backfill On'; + enBtn.classList.toggle('process-running', enProcessRunning); + enBtn.setAttribute('aria-pressed', backfillEnabled ? 'true' : 'false'); + } + // Show relay status section only when backfill or inbox is enabled + const relayGroup = document.getElementById('backfill-relay-group'); + if (relayGroup) { + relayGroup.style.display = (backfillEnabled || inboxEnabled) ? '' : 'none'; + } } - // Service status - const ssEl = document.getElementById('caching-service-status'); - if (ssEl && d.state) { - const s = d.state; - const hb = s.heartbeat_at ? new Date(s.heartbeat_at * 1000).toLocaleTimeString() : '—'; - ssEl.innerHTML = `

State: ${esc(s.service_state)} | Follows: ${s.followed_author_count ?? 0} | Connected relays: ${s.connected_relay_count ?? 0}/${s.selected_relay_count ?? 0} | Backfill: ${s.backfill_authors_complete ?? 0}/${s.backfill_authors_total ?? 0} | Events fetched: ${s.events_fetched ?? 0} | Inbox inserts: ${s.inbox_inserts ?? 0} | Heartbeat: ${hb}

`; - } - // Inbox status - const isEl = document.getElementById('caching-inbox-status'); - if (isEl && d.inbox) { - isEl.innerHTML = `

Pending: ${d.inbox.pending} | Live: ${d.inbox.live} | Backfill: ${d.inbox.backfill}

`; - } - // Upstream relay status window - const rsEl = document.getElementById('caching-relay-status'); + // Upstream relay status window. The API reads the unified + // caching_relays table, keeping status aligned with relay controls. + const rsEl = document.getElementById('backfill-relay-status'); if (rsEl) { if (d.upstreamRelays && d.upstreamRelays.length > 0) { const connected = d.upstreamRelays.filter(r => r.status_code == 2).length; @@ -460,7 +463,7 @@ async function loadCaching() { rsEl.innerHTML = `
Connected: ${connected}/${total}
` + '
' + d.upstreamRelays.map(r => { - const host = r.relay_url.replace(/^wss?:\/\//, '').replace(/\/relay$/, ''); + const host = r.relay_url.replace(/^wss?:\/\//, ''); let cls = 'relay-status-unknown'; let label = esc(r.status_text || 'unknown'); if (r.status_code == 2) { cls = 'relay-status-ok'; label = 'connected'; } @@ -475,7 +478,7 @@ async function loadCaching() { } } // Follows table - const tbody = document.getElementById('caching-follows-table-body'); + const tbody = document.getElementById('backfill-follows-table-body'); if (tbody && d.follows) { if (d.follows.length === 0) { tbody.innerHTML = 'No followed pubkeys'; @@ -501,7 +504,7 @@ async function loadCaching() { }).join('') + '
'; } - const refreshBtn = ``; + const refreshBtn = ``; const rowClass = isActive ? 'follows-row follows-row-active' : 'follows-row'; const activeIcon = isActive ? '⚡ ' : ''; return `${activeIcon}${esc(f.name) || 'unknown'}${esc(npub)}…${f.is_root ? '✓' : ''}${f.total_events ?? 0}${f.backfill_complete ? '✓' : '…'}${relaySummary}
${relayDetail || 'No relay progress data'}
${refreshBtn}
`; @@ -509,72 +512,78 @@ async function loadCaching() { } } // Pagination controls - const pgEl = document.getElementById('caching-follows-pagination'); + const pgEl = document.getElementById('backfill-follows-pagination'); if (pgEl && d.totalFollows > 0) { const totalPages = Math.ceil(d.totalFollows / (d.perPage || 50)); pgEl.innerHTML = 'Page ' + d.page + ' of ' + totalPages + ' (' + d.totalFollows + ' total)' + '
' + - (d.page > 1 ? '' : '') + - (d.page < totalPages ? '' : '') + + (d.page > 1 ? '' : '') + + (d.page < totalPages ? '' : '') + '
'; } else if (pgEl) { pgEl.innerHTML = ''; } - // Active target - const fsEl = document.getElementById('caching-follows-status'); + // Active target — only show backfill progress when backfill is actually enabled + const fsEl = document.getElementById('backfill-follows-status'); if (fsEl) { - if (d.active && d.active.pubkey && d.active.relay) { + const backfillOn = d.config?.caching_backfill_enabled === 'true'; + if (d.active && d.active.pubkey && d.active.relay && backfillOn) { fsEl.innerHTML = `⚡ Backfilling: ${d.active.pubkey.substring(0,16)}… @ ${esc(d.active.relay)}`; - } else if (d.state && d.state.service_state === 'running') { + } else if (backfillOn && d.state && d.state.service_state === 'running') { const incomplete = d.state.backfill_authors_complete < d.state.backfill_authors_total; fsEl.innerHTML = incomplete ? `⚡ Backfill in progress…` - : `✓ Backfill complete — listening for live events`; + : `✓ Backfill complete`; + } else if (backfillOn) { + fsEl.innerHTML = `✓ Backfill complete`; } else { - fsEl.innerHTML = `✓ Caching complete`; + fsEl.innerHTML = `Backfill is off`; } } - } catch (e) { console.error('[admin2] caching error:', e); } + } catch (e) { console.error('[admin2] backfill error:', e); } } -// Toggle caching_enabled config via the config API. -async function toggleCachingEnabled() { +// Toggle caching_backfill_enabled config via the config API. +async function toggleBackfillEnabled() { try { const res = await fetch('api/caching.php'); const d = await res.json(); - const current = d.config?.caching_enabled === 'true'; + const current = d.config?.caching_backfill_enabled === 'true'; const newVal = current ? 'false' : 'true'; - await fetch('api/config.php', { + const saveRes = await fetch('api/config.php', { method: 'POST', headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({key: 'caching_enabled', value: newVal}) + body: JSON.stringify({key: 'caching_backfill_enabled', value: newVal}) }); - loadCaching(); - } catch (e) { console.error('[admin2] toggle caching error:', e); } + if (!saveRes.ok) throw new Error('Backfill setting update failed'); + loadBackfill(); + } catch (e) { console.error('[admin2] toggle backfill error:', e); } } -// Toggle caching_inbox_enabled config via the config API. -async function toggleCachingInboxEnabled() { +// Inbox is always enabled by default and is controlled from Configuration. +// This legacy handler remains unused by the Backfill page. +async function toggleBackfillInboxEnabled() { try { const res = await fetch('api/caching.php'); const d = await res.json(); const current = d.config?.caching_inbox_enabled === 'true'; const newVal = current ? 'false' : 'true'; - await fetch('api/config.php', { + const saveRes = await fetch('api/config.php', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({key: 'caching_inbox_enabled', value: newVal}) }); - loadCaching(); + if (!saveRes.ok) throw new Error('Inbox setting update failed'); + loadBackfill(); } catch (e) { console.error('[admin2] toggle inbox error:', e); } } -// Re-run all caching: resets backfill progress for all followed authors and +// Re-run all backfill: resets backfill progress for all followed authors and // bumps caching_config_generation so the running service hot-reloads and // re-drains from the beginning. -async function rerunAllCaching() { +async function rerunAllBackfill() { if (!confirm('Reset backfill progress for ALL followed authors? The caching service will re-download everything from scratch.')) return; - const btn = document.getElementById('caching-rerun-all-btn'); + const btn = document.getElementById('backfill-rerun-all-btn'); if (btn) { btn.disabled = true; btn.textContent = 'Resetting…'; } try { const res = await fetch('api/caching.php', { @@ -585,21 +594,21 @@ async function rerunAllCaching() { const d = await res.json(); if (d.ok) { if (btn) { btn.textContent = '✓ Reset — re-draining'; } - setTimeout(() => { if (btn) { btn.disabled = false; btn.textContent = 'Re-run All Caching'; } loadCaching(); }, 2000); + setTimeout(() => { if (btn) { btn.disabled = false; btn.textContent = 'Re-run All Backfill'; } loadBackfill(); }, 2000); } else { alert('Reset failed: ' + (d.error || 'unknown error')); - if (btn) { btn.disabled = false; btn.textContent = 'Re-run All Caching'; } + if (btn) { btn.disabled = false; btn.textContent = 'Re-run All Backfill'; } } } catch (e) { - console.error('[admin2] rerunAllCaching error:', e); + console.error('[admin2] rerunAllBackfill error:', e); alert('Reset failed: ' + e.message); - if (btn) { btn.disabled = false; btn.textContent = 'Re-run All Caching'; } + if (btn) { btn.disabled = false; btn.textContent = 'Re-run All Backfill'; } } } // Refresh a single user: resets backfill progress for one followed author // and bumps caching_config_generation so the service re-fetches that author. -async function refreshCachingUser(pubkey, name) { +async function refreshBackfillUser(pubkey, name) { if (!confirm('Reset backfill progress for ' + (name || pubkey.substring(0, 16) + '…') + '? The caching service will re-download this user\'s events from scratch.')) return; try { const res = await fetch('api/caching.php', { @@ -609,16 +618,317 @@ async function refreshCachingUser(pubkey, name) { }); const d = await res.json(); if (d.ok) { - loadCaching(); + loadBackfill(); } else { alert('Refresh failed: ' + (d.error || 'unknown error')); } } catch (e) { - console.error('[admin2] refreshCachingUser error:', e); + console.error('[admin2] refreshBackfillUser error:', e); alert('Refresh failed: ' + e.message); } } +// ================================ +// CACHING (Live Subscription Design) +// ================================ + +async function loadCaching() { + try { + const res = await fetch('api/live_subscription.php'); + const d = await res.json(); + + // Live subscription config + if (d.config) { + const strategy = d.config.caching_live_strategy || 'whitelist'; + const kinds = d.config.caching_live_kinds || d.config.caching_kinds || ''; + const since = d.config.caching_live_since_seconds || '0'; + const limit = d.config.caching_live_limit || '0'; + const liveEnabled = d.config.caching_live_enabled === 'true'; + const backfillEnabled = d.config.caching_backfill_enabled === 'true'; + const serviceRunning = d.state?.service_state === 'running'; + const liveProcessRunning = liveEnabled && serviceRunning; + + // Toggle button state: the spinner reflects the running daemon, + // not merely a persisted configuration value. + const enBtn = document.getElementById('caching-live-toggle-btn'); + if (enBtn) { + enBtn.innerHTML = liveEnabled + ? (liveProcessRunning + ? 'Turn Caching Off' + : 'Turn Caching Off') + : 'Turn Caching On'; + enBtn.classList.toggle('process-running', liveProcessRunning); + enBtn.setAttribute('aria-pressed', liveEnabled ? 'true' : 'false'); + } + + // Set strategy radio + document.querySelectorAll('input[name="caching-strategy"]').forEach(r => { + r.checked = r.value === strategy; + }); + document.getElementById('caching-kinds').value = kinds; + document.getElementById('caching-since').value = since; + document.getElementById('caching-limit').value = limit; + // Update strategy note text without auto-saving + const note = document.getElementById('caching-strategy-note'); + if (note) { + note.textContent = strategy === 'cache_all' + ? 'All events on the relay (no authors filter). Use the Cleanup page to periodically remove unwanted data.' + : 'Only events from followed pubkeys (authors filter auto-populated from follow graph).'; + } + + // Update filter preview + updateCachingFilterPreview(); + } + + // Live subscription status from service state + const lsEl = document.getElementById('caching-live-status'); + if (lsEl) { + if (d.state && d.state.service_state) { + const s = d.state; + const hb = s.heartbeat_at ? new Date(s.heartbeat_at * 1000).toLocaleTimeString() : '—'; + const liveEnabled = d.config?.caching_live_enabled === 'true'; + lsEl.innerHTML = `

Service: ${esc(s.service_state)} | Live sub: ${liveEnabled ? 'ON' : 'OFF'} | Heartbeat: ${hb}

`; + } else { + lsEl.innerHTML = '

Waiting for caching service status…

'; + } + } + + // Relay selection list (merged with upstream status) + renderCachingRelaySelection(d); + } catch (e) { console.error('[admin2] caching error:', e); } +} + +// Toggle caching_live_enabled config via the live subscription API. +async function toggleCachingLiveEnabled() { + try { + const res = await fetch('api/live_subscription.php', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({action: 'toggle_enabled'}) + }); + const d = await res.json(); + if (d.ok) { + loadCaching(); + } else { + alert('Toggle failed: ' + (d.error || 'unknown error')); + } + } catch (e) { + console.error('[admin2] toggleCachingLiveEnabled error:', e); + alert('Toggle failed: ' + e.message); + } +} + +// Toggle a relay's live_enabled or backfill_enabled flag via the API. +// The caching service hot-reloads on config generation bump. +async function toggleRelay(relayUrl, column) { + try { + const res = await fetch('api/live_subscription.php', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({action: 'toggle_relay', relay_url: relayUrl, column}) + }); + const d = await res.json(); + if (!d.ok) { + console.warn('[admin2] toggleRelay failed:', d.error); + } + } catch (e) { + console.error('[admin2] toggleRelay error:', e); + } +} + +// Render the relay selection list from the unified caching_relays table. +// Each relay has live_enabled and backfill_enabled checkboxes that toggle +// immediately via the API (no "Save" button needed for individual toggles). +function renderCachingRelaySelection(d) { + const el = document.getElementById('caching-relay-selection'); + if (!el) return; + + // Use unified relays array if available, otherwise fall back to old format + let relays = d.relays; + if (!relays || relays.length === 0) { + // Fallback: build from old upstreamRelays + discoveredRelays + const relayMap = {}; + (d.discoveredRelays || []).forEach(r => { + relayMap[r.relay_url] = { follow_count: parseInt(r.follow_count, 10) || 0, status: null, live_enabled: false, backfill_enabled: false, is_bootstrap: false }; + }); + (d.upstreamRelays || []).forEach(r => { + if (!relayMap[r.relay_url]) relayMap[r.relay_url] = { follow_count: 0, status: null, live_enabled: false, backfill_enabled: false, is_bootstrap: false }; + relayMap[r.relay_url].status = r; + }); + const bootstrapSet = new Set((d.config?.caching_bootstrap_relays || '').split(',').map(s => s.trim()).filter(s => s)); + const liveSet = new Set((d.config?.caching_live_relays || d.config?.caching_bootstrap_relays || '').split(',').map(s => s.trim()).filter(s => s)); + relays = Object.entries(relayMap).map(([url, info]) => ({ + relay_url: url, + live_enabled: liveSet.has(url) ? 't' : 'f', + backfill_enabled: liveSet.has(url) ? 't' : 'f', + status_code: info.status ? info.status.status_code : 0, + status_text: info.status ? info.status.status_text : '', + follow_count: info.follow_count, + is_bootstrap: bootstrapSet.has(url) ? 't' : 'f' + })); + } + + if (relays.length === 0) { + el.innerHTML = '

No relays discovered yet (waiting for backfill progress data)

'; + return; + } + + let html = '
'; + html += '
' + + 'Live' + + 'Relay' + + 'Follows' + + 'Status' + + '
'; + relays.forEach(r => { + const url = r.relay_url; + const host = url.replace(/^wss?:\/\//, ''); + const isBootstrap = r.is_bootstrap === 't' || r.is_bootstrap === true; + const liveChecked = r.live_enabled === 't' || r.live_enabled === true; + const followCount = parseInt(r.follow_count, 10) || 0; + + let statusLabel = 'unknown'; + let statusClass = 'sel-unknown'; + const sc = parseInt(r.status_code, 10); + if (sc == 2) { statusClass = 'sel-ok'; statusLabel = 'connected'; } + else if (sc == 1) { statusClass = 'sel-connecting'; statusLabel = 'connecting'; } + else if (sc == 0) { + // If live_enabled, show as normal text (not dimmed) even before connected + if (liveChecked) { + statusClass = 'sel-enabled-waiting'; + statusLabel = 'waiting...'; + } else { + statusClass = 'sel-disconnected'; + statusLabel = 'disconnected'; + } + } + else { statusClass = 'sel-error'; statusLabel = esc(r.status_text || 'error'); } + + const bootstrapTag = isBootstrap ? ' bootstrap' : ''; + html += `
+ + ${esc(host)}${bootstrapTag} + ${followCount} follows + ${statusLabel} +
`; + }); + html += '
'; + el.innerHTML = html; +} + +// Wire up live preview updates on input changes +document.addEventListener('DOMContentLoaded', () => { + ['caching-kinds', 'caching-since', 'caching-limit'].forEach(id => { + const el = document.getElementById(id); + if (el) el.addEventListener('input', updateCachingFilterPreview); + }); +}); + +// Update the strategy description note when radio changes, and auto-save +function onCachingStrategyChange() { + const selected = document.querySelector('input[name="caching-strategy"]:checked'); + const note = document.getElementById('caching-strategy-note'); + if (selected && note) { + if (selected.value === 'whitelist') { + note.textContent = 'Only events from followed pubkeys (authors filter auto-populated from follow graph).'; + } else { + note.textContent = 'All events on the relay (no authors filter). Use the Cleanup page to periodically remove unwanted data.'; + } + } + // Auto-save strategy immediately so auto-refresh doesn't revert it + const strategy = selected?.value || 'whitelist'; + fetch('api/live_subscription.php', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({action: 'save_config', strategy, kinds: '', since_seconds: '', limit: ''}) + }).catch(e => console.error('[admin2] auto-save strategy error:', e)); + // Update filter preview + updateCachingFilterPreview(); +} + +// Build and display the subscription filter preview +function updateCachingFilterPreview() { + const el = document.getElementById('caching-filter-preview'); + if (!el) return; + + const strategy = document.querySelector('input[name="caching-strategy"]:checked')?.value || 'whitelist'; + const kindsRaw = document.getElementById('caching-kinds').value.trim(); + const sinceSec = parseInt(document.getElementById('caching-since').value, 10) || 0; + const limitVal = parseInt(document.getElementById('caching-limit').value, 10) || 0; + + const filter = {}; + + // Authors: only in whitelist mode + if (strategy === 'whitelist') { + filter.authors = '[auto-populated from follow graph]'; + } + + // Kinds + if (kindsRaw) { + const kinds = kindsRaw.split(',').map(s => parseInt(s.trim(), 10)).filter(n => !isNaN(n)); + if (kinds.length > 0) { + filter.kinds = kinds; + } + } + + // Since + if (sinceSec > 0) { + filter.since = 'now - ' + sinceSec + 's'; + } else { + filter.since = 'now'; + } + + // Limit + if (limitVal > 0) { + filter.limit = limitVal; + } + + el.textContent = JSON.stringify(filter, null, 2); +} + +// Save live subscription config +async function saveCachingConfig() { + const strategy = document.querySelector('input[name="caching-strategy"]:checked')?.value || 'whitelist'; + const kinds = document.getElementById('caching-kinds').value.trim(); + const since = document.getElementById('caching-since').value.trim(); + const limit = document.getElementById('caching-limit').value.trim(); + const statusEl = document.getElementById('caching-save-status'); + if (statusEl) statusEl.textContent = 'Saving…'; + + try { + const res = await fetch('api/live_subscription.php', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + action: 'save_config', + strategy, + kinds, + since_seconds: since, + limit + }) + }); + const d = await res.json(); + if (d.ok) { + if (statusEl) { + statusEl.textContent = '✓ Configuration saved.'; + statusEl.style.color = 'var(--success-color, #27ae60)'; + setTimeout(() => { if (statusEl) statusEl.textContent = ''; }, 3000); + } + } else { + if (statusEl) { + statusEl.textContent = 'Error: ' + (d.error || 'unknown'); + statusEl.style.color = 'var(--error-color, #c0392b)'; + } + } + } catch (e) { + console.error('[admin2] saveCachingConfig error:', e); + if (statusEl) { + statusEl.textContent = 'Error: ' + e.message; + statusEl.style.color = 'var(--error-color, #c0392b)'; + } + } +} + // ================================ // DMs // ================================ diff --git a/admin/assets/index.css b/admin/assets/index.css index 4252f04..b933034 100644 --- a/admin/assets/index.css +++ b/admin/assets/index.css @@ -341,7 +341,7 @@ body { line-height: 1.6; } -/* Cleanup radio group */ +/* Radio group — monochrome aesthetic */ .radio-group { display: flex; gap: 16px; @@ -357,6 +357,25 @@ body { font-size: 13px; } +.radio-group input[type="radio"] { + appearance: none; + -webkit-appearance: none; + width: 14px; + height: 14px; + border: 1px solid var(--primary-color); + border-radius: 50%; + margin: 0; + cursor: pointer; + background: var(--secondary-color); + padding: 0; +} + +.radio-group input[type="radio"]:checked { + border-color: var(--accent-color); + background: var(--accent-color); + box-shadow: inset 0 0 0 3px var(--secondary-color); +} + /* Cleanup form group */ .form-group { margin-bottom: 10px; @@ -691,6 +710,50 @@ button:disabled { flex: 1; } +/* Process toggle buttons describe the action and show an active red braille spinner. */ +.process-toggle-btn, +.process-toggle-btn:hover, +.process-toggle-btn:focus, +.process-toggle-btn:active { + min-width: 170px; + background: var(--secondary-color) !important; + color: var(--primary-color) !important; + border-color: var(--border-color) !important; +} + +.process-toggle-btn.process-running, +.process-toggle-btn.process-running:hover, +.process-toggle-btn.process-running:focus, +.process-toggle-btn.process-running:active { + color: #d32f2f !important; +} + +.process-spinner { + display: inline-block; + width: 1.15em; + margin-right: 4px; + color: #d32f2f; +} + +.process-spinner::after { + content: '⠋'; + display: inline-block; + color: #d32f2f !important; + animation: process-braille-glyph 0.8s steps(8, end) infinite; +} + +@keyframes process-braille-glyph { + 0% { content: '⠋'; } + 12.5% { content: '⠙'; } + 25% { content: '⠹'; } + 37.5% { content: '⠸'; } + 50% { content: '⠼'; } + 62.5% { content: '⠴'; } + 75% { content: '⠦'; } + 87.5% { content: '⠧'; } + 100% { content: '⠋'; } +} + .user-info { padding: 10px; border: var(--border-width) solid var(--border-color); @@ -1641,7 +1704,8 @@ bdi { /* Name cells in tables: prevent layout breakage. */ #stats-pubkeys-table td:nth-child(2), -#caching-follows-table td:nth-child(1) { +#caching-follows-table td:nth-child(1), +#backfill-follows-table td:nth-child(1) { max-width: 200px; overflow: hidden; text-overflow: ellipsis; @@ -1886,3 +1950,134 @@ bdi { color: var(--secondary-color); border-color: var(--accent-color); } + +/* Relay selection list (caching page) */ +.relay-selection-list { + display: flex; + flex-direction: column; + gap: 2px; + max-height: 300px; + overflow-y: auto; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + padding: 4px; +} +.relay-selection-row { + display: flex; + align-items: center; + gap: 4px; + padding: 3px 6px; + border-radius: var(--border-radius); + font-size: 12px; + font-family: var(--font-family); + border: 1px solid transparent; +} +.relay-selection-header { + display: flex; + align-items: center; + gap: 4px; + padding: 3px 6px; + font-size: 10px; + font-weight: bold; + color: var(--text-muted, #888); + border-bottom: 1px solid var(--border-color); + margin-bottom: 2px; +} +.relay-selection-header .sel-checkbox-label { + flex-shrink: 0; + width: 30px; + text-align: center; + font-size: 9px; +} +.relay-selection-header .sel-url { + flex: 1; + min-width: 0; +} +.relay-selection-header .sel-follows { + flex-shrink: 0; + width: 60px; + text-align: right; +} +.relay-selection-header .sel-badge { + flex-shrink: 0; + width: 70px; + text-align: center; +} +.relay-selection-row .sel-checkbox-live { + flex-shrink: 0; + width: 13px; + height: 13px; + margin: 0 8px 0 0; + cursor: pointer; +} +.relay-selection-row .sel-url { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; +} +.relay-selection-row .sel-follows { + flex-shrink: 0; + font-size: 11px; + color: var(--text-muted, #888); + width: 60px; + text-align: right; +} +.relay-selection-row .sel-badge { + flex-shrink: 0; + font-size: 10px; + padding: 1px 5px; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + width: 70px; + text-align: center; + white-space: nowrap; + box-sizing: border-box; +} +/* Status colors for relay selection rows */ +.relay-selection-row.sel-ok { + color: var(--primary-color); +} +.relay-selection-row.sel-ok .sel-badge { + border-color: var(--primary-color); +} +.relay-selection-row.sel-connecting { + color: var(--muted-color); +} +.relay-selection-row.sel-connecting .sel-badge { + border-color: var(--muted-color); +} +.relay-selection-row.sel-disconnected { + color: var(--muted-color); +} +.relay-selection-row.sel-disconnected .sel-badge { + border-color: var(--muted-color); +} +.relay-selection-row.sel-error { + color: var(--accent-color); +} +.relay-selection-row.sel-error .sel-badge { + border-color: var(--accent-color); +} +.relay-selection-row.sel-unknown { + color: var(--muted-color); +} +.relay-selection-row.sel-unknown .sel-badge { + border-color: var(--muted-color); +} +.relay-selection-row.sel-enabled-waiting { + color: var(--primary-color); +} +.relay-selection-row.sel-enabled-waiting .sel-badge { + border-color: var(--primary-color); + font-style: italic; +} +.bootstrap-tag { + font-size: 10px; + color: var(--text-muted, #888); + background: var(--bg-code, #f0f0f0); + padding: 0 4px; + border-radius: 2px; + margin-left: 4px; +} diff --git a/admin/cache/chart_day.txt b/admin/cache/chart_day.txt index 51ed634..74057e2 100644 --- a/admin/cache/chart_day.txt +++ b/admin/cache/chart_day.txt @@ -1,15 +1,15 @@ New Events — Last 24 Hours - 21 | - 19 | - 17 | - 15 | - 13 | - 11 | X - 9 | X - 7 | X - 5 | X - 3 | X X X - 1 | X X X X X XX X X XX X X X X X X X X XX X X X XX X X +10071 |XX X +9064 |XX X X +8057 |XXX X X X +7050 |XXXXXXX X XX +6043 |XXXXXXX X XXX +5036 |XXXXXXX X XXXXX +4029 |XXXXXXXXX XXXXX +3022 |XXXXXXXXXX XXXXX +2015 |XXXXXXXXXX XXXXXX +1008 |XXXXXXXXXX XXXXXX + 1 |XXXXXXXXXX XXXXXX X XXX X X X X XX X XXXXXXX XX +-------------------------------------------------------------------------------- 0s 1h 3h 4h 6h 7h 9h 10h 12h 13h 15h 16h 18h 19h 21h 22h diff --git a/admin/cache/stats_kinds.json b/admin/cache/stats_kinds.json index 0ab2891..fa002f6 100644 --- a/admin/cache/stats_kinds.json +++ b/admin/cache/stats_kinds.json @@ -1 +1 @@ -[{"kind":1,"count":501,"pct":56.8},{"kind":7,"count":200,"pct":22.7},{"kind":9734,"count":110,"pct":12.5},{"kind":6,"count":30,"pct":3.4},{"kind":9735,"count":20,"pct":2.3},{"kind":3,"count":11,"pct":1.2},{"kind":0,"count":10,"pct":1.1}] \ No newline at end of file +[{"kind":1,"count":2135,"pct":96.7},{"kind":6,"count":41,"pct":1.9},{"kind":30023,"count":11,"pct":0.5},{"kind":10002,"count":8,"pct":0.4},{"kind":0,"count":5,"pct":0.2},{"kind":3,"count":4,"pct":0.2},{"kind":10000,"count":4,"pct":0.2}] \ No newline at end of file diff --git a/admin/cache/stats_pubkeys.json b/admin/cache/stats_pubkeys.json index d35df13..90b35c6 100644 --- a/admin/cache/stats_pubkeys.json +++ b/admin/cache/stats_pubkeys.json @@ -1 +1 @@ -[{"pubkey":"4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa","name":"","count":182,"pct":20.6},{"pubkey":"eff555c8453f665bb54f907188aefba0e85c29aa9f5c30fa13d78d003c71265c","name":"Random User 4","count":70,"pct":7.9},{"pubkey":"9a90e4db8996e235a5bf1501013149d5f250b60e79336b69035f0b4dab8a4302","name":"Random User 9","count":70,"pct":7.9},{"pubkey":"b80c8c5fb175c50fe97d3a63658fa8b5dcc3e20da22dff8cef54317f6fe25394","name":"Random User 7","count":70,"pct":7.9},{"pubkey":"3d68fc5c3ddf8feef6cb6345817e4c6e421b5d292bef0be8d9a4809a23a4f0f5","name":"Random User 1","count":70,"pct":7.9},{"pubkey":"1c38f0e5446b8f5d7bc074f9f4b7afe85980706e765a5b1a3e4a52504e521ec1","name":"Random User 6","count":70,"pct":7.9},{"pubkey":"084857a219f6d73ec30fb90376e6f30161b07cd5d2130dffe47d32f229610b8a","name":"Random User 5","count":70,"pct":7.9},{"pubkey":"71691b130642de5af2b7f0a5ecf4eb374c46d913dbc72bc802005b24f9e87e11","name":"Random User 8","count":70,"pct":7.9},{"pubkey":"8d8b59e842e1529e0c448f05e570eccca82d9ca4500b758c9d44f478e96e8cdf","name":"Random User 10","count":70,"pct":7.9},{"pubkey":"f4bd9e04029c11440d545ffa66aeac45b87becf4075e2f321b0f5e8c30f187c2","name":"Random User 2","count":70,"pct":7.9},{"pubkey":"f2c0cadeee26653d9d2acd3788f6cc26d19b680d439640501eb1545afb94dd56","name":"Random User 3","count":70,"pct":7.9}] \ No newline at end of file +[{"pubkey":"4c800257a588a82849d049817c2bdaad984b25a45ad9f6dad66e47d3b47e3b2f","name":"mleku","count":572,"pct":25.9},{"pubkey":"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c","name":"Vitor Pamplona","count":564,"pct":25.5},{"pubkey":"1ec454734dcbf6fe54901ce25c0c7c6bca5edd89443416761fadc321d38df139","name":"Laan Tungir","count":525,"pct":23.8},{"pubkey":"4d7842051782e0d3feb034d150adc2b6bae4ee3b49786793bffa468b6f5b96b3","name":"FLASH","count":509,"pct":23.1},{"pubkey":"52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8","name":"Didactyl Agent","count":34,"pct":1.5},{"pubkey":"fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52","name":"","count":1,"pct":0},{"pubkey":"4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa","name":"","count":1,"pct":0},{"pubkey":"82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2","name":"","count":1,"pct":0},{"pubkey":"8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e","name":"","count":1,"pct":0}] \ No newline at end of file diff --git a/admin/index.php b/admin/index.php index caacc49..eeb8c44 100644 --- a/admin/index.php +++ b/admin/index.php @@ -41,7 +41,7 @@ $display_name = $relay_version ? ($relay_name . ' ' . $relay_version) : $relay_n C-Relay-PG Admin - + @@ -54,6 +54,7 @@ $display_name = $relay_version ? ($relay_name . ' ' . $relay_version) : $relay_n
  • +
  • @@ -357,54 +358,105 @@ $display_name = $relay_version ? ($relay_name . ' ' . $relay_version) : $relay_n - -