- New caching_custom_backfill_jobs and caching_custom_backfill_batches tables - Admin API (POST create/cancel, GET list) at admin/api/custom_backfill.php - Admin UI form with preset buttons and job status table on Backfill page - Caching daemon module (custom_backfill.c) processes jobs independently - pg_inbox functions for job/batch claim, progress update, and cancel - Three batch modes: author-batched, id-batched, time-window scan - Fixed done_batches overcount on job completion - Admin config now environment-variable driven (C_RELAY_DB_*) - admin/serve.sh supports separate instances for different databases - make_and_restart_relay.sh only kills relay on target port, not all relays
725 lines
40 KiB
PHP
725 lines
40 KiB
PHP
<?php
|
|
/**
|
|
* admin2 — Full PHP admin for C-Relay-PG.
|
|
* Recreates the original api/ admin interface with a PHP/PDO backend
|
|
* instead of Nostr WebSocket admin commands.
|
|
*/
|
|
require_once __DIR__ . '/lib/helpers.php';
|
|
$pdo = db();
|
|
|
|
// Fetch relay info for the header (from config table)
|
|
$relay_name = '';
|
|
$relay_desc = '';
|
|
$relay_pubkey = '';
|
|
$relay_version = '';
|
|
try {
|
|
$relay_name = $pdo->query("SELECT value FROM config WHERE key = 'relay_name'")->fetchColumn() ?: 'C-Relay-PG';
|
|
$relay_desc = $pdo->query("SELECT value FROM config WHERE key = 'relay_description'")->fetchColumn() ?: '';
|
|
$relay_pubkey = $pdo->query("SELECT value FROM config WHERE key = 'relay_pubkey'")->fetchColumn() ?: '';
|
|
$relay_version = $pdo->query("SELECT value FROM config WHERE key = 'relay_version'")->fetchColumn() ?: '';
|
|
} catch (PDOException $e) {}
|
|
|
|
// Convert relay pubkey (hex) to npub and format into 3 lines of 3 groups of 7 chars
|
|
$relay_npub = $relay_pubkey;
|
|
if ($relay_pubkey && preg_match('/^[0-9a-fA-F]{64}$/', $relay_pubkey)) {
|
|
$relay_npub = hex_to_npub($relay_pubkey);
|
|
}
|
|
// Format npub (63 chars) into 3 lines of "xxxxxxx xxxxxxx xxxxxxx"
|
|
$formatted_npub = $relay_npub;
|
|
if (strlen($relay_npub) === 63) {
|
|
$line1 = substr($relay_npub, 0, 7) . ' ' . substr($relay_npub, 7, 7) . ' ' . substr($relay_npub, 14, 7);
|
|
$line2 = substr($relay_npub, 21, 7) . ' ' . substr($relay_npub, 28, 7) . ' ' . substr($relay_npub, 35, 7);
|
|
$line3 = substr($relay_npub, 42, 7) . ' ' . substr($relay_npub, 49, 7) . ' ' . substr($relay_npub, 56, 7);
|
|
$formatted_npub = $line1 . "\n" . $line2 . "\n" . $line3;
|
|
}
|
|
$display_name = $relay_version ? ($relay_name . ' ' . $relay_version) : $relay_name;
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>C-Relay-PG Admin</title>
|
|
<link rel="stylesheet" href="assets/index.css?v=<?= filemtime(__DIR__ . '/assets/index.css') ?>">
|
|
</head>
|
|
|
|
<body>
|
|
<!-- Side Navigation Menu -->
|
|
<nav class="side-nav" id="side-nav">
|
|
<ul class="nav-menu">
|
|
<li><button class="nav-item" data-page="statistics">Statistics</button></li>
|
|
<li><button class="nav-item" data-page="subscriptions">Subscriptions</button></li>
|
|
<li><button class="nav-item" data-page="configuration">Configuration</button></li>
|
|
<li><button class="nav-item" data-page="authorization">Authorization</button></li>
|
|
<li><button class="nav-item" data-page="ip-bans">IP BAN</button></li>
|
|
<li><button class="nav-item" data-page="relay-events">Relay Events</button></li>
|
|
<li><button class="nav-item" data-page="backfill">Backfill</button></li>
|
|
<li><button class="nav-item" data-page="caching">Caching</button></li>
|
|
<li><button class="nav-item" data-page="dm">DM</button></li>
|
|
<li><button class="nav-item" data-page="cleanup">Cleanup</button></li>
|
|
<li><button class="nav-item" data-page="database">Database Query</button></li>
|
|
</ul>
|
|
<div class="nav-footer">
|
|
<button class="nav-footer-btn" id="nav-dark-mode-btn">DARK MODE</button>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- Side Navigation Overlay -->
|
|
<div class="side-nav-overlay" id="side-nav-overlay"></div>
|
|
|
|
<!-- Header with title and profile display -->
|
|
<div class="section">
|
|
<div class="header-content">
|
|
<div class="header-title clickable" id="header-title" onclick="toggleNav()">
|
|
<span class="relay-letter" data-letter="R">R</span>
|
|
<span class="relay-letter" data-letter="E">E</span>
|
|
<span class="relay-letter" data-letter="L">L</span>
|
|
<span class="relay-letter" data-letter="A">A</span>
|
|
<span class="relay-letter" data-letter="Y">Y</span>
|
|
</div>
|
|
<div class="relay-info">
|
|
<div id="relay-name" class="relay-name"><?= e($display_name) ?></div>
|
|
<div id="relay-description" class="relay-description"><?= e($relay_desc) ?></div>
|
|
<div id="relay-pubkey-container" class="relay-pubkey-container" title="Click to copy npub">
|
|
<div id="relay-pubkey" class="relay-pubkey"><?= e($formatted_npub) ?></div>
|
|
</div>
|
|
</div>
|
|
<div class="profile-area" id="profile-area" style="display: none;">
|
|
<div class="admin-label">admin</div>
|
|
<div class="profile-container">
|
|
<img id="header-user-image" class="header-user-image" alt="Profile" style="display: none;">
|
|
<span id="header-user-name" class="header-user-name">Loading...</span>
|
|
</div>
|
|
<!-- Logout dropdown (under admin picture) -->
|
|
<div class="logout-dropdown" id="logout-dropdown" style="display: none;">
|
|
<button type="button" class="logout-btn" onclick="logout()">LOGOUT</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Login Modal Overlay (nostr_login_lite) -->
|
|
<div id="login-modal" class="login-modal-overlay" style="display: none;">
|
|
<div class="login-modal-content">
|
|
<div id="login-modal-container"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Access Denied Overlay (shown when non-admin user logs in) -->
|
|
<div id="access-denied-overlay" class="access-denied-overlay" style="display: none;">
|
|
<div class="access-denied-content">
|
|
<div class="access-denied-icon">⛔</div>
|
|
<h2>ACCESS DENIED</h2>
|
|
<p class="access-denied-message">This interface is restricted to the relay administrator.</p>
|
|
<p class="access-denied-submessage">The logged-in account does not have admin privileges.</p>
|
|
<button type="button" class="access-denied-logout-btn" onclick="logout()">LOGOUT</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- DATABASE STATISTICS Section -->
|
|
<div class="section flex-section" id="databaseStatisticsSection">
|
|
<div class="section-header">DATABASE STATISTICS</div>
|
|
|
|
<!-- Event Rate Graph — range selector tabs + ASCII chart (server-rendered) -->
|
|
<div class="chart-range-tabs">
|
|
<button class="chart-tab active" data-range="hour">1H</button>
|
|
<button class="chart-tab" data-range="day">1D</button>
|
|
<button class="chart-tab" data-range="month">1M</button>
|
|
<button class="chart-tab" data-range="year">1Y</button>
|
|
</div>
|
|
<div id="event-rate-chart" class="event-rate-chart-container">Loading chart...</div>
|
|
|
|
<!-- Database Overview Table -->
|
|
<div class="input-group">
|
|
<div class="config-table-container">
|
|
<table class="config-table" id="stats-overview-table">
|
|
<thead><tr><th>Metric</th><th>Value</th></tr></thead>
|
|
<tbody id="stats-overview-table-body">
|
|
<tr><td>Database Size</td><td id="db-size">-</td></tr>
|
|
<tr><td>Total Events</td><td id="total-events">-</td></tr>
|
|
<tr><td>Process ID</td><td id="process-id">-</td></tr>
|
|
<tr><td>WebSocket Connections</td><td id="websocket-connections">-</td></tr>
|
|
<tr><td>Active Subscriptions</td><td id="active-subscriptions">-</td></tr>
|
|
<tr><td>Memory Usage</td><td id="memory-usage">-</td></tr>
|
|
<tr><td>CPU Usage</td><td id="cpu-usage">-</td></tr>
|
|
<tr><td>CPU Core</td><td id="cpu-core">-</td></tr>
|
|
<tr><td>Oldest Event</td><td id="oldest-event">-</td></tr>
|
|
<tr><td>Newest Event</td><td id="newest-event">-</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Event Kind Distribution Table -->
|
|
<div class="input-group">
|
|
<label>Event Kind Distribution:</label>
|
|
<div class="config-table-container">
|
|
<table class="config-table" id="stats-kinds-table">
|
|
<thead><tr><th>Event Kind</th><th>Count</th><th>Percentage</th></tr></thead>
|
|
<tbody id="stats-kinds-table-body">
|
|
<tr><td colspan="3" style="text-align: center; font-style: italic;">Loading...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Time-based Statistics Table -->
|
|
<div class="input-group">
|
|
<label>Time-based Statistics:</label>
|
|
<div class="config-table-container">
|
|
<table class="config-table" id="stats-time-table">
|
|
<thead><tr><th>Period</th><th>Events</th></tr></thead>
|
|
<tbody id="stats-time-table-body">
|
|
<tr><td>Last 24 Hours</td><td id="events-24h">-</td></tr>
|
|
<tr><td>Last 7 Days</td><td id="events-7d">-</td></tr>
|
|
<tr><td>Last 30 Days</td><td id="events-30d">-</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Top Pubkeys Table -->
|
|
<div class="input-group">
|
|
<label>Top Pubkeys by Event Count:</label>
|
|
<div class="config-table-container">
|
|
<table class="config-table" id="stats-pubkeys-table">
|
|
<thead><tr><th>Rank</th><th>Name</th><th>Pubkey</th><th>Event Count</th><th>Percentage</th></tr></thead>
|
|
<tbody id="stats-pubkeys-table-body">
|
|
<tr><td colspan="5" style="text-align: center; font-style: italic;">Loading...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Name-field usage stats (from profiles cache) -->
|
|
<div class="input-group" id="name-field-usage" style="display:none;"></div>
|
|
</div>
|
|
|
|
<!-- SUBSCRIPTION DETAILS Section -->
|
|
<div class="section flex-section" id="subscriptionDetailsSection" style="display: none;">
|
|
<div class="section-header">ACTIVE SUBSCRIPTION DETAILS</div>
|
|
<div class="input-group">
|
|
<div class="config-table-container">
|
|
<table class="config-table" id="subscription-details-table">
|
|
<thead><tr><th>Sub ID</th><th>Pubkey</th><th>Filters</th><th>Created</th><th>Events Sent</th></tr></thead>
|
|
<tbody id="subscription-details-table-body">
|
|
<tr><td colspan="5" style="text-align: center; font-style: italic;">Loading...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- CONFIGURATION Section -->
|
|
<div id="div_config" class="section flex-section" style="display: none;">
|
|
<div class="section-header">RELAY CONFIGURATION</div>
|
|
<div id="config-display">
|
|
<div class="config-table-container">
|
|
<table class="config-table" id="config-table">
|
|
<thead><tr><th>Parameter</th><th>Value</th><th>Actions</th></tr></thead>
|
|
<tbody id="config-table-body">
|
|
<tr><td colspan="3" style="text-align: center; font-style: italic;">Loading...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="inline-buttons">
|
|
<button type="button" id="fetch-config-btn" onclick="loadConfig()">REFRESH</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- AUTH RULES Section -->
|
|
<div class="section flex-section" id="authRulesSection" style="display: none;">
|
|
<div class="section-header">AUTH RULES MANAGEMENT</div>
|
|
<div id="authRulesTableContainer">
|
|
<table class="config-table" id="authRulesTable">
|
|
<thead><tr><th>Rule Type</th><th>Pattern Type</th><th>Pattern Value</th><th>Status</th><th>Actions</th></tr></thead>
|
|
<tbody id="authRulesTableBody">
|
|
<tr><td colspan="5" style="text-align: center; font-style: italic;">Loading...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div id="authRuleInputSections" style="display: block;">
|
|
<div class="input-group">
|
|
<label for="authRulePubkey">Public Key (npub or hex):</label>
|
|
<input type="text" id="authRulePubkey" placeholder="npub1... or 64-character hex pubkey">
|
|
</div>
|
|
<div class="inline-buttons">
|
|
<button type="button" onclick="addAuthRule('whitelist')">ADD TO WHITELIST</button>
|
|
<button type="button" onclick="addAuthRule('blacklist')">ADD TO BLACKLIST</button>
|
|
<button type="button" onclick="loadAuthRules()">REFRESH</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- WEB OF TRUST Section -->
|
|
<div class="section flex-section" id="wotSection" style="display: none;">
|
|
<div class="section-header">WEB OF TRUST</div>
|
|
<div id="wotKind3Status" class="wot-status-row">
|
|
<span>Admin Contact List (kind 3):</span>
|
|
<span id="wotKind3Indicator" class="wot-indicator wot-unknown">Loading...</span>
|
|
</div>
|
|
<div class="wot-level-selector">
|
|
<label>WoT Level:</label>
|
|
<div class="inline-buttons">
|
|
<button type="button" class="wot-level-btn" onclick="setWotLevel(0)">OFF</button>
|
|
<button type="button" class="wot-level-btn" onclick="setWotLevel(1)">WRITE ONLY</button>
|
|
<button type="button" class="wot-level-btn" onclick="setWotLevel(2)">FULL</button>
|
|
</div>
|
|
<div class="wot-level-description" id="wotLevelDescription">Loading...</div>
|
|
</div>
|
|
<div class="wot-stats-row">
|
|
<span>Whitelisted Pubkeys:</span>
|
|
<span id="wotWhitelistCount">—</span>
|
|
</div>
|
|
<div class="inline-buttons">
|
|
<button type="button" onclick="syncWot()">SYNC FROM KIND 3</button>
|
|
<button type="button" onclick="loadWotStatus()">REFRESH STATUS</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- NIP-17 DIRECT MESSAGES Section -->
|
|
<div class="section" id="nip17DMSection" style="display: none;">
|
|
<div class="section-header"><h2>NIP-17 DIRECT MESSAGES</h2></div>
|
|
<div class="input-group">
|
|
<label>Received Messages (kind 4/14/15):</label>
|
|
<div id="dm-inbox" class="log-panel" style="height: 300px; overflow-y: auto;">
|
|
<div class="log-entry">Loading...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- IP BANS Section -->
|
|
<div class="section" id="ipBansSection" style="display: none;">
|
|
<div class="section-header">IP BAN MANAGEMENT</div>
|
|
<div class="input-group">
|
|
<div class="config-table-container">
|
|
<table class="config-table" id="ip-bans-stats-table">
|
|
<thead><tr><th>Total IPs Tracked</th><th>Currently Banned</th><th>Total Bans Issued</th></tr></thead>
|
|
<tbody>
|
|
<tr><td id="ip-bans-total">-</td><td id="ip-bans-active">-</td><td id="ip-bans-issued">-</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="input-group">
|
|
<h3>Manually Ban IP Address</h3>
|
|
<div class="form-group">
|
|
<label for="ban-ip-input">IP Address:</label>
|
|
<input type="text" id="ban-ip-input" placeholder="192.168.1.100">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ban-duration-select">Ban Duration:</label>
|
|
<select id="ban-duration-select">
|
|
<option value="3600">1 Hour</option>
|
|
<option value="86400" selected>24 Hours</option>
|
|
<option value="604800">7 Days</option>
|
|
<option value="2592000">30 Days</option>
|
|
<option value="31536000">1 Year</option>
|
|
<option value="999999999">Permanent</option>
|
|
</select>
|
|
</div>
|
|
<div class="inline-buttons">
|
|
<button type="button" onclick="addBan()">BAN IP</button>
|
|
<button type="button" onclick="loadIpBans()">REFRESH</button>
|
|
</div>
|
|
<div id="add-ban-status" class="status-message"></div>
|
|
</div>
|
|
<div class="input-group">
|
|
<label>Banned IP Addresses:</label>
|
|
<div class="config-table-container">
|
|
<table class="config-table" id="ip-bans-table">
|
|
<thead><tr><th>IP Address</th><th>Status</th><th>Banned Until</th><th>Failures</th><th>Actions</th></tr></thead>
|
|
<tbody id="ip-bans-tbody">
|
|
<tr><td colspan="5" style="text-align: center;">Loading...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- RELAY EVENTS Section -->
|
|
<div class="section" id="relayEventsSection" style="display: none;">
|
|
<div class="section-header">RELAY EVENTS MANAGEMENT</div>
|
|
<div class="input-group">
|
|
<h3>Recent Events</h3>
|
|
<div class="inline-buttons">
|
|
<button type="button" onclick="loadEvents()">REFRESH</button>
|
|
</div>
|
|
<div class="config-table-container">
|
|
<table class="config-table" id="live-relay-events-table">
|
|
<thead><tr><th>Time</th><th>Kind</th><th>Name</th><th>ID</th><th>Content Preview</th></tr></thead>
|
|
<tbody id="live-relay-events-table-body">
|
|
<tr><td colspan="5" style="text-align: center;">Loading...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- BACKFILL Section -->
|
|
<div class="section" id="backfillSection" style="display: none;">
|
|
<div class="section-header">BACKFILL</div>
|
|
<div class="input-group">
|
|
<div id="backfill-controls" class="inline-buttons">
|
|
<button type="button" class="process-toggle-btn" id="backfill-toggle-btn" onclick="toggleBackfillEnabled()">Backfill status: loading…</button>
|
|
</div>
|
|
</div>
|
|
<div class="input-group" id="backfill-relay-group" style="display:none">
|
|
<h3>Upstream Relays <span style="font-size:11px;font-weight:normal;color:var(--text-muted,#888)">(live connection state, updated every 15s)</span></h3>
|
|
<div id="backfill-relay-status" class="status-display">
|
|
<p>Loading...</p>
|
|
</div>
|
|
<div class="inline-buttons" style="margin-top:8px">
|
|
<button type="button" id="backfill-rerun-all-btn" onclick="rerunAllBackfill()">Re-run All Backfill</button>
|
|
</div>
|
|
</div>
|
|
<div class="input-group">
|
|
<h3>Followed Pubkeys <span style="font-size:11px;font-weight:normal;color:var(--text-muted,#888)">(click a row for per-relay details)</span></h3>
|
|
<div id="backfill-follows-status" class="status-message"></div>
|
|
<div id="backfill-follows-pagination" class="pagination-bar"></div>
|
|
<div class="config-table-container">
|
|
<table class="config-table" id="backfill-follows-table">
|
|
<thead><tr><th>Name</th><th>npub</th><th>Root?</th><th>Events in DB</th><th>Backfill</th><th>Relays</th></tr></thead>
|
|
<tbody id="backfill-follows-table-body">
|
|
<tr><td colspan="6" style="text-align: center; font-style: italic;">Loading...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="input-group">
|
|
<h3>Custom Backfill <span style="font-size:11px;font-weight:normal;color:var(--text-muted,#888)">(ad-hoc backfill with arbitrary NIP-01 filters)</span></h3>
|
|
<div class="inline-buttons" style="margin-bottom:8px">
|
|
<button type="button" onclick="presetCustomBackfill('missing_profiles_kind1')">↻ Missing Profiles (kind-1 authors)</button>
|
|
<button type="button" onclick="presetCustomBackfill('missing_profiles_all')">↻ Missing Profiles (all pubkeys)</button>
|
|
<button type="button" onclick="presetCustomBackfill('kind0_daterange')">↻ Kind-0 Date Range</button>
|
|
<button type="button" onclick="presetCustomBackfill('relay_lists')">↻ Relay Lists (10002)</button>
|
|
<button type="button" onclick="presetCustomBackfill('contact_lists')">↻ Contact Lists (3)</button>
|
|
</div>
|
|
<div class="config-form">
|
|
<div class="form-row">
|
|
<label>Label</label>
|
|
<input type="text" id="cb-label" placeholder="e.g. Missing profiles" style="flex:1">
|
|
</div>
|
|
<div class="form-row">
|
|
<label>Kinds <span style="font-size:10px;color:var(--text-muted,#888)">(CSV: 0 or 0,1,3)</span></label>
|
|
<input type="text" id="cb-kinds" placeholder="0" style="flex:1">
|
|
</div>
|
|
<div class="form-row">
|
|
<label>Authors source</label>
|
|
<select id="cb-authors-source" onchange="toggleCustomBackfillFields()">
|
|
<option value="none">None (time-window scan)</option>
|
|
<option value="manual">Manual list</option>
|
|
<option value="all_in_db">All pubkeys in DB</option>
|
|
<option value="kind1_authors">Kind-1 authors only</option>
|
|
<option value="followed">Followed set</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-row" id="cb-authors-list-row" style="display:none">
|
|
<label>Authors list <span style="font-size:10px;color:var(--text-muted,#888)">(hex pubkeys, one per line)</span></label>
|
|
<textarea id="cb-authors-list" rows="3" placeholder="hex pubkey hex pubkey" style="flex:1"></textarea>
|
|
</div>
|
|
<div class="form-row" id="cb-missing-kind0-row">
|
|
<label><input type="checkbox" id="cb-missing-kind0"> Only authors missing kind-0</label>
|
|
</div>
|
|
<div class="form-row">
|
|
<label>Event IDs <span style="font-size:10px;color:var(--text-muted,#888)">(hex IDs, one per line; alternative to authors)</span></label>
|
|
<textarea id="cb-ids-list" rows="2" placeholder="hex event id" style="flex:1"></textarea>
|
|
</div>
|
|
<div class="form-row">
|
|
<label>Since</label>
|
|
<input type="datetime-local" id="cb-since">
|
|
<label style="margin-left:8px">Until</label>
|
|
<input type="datetime-local" id="cb-until">
|
|
</div>
|
|
<div class="form-row">
|
|
<label>Limit <span style="font-size:10px;color:var(--text-muted,#888)">(max events per REQ, 0=no limit)</span></label>
|
|
<input type="number" id="cb-limit" value="0" min="0" style="width:80px">
|
|
</div>
|
|
<div class="form-row">
|
|
<label>Tag filters <span style="font-size:10px;color:var(--text-muted,#888)">(JSON: {"#t":["nostr"]})</span></label>
|
|
<input type="text" id="cb-tag-filters" placeholder='{"#t":["nostr"]}' style="flex:1">
|
|
</div>
|
|
<div class="form-row">
|
|
<label>Batch size <span style="font-size:10px;color:var(--text-muted,#888)">(authors/IDs per REQ)</span></label>
|
|
<input type="number" id="cb-batch-size" value="100" min="1" style="width:80px">
|
|
</div>
|
|
<div class="form-row">
|
|
<label>Relay mode</label>
|
|
<select id="cb-relay-mode" onchange="toggleCustomBackfillFields()">
|
|
<option value="bootstrap">Bootstrap relays</option>
|
|
<option value="outbox">Outbox relays (per-author)</option>
|
|
<option value="custom">Custom list</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-row" id="cb-relay-list-row" style="display:none">
|
|
<label>Custom relays <span style="font-size:10px;color:var(--text-muted,#888)">(wss:// URLs, one per line)</span></label>
|
|
<textarea id="cb-relay-list" rows="3" placeholder="wss://relay.damus.io" style="flex:1"></textarea>
|
|
</div>
|
|
<div class="inline-buttons" style="margin-top:8px">
|
|
<button type="button" id="cb-submit-btn" onclick="submitCustomBackfill()">Create Backfill Job</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="input-group">
|
|
<h3>Custom Backfill Jobs</h3>
|
|
<div id="custom-backfill-jobs-status" class="status-message"></div>
|
|
<div class="config-table-container">
|
|
<table class="config-table" id="custom-backfill-jobs-table">
|
|
<thead><tr><th>Label</th><th>Filter</th><th>Mode</th><th>Batches</th><th>Status</th><th>Events</th><th>Started</th><th>Actions</th></tr></thead>
|
|
<tbody id="custom-backfill-jobs-table-body">
|
|
<tr><td colspan="8" style="text-align:center;font-style:italic;">No jobs</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="inline-buttons">
|
|
<button type="button" onclick="loadBackfill()">REFRESH</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- CACHING Section (Live Subscription Design) -->
|
|
<div class="section" id="cachingSection" style="display: none;">
|
|
<div class="section-header">CACHING (LIVE SUBSCRIPTION)</div>
|
|
<div class="input-group">
|
|
<div id="caching-controls" class="inline-buttons">
|
|
<button type="button" class="process-toggle-btn" id="caching-live-toggle-btn" onclick="toggleCachingLiveEnabled()">Caching status: loading…</button>
|
|
</div>
|
|
</div>
|
|
<div class="input-group">
|
|
<h3>Subscription Design</h3>
|
|
<p style="font-size:12px;color:var(--text-muted,#888);margin:0 0 12px 0">
|
|
Configure the Nostr subscription filter sent to upstream relays for live event streaming.
|
|
</p>
|
|
<div class="form-group">
|
|
<label>Strategy:</label>
|
|
<div class="radio-group" id="caching-strategy-group">
|
|
<label><input type="radio" name="caching-strategy" value="whitelist" onchange="onCachingStrategyChange()"> Whitelist Follows</label>
|
|
<label><input type="radio" name="caching-strategy" value="cache_all" onchange="onCachingStrategyChange()"> Cache Everything</label>
|
|
</div>
|
|
<div id="caching-strategy-note" style="font-size:11px;color:var(--text-muted,#888);margin-top:4px">
|
|
Whitelist: only events from followed pubkeys. Cache Everything: all events on the relay.
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="caching-kinds">Event Kinds (comma-separated, empty = all):</label>
|
|
<input type="text" id="caching-kinds" placeholder="e.g. 1, 7, 9734" style="width:100%;max-width:400px">
|
|
<div style="font-size:11px;color:var(--text-muted,#888);margin-top:2px">
|
|
Common kinds: 0 (profiles), 1 (text), 3 (contacts), 4 (DM), 5 (delete),
|
|
6 (repost), 7 (reaction), 9734 (zap request), 9735 (zap receipt), 10002 (relay list)
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="caching-since">Since lookback (seconds before now):</label>
|
|
<input type="number" id="caching-since" value="0" min="0" style="width:120px">
|
|
<span style="font-size:11px;color:var(--text-muted,#888);margin-left:8px">0 = no lookback (since=now)</span>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="caching-limit">Max events per response (0 = no limit):</label>
|
|
<input type="number" id="caching-limit" value="0" min="0" style="width:120px">
|
|
</div>
|
|
</div>
|
|
<div class="input-group">
|
|
<h3>Subscription Filter Preview</h3>
|
|
<pre id="caching-filter-preview" style="background:var(--bg-code,#f5f5f5);padding:8px;border-radius:4px;overflow-x:auto;font-size:11px;margin-top:4px;white-space:pre-wrap;border:1px solid var(--border,#ddd);min-height:60px">{
|
|
"kinds": [1, 7, 9734],
|
|
"since": 1234567890
|
|
}</pre>
|
|
<div class="inline-buttons" style="margin-top:12px">
|
|
<button type="button" onclick="saveCachingConfig()">Save Configuration</button>
|
|
<button type="button" onclick="loadCaching()">Refresh</button>
|
|
</div>
|
|
<div id="caching-save-status" class="status-message" style="margin-top:8px"></div>
|
|
</div>
|
|
<div class="input-group">
|
|
<h3>Live Subscription Status</h3>
|
|
<div id="caching-live-status" class="status-display">
|
|
<p>Loading...</p>
|
|
</div>
|
|
</div>
|
|
<div class="input-group">
|
|
<h3>Relay Selection <span style="font-size:11px;font-weight:normal;color:var(--text-muted,#888)">(check Live or Backfill to enable; changes take effect immediately)</span></h3>
|
|
<div id="caching-relay-selection" class="status-display">
|
|
<p>Loading...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- EVENT CLEANUP Section -->
|
|
<div class="section" id="cleanupSection" style="display: none;">
|
|
<div class="section-header"><h2>EVENT CLEANUP</h2></div>
|
|
|
|
<!-- Saved Queries Table -->
|
|
<div class="input-group" id="saved-queries-group">
|
|
<label>Saved Queries</label>
|
|
<div class="config-table-container">
|
|
<table class="config-table" id="cleanup-saved-queries-table">
|
|
<thead><tr><th>Name</th><th>Filters</th><th>Last Preview</th><th>Last Executed</th><th>Actions</th></tr></thead>
|
|
<tbody id="cleanup-saved-queries-body">
|
|
<tr><td colspan="5" style="text-align: center; font-style: italic;">Loading...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="inline-buttons" style="margin-top:8px">
|
|
<button type="button" onclick="newCleanupQuery()">New Query</button>
|
|
<button type="button" onclick="loadCleanupQueries()">Refresh</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Query Builder -->
|
|
<div class="input-group" id="cleanup-builder-group">
|
|
<label>Query Builder</label>
|
|
<div class="form-group">
|
|
<label for="cleanup-name">Query Name:</label>
|
|
<input type="text" id="cleanup-name" placeholder="My Cleanup Query" style="width:100%;max-width:400px">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Pubkey Filter:</label>
|
|
<div class="radio-group">
|
|
<label><input type="radio" name="cleanup-follows" value="all" checked> All</label>
|
|
<label><input type="radio" name="cleanup-follows" value="roots"> Roots only (admins)</label>
|
|
<label><input type="radio" name="cleanup-follows" value="follows_no_roots"> Follows (non-root)</label>
|
|
<label><input type="radio" name="cleanup-follows" value="follows"> Follows (incl. roots)</label>
|
|
<label><input type="radio" name="cleanup-follows" value="non_follows"> Non-follows</label>
|
|
</div>
|
|
<div style="font-size:11px;color:var(--text-muted,#888);margin-top:2px">
|
|
Roots = your admin/root npubs. Follows = pubkeys your roots follow.
|
|
Non-follows = pubkeys not in the caching follow graph at all.
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pubkey list (roots + follows) with display names -->
|
|
<div class="form-group">
|
|
<label>
|
|
<span style="cursor:pointer" onclick="toggleCleanupPubkeyList()">
|
|
<span id="cleanup-pubkey-list-arrow">▶</span> Follow Graph Pubkeys
|
|
</span>
|
|
<span id="cleanup-pubkey-counts" style="font-size:11px;margin-left:8px;color:var(--text-muted,#888)"></span>
|
|
</label>
|
|
<div id="cleanup-pubkey-list" style="display:none;margin-top:6px">
|
|
<div class="radio-group" style="margin-bottom:6px">
|
|
<label><input type="radio" name="cleanup-pubkey-mode" value="ignore" checked> Ignore checkboxes</label>
|
|
<label><input type="radio" name="cleanup-pubkey-mode" value="include"> Include only checked</label>
|
|
<label><input type="radio" name="cleanup-pubkey-mode" value="exclude"> Exclude checked</label>
|
|
</div>
|
|
<div style="font-size:11px;color:var(--text-muted,#888);margin-bottom:6px">
|
|
Check pubkeys below, then pick a mode. "Include only checked" overrides the bucket filter
|
|
(clean exactly those). "Exclude checked" subtracts them from the bucket filter
|
|
(clean the bucket but spare these). The SQL box updates live.
|
|
</div>
|
|
<div style="display:flex;gap:12px;flex-wrap:wrap;align-items:flex-start">
|
|
<div style="flex:1;min-width:280px">
|
|
<div style="font-size:12px;font-weight:bold;margin-bottom:4px">Roots (admins)</div>
|
|
<div id="cleanup-roots-list" class="config-table-container" style="max-height:240px;overflow:auto"></div>
|
|
</div>
|
|
<div style="flex:1;min-width:280px">
|
|
<div style="font-size:12px;font-weight:bold;margin-bottom:4px">Follows (non-root)</div>
|
|
<div id="cleanup-follows-list" class="config-table-container" style="max-height:240px;overflow:auto"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="cleanup-kinds">Event Kinds (comma-separated, empty = all):</label>
|
|
<input type="text" id="cleanup-kinds" placeholder="e.g. 1, 7, 9734" style="width:100%;max-width:400px">
|
|
<div style="font-size:11px;color:var(--text-muted,#888);margin-top:2px">
|
|
Common kinds: 0 (profiles), 1 (text), 3 (contacts), 4 (DM), 5 (delete),
|
|
6 (repost), 7 (reaction), 9734 (zap request), 9735 (zap receipt), 10002 (relay list)
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>From (delete events after this):</label>
|
|
<div class="datetime-group">
|
|
<input type="date" id="cleanup-from-date" class="cleanup-date-input">
|
|
<input type="time" id="cleanup-from-time" class="cleanup-time-input">
|
|
<span class="datetime-hint">leave empty for no bound</span>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>To (delete events before this):</label>
|
|
<div class="datetime-group">
|
|
<input type="date" id="cleanup-to-date" class="cleanup-date-input">
|
|
<input type="time" id="cleanup-to-time" class="cleanup-time-input">
|
|
<span class="datetime-hint">leave empty for no bound</span>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="cleanup-max-events">Max Events (0 = no limit):</label>
|
|
<input type="number" id="cleanup-max-events" value="0" min="0" style="width:120px">
|
|
</div>
|
|
|
|
<!-- SQL box — live, editable, validated. Border = red when invalid, black when valid. -->
|
|
<div class="form-group" id="cleanup-sql-box-group">
|
|
<label for="cleanup-sql-box">SQL Query
|
|
<span id="cleanup-sql-status" style="font-size:11px;margin-left:8px;font-weight:normal"></span>
|
|
</label>
|
|
<textarea id="cleanup-sql-box" rows="10" spellcheck="false"
|
|
style="width:100%;font-family:monospace;font-size:12px;
|
|
border:2px solid #000;border-radius:4px;padding:8px;
|
|
white-space:pre;overflow:auto;resize:vertical"></textarea>
|
|
<div style="font-size:11px;color:var(--text-muted,#888);margin-top:2px">
|
|
This box shows the query built from your filters and is editable.
|
|
The border turns <span style="color:#c0392b;font-weight:bold">red</span> if the SQL is invalid,
|
|
<span style="font-weight:bold">black</span> when valid. Preview runs the edited SELECT;
|
|
Execute Delete extracts your WHERE clause and runs it inside a safe
|
|
<code>DELETE ... id IN (subquery)</code> template.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="inline-buttons" style="margin-top:8px">
|
|
<button type="button" onclick="previewCleanup()">Preview</button>
|
|
<button type="button" onclick="saveCleanupQuery()">Save Query</button>
|
|
<button type="button" onclick="executeCleanup()" style="background:#c0392b;color:#fff">Execute Delete</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Results Area -->
|
|
<div class="input-group" id="cleanup-results-group" style="display:none;">
|
|
<label>Results</label>
|
|
<div id="cleanup-results-summary" class="info-box"></div>
|
|
<div id="cleanup-results-breakdown" class="config-table-container" style="margin-top:8px"></div>
|
|
</div>
|
|
|
|
<!-- Confirmation Dialog -->
|
|
<div id="cleanup-confirm-dialog" class="modal-overlay" style="display:none;">
|
|
<div class="modal-content" style="background:var(--bg-card,#fff);border:1px solid var(--border,#ccc);border-radius:8px;padding:20px;max-width:480px;margin:10% auto;box-shadow:0 4px 20px rgba(0,0,0,0.3)">
|
|
<h3 style="margin-top:0">Confirm Delete</h3>
|
|
<p id="cleanup-confirm-msg">Are you sure you want to delete these events?</p>
|
|
<p style="color:#c0392b;font-weight:bold">This action cannot be undone.</p>
|
|
<div class="inline-buttons" style="margin-top:12px">
|
|
<button type="button" id="cleanup-confirm-btn" onclick="executeCleanupConfirmed()" style="background:#c0392b;color:#fff">Delete</button>
|
|
<button type="button" onclick="document.getElementById('cleanup-confirm-dialog').style.display='none'">Cancel</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- SQL QUERY Section -->
|
|
<div class="section" id="sqlQuerySection" style="display: none;">
|
|
<div class="section-header"><h2>SQL QUERY CONSOLE</h2></div>
|
|
<div class="input-group">
|
|
<label for="sql-input">SQL Query (SELECT only):</label>
|
|
<textarea id="sql-input" rows="5" placeholder="SELECT * FROM events LIMIT 10"></textarea>
|
|
</div>
|
|
<div class="input-group">
|
|
<div class="inline-buttons">
|
|
<button type="button" onclick="executeQuery()">EXECUTE QUERY</button>
|
|
<button type="button" onclick="document.getElementById('sql-input').value=''">CLEAR</button>
|
|
</div>
|
|
</div>
|
|
<div class="input-group">
|
|
<label>Query Results:</label>
|
|
<div id="query-info" class="info-box"></div>
|
|
<div id="query-table" class="config-table-container"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Nostr libraries for nostr_login_lite auth modal -->
|
|
<script src="assets/nostr.bundle.js"></script>
|
|
<script src="assets/nostr-lite.js"></script>
|
|
<script src="assets/app.js"></script>
|
|
</body>
|
|
|
|
</html>
|