Files
c-relay-pg/admin/public/index.php
T

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 &middot;
<a href="wss://<?= e($_SERVER['HTTP_HOST'] ?? '') ?>">wss://<?= e($_SERVER['HTTP_HOST'] ?? '') ?></a>
&middot; 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>