181 lines
7.6 KiB
PHP
181 lines
7.6 KiB
PHP
<?php
|
|
/**
|
|
* admin/public/index.php — Public relay stats page.
|
|
*
|
|
* Served at https://<domain>/relay/stats/ (no authentication required).
|
|
*
|
|
* Shows the relay name/description/pubkey (read-only, from the config table)
|
|
* and the server-rendered ASCII event-rate chart. The chart is fetched from
|
|
* the public endpoint at /relay/api/chart.php?range=...
|
|
*
|
|
* This page exposes ONLY read-only, non-sensitive information:
|
|
* - relay_name, relay_description, relay_pubkey, relay_version (from config)
|
|
* - the event-rate chart (aggregate COUNT query, no user data)
|
|
*
|
|
* All sensitive admin endpoints (config edits, auth rules, IP bans, DMs,
|
|
* raw SQL queries) remain behind Basic Auth at /relay/admin/.
|
|
*/
|
|
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);
|
|
}
|
|
$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><?= e($display_name) ?></title>
|
|
<link rel="stylesheet" href="/relay/assets/index.css">
|
|
<style>
|
|
/* Public page uses the admin theme but hides the nav and centers content. */
|
|
.side-nav, .side-nav-overlay { display: none !important; }
|
|
.public-wrap { max-width: 1100px; margin: 0 auto; padding: 20px; }
|
|
.public-chart-box {
|
|
background: var(--secondary-color);
|
|
border: var(--border-width) solid var(--border-color);
|
|
border-radius: var(--border-radius);
|
|
padding: 12px;
|
|
margin-top: 16px;
|
|
}
|
|
.public-footer {
|
|
margin-top: 24px;
|
|
padding-top: 12px;
|
|
border-top: var(--border-width) solid var(--border-color);
|
|
font-size: 12px;
|
|
color: var(--muted-color);
|
|
text-align: center;
|
|
}
|
|
.public-footer a { color: var(--accent-color); text-decoration: none; }
|
|
.event-rate-chart-container {
|
|
white-space: pre;
|
|
font-family: var(--font-family);
|
|
font-size: 12px;
|
|
line-height: 1.2;
|
|
overflow-x: auto;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<!-- Header with title and relay info (read-only) -->
|
|
<div class="section">
|
|
<div class="header-content">
|
|
<div class="header-title">
|
|
<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>
|
|
</div>
|
|
|
|
<div class="public-wrap">
|
|
<div class="section-header">EVENT RATE</div>
|
|
|
|
<!-- Range selector tabs -->
|
|
<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>
|
|
|
|
<!-- Server-rendered ASCII chart (fetched from the public chart endpoint) -->
|
|
<div class="public-chart-box">
|
|
<div id="event-rate-chart" class="event-rate-chart-container">Loading chart...</div>
|
|
</div>
|
|
|
|
<div class="public-footer">
|
|
Public relay statistics ·
|
|
<a href="wss://<?= e($_SERVER['HTTP_HOST'] ?? '') ?>">wss://<?= e($_SERVER['HTTP_HOST'] ?? '') ?></a>
|
|
· Admin? visit <a href="/relay/admin/">/relay/admin/</a>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Minimal chart loader — fetches the public ASCII chart endpoint and
|
|
// injects the plain-text response into the chart container. No auth.
|
|
const chartEl = document.getElementById('event-rate-chart');
|
|
let currentRange = 'hour';
|
|
let chartTimer = null;
|
|
|
|
async function loadChart(range) {
|
|
currentRange = range;
|
|
if (!chartEl) return;
|
|
chartEl.textContent = 'Loading chart...';
|
|
try {
|
|
const res = await fetch('/relay/api/chart.php?range=' + encodeURIComponent(range));
|
|
if (!res.ok) { chartEl.textContent = 'Chart load failed (HTTP ' + res.status + ')'; return; }
|
|
const text = await res.text();
|
|
chartEl.textContent = text || '(no data)';
|
|
} catch (e) {
|
|
chartEl.textContent = 'Chart load failed: ' + e.message;
|
|
}
|
|
}
|
|
|
|
document.querySelectorAll('.chart-tab').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
document.querySelectorAll('.chart-tab').forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
const range = btn.getAttribute('data-range');
|
|
loadChart(range);
|
|
// Hour chart is live — auto-refresh every 30s. Others are cached server-side.
|
|
if (chartTimer) { clearInterval(chartTimer); chartTimer = null; }
|
|
if (range === 'hour') {
|
|
chartTimer = setInterval(() => loadChart(currentRange), 30000);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Initial load + live refresh for the default (hour) range.
|
|
loadChart('hour');
|
|
chartTimer = setInterval(() => loadChart(currentRange), 30000);
|
|
|
|
// Click-to-copy the relay npub.
|
|
const pkContainer = document.getElementById('relay-pubkey-container');
|
|
if (pkContainer) {
|
|
pkContainer.style.cursor = 'pointer';
|
|
pkContainer.addEventListener('click', () => {
|
|
const txt = (document.getElementById('relay-pubkey').textContent || '').replace(/\s+/g, '');
|
|
if (txt && navigator.clipboard) navigator.clipboard.writeText(txt);
|
|
});
|
|
}
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|