v0.1.16 - Got the admin page kinda working

This commit is contained in:
Your Name
2025-12-12 16:41:38 -04:00
parent d0bf851e86
commit fe27b5e41a
13 changed files with 22305 additions and 9425 deletions

View File

@@ -136,7 +136,7 @@ async function fetchRelayInfo(relayUrl) {
method: 'GET',
headers: {
'Accept': 'application/nostr+json',
'User-Agent': 'C-Relay-Admin-API/1.0'
'User-Agent': 'Blossom-Admin-API/1.0'
},
timeout: 10000 // 10 second timeout
});
@@ -511,10 +511,8 @@ function updateAdminSectionsVisibility() {
function loadCurrentPageData() {
switch (currentPage) {
case 'statistics':
// Load statistics immediately (no auto-refresh - using real-time monitoring events)
sendStatsQuery().catch(error => {
console.log('Auto-fetch statistics failed: ' + error.message);
});
// Start HTTP polling for statistics (polls every 10 seconds)
startStatsPolling();
break;
case 'configuration':
// Load configuration
@@ -1311,7 +1309,7 @@ function initializeEventRateChart() {
eventRateChart = new ASCIIBarChart('event-rate-chart', {
maxHeight: 11, // Chart height in lines
maxDataPoints: 76, // Show last 76 bins (5+ minutes of history)
title: 'New Events', // Chart title
title: 'New Blobs', // Chart title
xAxisLabel: '', // No X-axis label
yAxisLabel: '', // No Y-axis label
autoFitWidth: true, // Enable responsive font sizing
@@ -3298,7 +3296,7 @@ async function testPostEvent() {
["t", "test"],
["client", "c-relay-admin-api"]
],
content: `Test event from C-Relay Admin API at ${new Date().toISOString()}`
content: `Test event from Blossom Admin API at ${new Date().toISOString()}`
};
logTestEvent('SENT', `Test event (before signing): ${JSON.stringify(testEvent)}`, 'EVENT');
@@ -3642,8 +3640,8 @@ function updateRelayInfoInHeader() {
// Get relay info from NIP-11 data or use defaults
const relayInfo = getRelayInfo();
const relayName = relayInfo.name || 'C-Relay';
const relayDescription = relayInfo.description || 'Nostr Relay';
const relayName = relayInfo.name || 'Blossom';
const relayDescription = relayInfo.description || 'Blob Storage Server';
// Convert relay pubkey to npub
let relayNpub = 'Loading...';
@@ -3682,8 +3680,8 @@ function getRelayInfo() {
// Default values
return {
name: 'C-Relay',
description: 'Nostr Relay',
name: 'Blossom',
description: 'Blob Storage Server',
pubkey: relayPubkey
};
}
@@ -3692,17 +3690,17 @@ function getRelayInfo() {
function updateStoredRelayInfo(configData) {
if (configData && configData.data) {
// Extract relay info from config data - handle both object and array formats
let relayName = 'C-Relay';
let relayDescription = 'Nostr Relay';
let relayName = 'Blossom';
let relayDescription = 'Blob Storage Server';
if (Array.isArray(configData.data)) {
// Array format: [{key: 'x', value: 'y'}, ...]
relayName = configData.data.find(item => item.key === 'relay_name')?.value || 'C-Relay';
relayDescription = configData.data.find(item => item.key === 'relay_description')?.value || 'Nostr Relay';
relayName = configData.data.find(item => item.key === 'relay_name')?.value || 'Blossom';
relayDescription = configData.data.find(item => item.key === 'relay_description')?.value || 'Blob Storage Server';
} else {
// Object format: {key1: 'value1', key2: 'value2', ...}
relayName = configData.data.relay_name || 'C-Relay';
relayDescription = configData.data.relay_description || 'Nostr Relay';
relayName = configData.data.relay_name || 'Blossom';
relayDescription = configData.data.relay_description || 'Blob Storage Server';
}
relayInfoData = {
@@ -3837,7 +3835,7 @@ async function sendRestartCommand() {
}
}
// Send stats_query command to get database statistics using Administrator API (inner events)
// Send query_view commands to get database statistics via HTTP POST
async function sendStatsQuery() {
if (!isLoggedIn || !userPubkey) {
log('Must be logged in to query database statistics', 'ERROR');
@@ -3845,74 +3843,81 @@ async function sendStatsQuery() {
return;
}
if (!relayPool) {
log('SimplePool connection not available', 'ERROR');
updateStatsStatus('error', 'No relay connection');
return;
}
try {
updateStatsStatus('loading', 'Querying database...');
// Create command array for stats query
const command_array = ["stats_query", "all"];
// Query blob_overview view for basic stats
const overviewData = await sendAdminCommandHTTP(['query_view', 'blob_overview']);
handleViewQueryResponse('blob_overview', overviewData);
// Encrypt the command array directly using NIP-44
const encrypted_content = await encryptForRelay(JSON.stringify(command_array));
if (!encrypted_content) {
throw new Error('Failed to encrypt command array');
}
// Query blob_type_distribution view
const typeData = await sendAdminCommandHTTP(['query_view', 'blob_type_distribution']);
handleViewQueryResponse('blob_type_distribution', typeData);
// Create single kind 23456 admin event
const statsEvent = {
kind: 23456,
pubkey: userPubkey,
created_at: Math.floor(Date.now() / 1000),
tags: [["p", getRelayPubkey()]],
content: encrypted_content
};
// Query blob_time_stats view
const timeData = await sendAdminCommandHTTP(['query_view', 'blob_time_stats']);
handleViewQueryResponse('blob_time_stats', timeData);
// Sign the event
const signedEvent = await window.nostr.signEvent(statsEvent);
if (!signedEvent || !signedEvent.sig) {
throw new Error('Event signing failed');
}
log('Sending stats query command...', 'INFO');
// Publish via SimplePool
const url = relayConnectionUrl.value.trim();
const publishPromises = relayPool.publish([url], signedEvent);
// Use Promise.allSettled to capture per-relay outcomes
const results = await Promise.allSettled(publishPromises);
// Check if any relay accepted the event
let successCount = 0;
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
successCount++;
log(`Stats query published successfully to relay ${index}`, 'INFO');
} else {
log(`Stats query failed on relay ${index}: ${result.reason?.message || result.reason}`, 'ERROR');
}
});
if (successCount === 0) {
const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; ');
throw new Error(`All relays rejected stats query event. Details: ${errorDetails}`);
}
log('Stats query command sent successfully - waiting for response...', 'INFO');
updateStatsStatus('waiting', 'Waiting for response...');
// Query top_uploaders view
const uploadersData = await sendAdminCommandHTTP(['query_view', 'top_uploaders']);
handleViewQueryResponse('top_uploaders', uploadersData);
log('All view queries completed successfully', 'INFO');
updateStatsStatus('loaded');
} catch (error) {
log(`Failed to send stats query: ${error.message}`, 'ERROR');
log(`Failed to send view queries: ${error.message}`, 'ERROR');
updateStatsStatus('error', error.message);
}
}
// Handle stats_query response and populate tables
// Handle query_view response and populate appropriate table
function handleViewQueryResponse(viewName, responseData) {
try {
console.log(`Processing view query response: ${viewName}`, responseData);
if (responseData.query_type !== 'query_view') {
log('Ignoring non-view-query response', 'WARNING');
return;
}
if (responseData.status !== 'success') {
log(`View query failed: ${responseData.error || 'Unknown error'}`, 'ERROR');
return;
}
// Route to appropriate handler based on view name
switch (viewName) {
case 'blob_overview':
if (responseData.data && Array.isArray(responseData.data) && responseData.data.length > 0) {
populateStatsOverview(responseData.data[0]);
}
break;
case 'blob_type_distribution':
if (responseData.data && Array.isArray(responseData.data)) {
populateStatsKinds(responseData.data);
}
break;
case 'blob_time_stats':
if (responseData.data && Array.isArray(responseData.data) && responseData.data.length > 0) {
populateStatsTime(responseData.data[0]);
}
break;
case 'top_uploaders':
if (responseData.data && Array.isArray(responseData.data)) {
populateStatsPubkeys(responseData.data);
}
break;
default:
console.log(`Unknown view name: ${viewName}`);
}
} catch (error) {
log(`Error processing ${viewName} response: ${error.message}`, 'ERROR');
}
}
// Legacy handler for backward compatibility
function handleStatsQueryResponse(responseData) {
try {
log('Processing stats query response...', 'INFO');
@@ -3923,20 +3928,30 @@ function handleStatsQueryResponse(responseData) {
return;
}
// Populate overview table
populateStatsOverview(responseData);
// Extract the actual data object
const statsData = responseData.data || responseData;
console.log('Extracted stats data:', statsData);
// Populate event kinds table
populateStatsKinds(responseData);
// Populate overview table with blob statistics
populateStatsOverview(statsData);
// Populate blob type distribution table
if (statsData.type_distribution && Array.isArray(statsData.type_distribution)) {
populateStatsKinds(statsData.type_distribution);
}
// Populate time-based statistics
populateStatsTime(responseData);
if (statsData.blobs_24h !== undefined) {
populateStatsTime(statsData);
}
// Populate top pubkeys table
populateStatsPubkeys(responseData);
// Populate top uploaders table
if (statsData.top_uploaders && Array.isArray(statsData.top_uploaders)) {
populateStatsPubkeys(statsData.top_uploaders);
}
updateStatsStatus('loaded');
log('Database statistics updated successfully', 'INFO');
log('Blob statistics updated successfully', 'INFO');
} catch (error) {
log(`Error processing stats response: ${error.message}`, 'ERROR');
@@ -4110,32 +4125,44 @@ function populateStatsOverview(data) {
if (!data) return;
// Update individual cells with flash animation for changed values
updateStatsCell('db-size', data.database_size_bytes ? formatFileSize(data.database_size_bytes) : '-');
updateStatsCell('total-events', data.total_events || '-');
updateStatsCell('oldest-event', data.database_created_at ? formatTimestamp(data.database_created_at) : '-');
updateStatsCell('newest-event', data.latest_event_at ? formatTimestamp(data.latest_event_at) : '-');
// Backend sends: total_bytes, total_blobs, first_upload, last_upload
updateStatsCell('db-size', data.total_bytes ? formatFileSize(data.total_bytes) : '-');
updateStatsCell('total-size', data.total_bytes ? formatFileSize(data.total_bytes) : '-');
updateStatsCell('total-events', data.total_blobs || '-');
updateStatsCell('oldest-event', data.first_upload ? formatTimestamp(data.first_upload) : '-');
updateStatsCell('newest-event', data.last_upload ? formatTimestamp(data.last_upload) : '-');
}
// Populate event kinds distribution table
function populateStatsKinds(data) {
const tableBody = document.getElementById('stats-kinds-table-body');
if (!tableBody || !data.event_kinds) return;
if (!tableBody) return;
tableBody.innerHTML = '';
if (data.event_kinds.length === 0) {
// Handle both old format (data.event_kinds) and new format (direct array from query_view)
const kindsData = data.event_kinds || data;
if (!Array.isArray(kindsData) || kindsData.length === 0) {
const row = document.createElement('tr');
row.innerHTML = '<td colspan="3" style="text-align: center; font-style: italic;">No event data</td>';
row.innerHTML = '<td colspan="3" style="text-align: center; font-style: italic;">No blob type data</td>';
tableBody.appendChild(row);
return;
}
data.event_kinds.forEach(kind => {
// Calculate total for percentages if not provided
const total = kindsData.reduce((sum, item) => sum + (item.blob_count || item.count || 0), 0);
kindsData.forEach(item => {
const row = document.createElement('tr');
const mimeType = item.mime_type || item.kind || '-';
const count = item.blob_count || item.count || 0;
const percentage = item.percentage || (total > 0 ? ((count / total) * 100).toFixed(1) : 0);
row.innerHTML = `
<td>${kind.kind}</td>
<td>${kind.count}</td>
<td>${kind.percentage}%</td>
<td>${mimeType}</td>
<td>${count}</td>
<td>${percentage}%</td>
`;
tableBody.appendChild(row);
});
@@ -4145,48 +4172,59 @@ function populateStatsKinds(data) {
function populateStatsTime(data) {
if (!data) return;
// Access the nested time_stats object from backend response
const timeStats = data.time_stats || {};
// Update cells with flash animation for changed values
updateStatsCell('events-24h', timeStats.last_24h || '0');
updateStatsCell('events-7d', timeStats.last_7d || '0');
updateStatsCell('events-30d', timeStats.last_30d || '0');
updateStatsCell('events-24h', data.blobs_24h || '0');
updateStatsCell('events-7d', data.blobs_7d || '0');
updateStatsCell('events-30d', data.blobs_30d || '0');
}
// Populate top pubkeys table
function populateStatsPubkeys(data) {
const tableBody = document.getElementById('stats-pubkeys-table-body');
if (!tableBody || !data.top_pubkeys) return;
if (!tableBody) return;
tableBody.innerHTML = '';
if (data.top_pubkeys.length === 0) {
// Handle both old format (data.top_pubkeys) and new format (direct array from query_view)
const pubkeysData = data.top_pubkeys || data;
if (!Array.isArray(pubkeysData) || pubkeysData.length === 0) {
const row = document.createElement('tr');
row.innerHTML = '<td colspan="4" style="text-align: center; font-style: italic;">No pubkey data</td>';
row.innerHTML = '<td colspan="5" style="text-align: center; font-style: italic;">No uploader data</td>';
tableBody.appendChild(row);
return;
}
data.top_pubkeys.forEach((pubkey, index) => {
// Calculate total for percentages if not provided
const total = pubkeysData.reduce((sum, item) => sum + (item.blob_count || 0), 0);
pubkeysData.forEach((item, index) => {
const row = document.createElement('tr');
// Handle both uploader_pubkey (new) and pubkey (old) field names
const pubkeyValue = item.uploader_pubkey || item.pubkey || '-';
const count = item.blob_count || 0;
const totalBytes = item.total_bytes || 0;
const percentage = item.percentage || (total > 0 ? ((count / total) * 100).toFixed(1) : 0);
// Convert hex pubkey to npub for display
let displayPubkey = pubkey.pubkey || '-';
let displayPubkey = pubkeyValue;
let npubLink = displayPubkey;
try {
if (pubkey.pubkey && pubkey.pubkey.length === 64 && /^[0-9a-fA-F]+$/.test(pubkey.pubkey)) {
const npub = window.NostrTools.nip19.npubEncode(pubkey.pubkey);
if (pubkeyValue && pubkeyValue.length === 64 && /^[0-9a-fA-F]+$/.test(pubkeyValue)) {
const npub = window.NostrTools.nip19.npubEncode(pubkeyValue);
displayPubkey = npub;
npubLink = `<a href="https://njump.me/${npub}" target="_blank" class="npub-link">${npub}</a>`;
}
} catch (error) {
console.log('Failed to encode pubkey to npub:', error.message);
}
row.innerHTML = `
<td>${index + 1}</td>
<td style="font-family: 'Courier New', monospace; font-size: 12px; word-break: break-all;">${npubLink}</td>
<td>${pubkey.event_count}</td>
<td>${pubkey.percentage}%</td>
<td>${count}</td>
<td>${formatFileSize(totalBytes)}</td>
<td>${percentage}%</td>
`;
tableBody.appendChild(row);
});
@@ -4467,15 +4505,68 @@ function updateStatsCell(cellId, newValue) {
}
}
// Start auto-refreshing database statistics every 10 seconds
// Start polling for statistics (every 10 seconds)
function startStatsPolling() {
console.log('=== STARTING STATISTICS POLLING ===');
console.log('Current page:', currentPage);
console.log('Is logged in:', isLoggedIn);
console.log('User pubkey:', userPubkey);
console.log('Relay pubkey:', relayPubkey);
// Stop any existing polling first
stopStatsPolling();
// Fetch immediately
console.log('Fetching statistics immediately...');
sendStatsQuery().catch(error => {
console.error('Initial stats fetch failed:', error);
});
// Set up polling interval (10 seconds)
console.log('Setting up 10-second polling interval...');
statsAutoRefreshInterval = setInterval(() => {
console.log('⏰ Polling interval triggered - fetching statistics...');
sendStatsQuery().catch(error => {
console.error('Polling stats fetch failed:', error);
});
}, 10000);
console.log('Statistics polling started successfully');
console.log('Interval ID:', statsAutoRefreshInterval);
log('Statistics polling started (10 second interval)', 'INFO');
}
// Stop polling for statistics
function stopStatsPolling() {
if (statsAutoRefreshInterval) {
clearInterval(statsAutoRefreshInterval);
statsAutoRefreshInterval = null;
log('Statistics polling stopped', 'INFO');
}
if (countdownInterval) {
clearInterval(countdownInterval);
countdownInterval = null;
}
// Reset countdown display
updateCountdownDisplay();
}
// Legacy function - kept for backward compatibility
function startStatsAutoRefresh() {
// DISABLED - Using real-time monitoring events instead of polling
// This function is kept for backward compatibility but no longer starts auto-refresh
log('Database statistics auto-refresh DISABLED - using real-time monitoring events', 'INFO');
}
// Stop auto-refreshing database statistics
// Legacy function - kept for backward compatibility
function stopStatsAutoRefresh() {
stopStatsPolling();
}
// Original stopStatsAutoRefresh implementation (now unused)
function stopStatsAutoRefresh_ORIGINAL() {
if (statsAutoRefreshInterval) {
clearInterval(statsAutoRefreshInterval);
statsAutoRefreshInterval = null;
@@ -4659,6 +4750,11 @@ function closeSideNav() {
}
function switchPage(pageName) {
// Stop statistics polling if leaving statistics page
if (currentPage === 'statistics' && pageName !== 'statistics') {
stopStatsPolling();
}
// Update current page
currentPage = pageName;
@@ -4728,7 +4824,7 @@ function switchPage(pageName) {
// Initialize the app
document.addEventListener('DOMContentLoaded', () => {
console.log('C-Relay Admin API interface loaded');
console.log('Blossom Admin API interface loaded');
// Initialize dark mode
initializeDarkMode();