Files
client/www/db.html

1148 lines
35 KiB
HTML

<!DOCTYPE html>
<?xml version="1.0" encoding="UTF-8"?>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8" />
<title>DB</title>
<link rel="stylesheet" href="./css/client.css" />
<!-- Initialize theme BEFORE any components load -->
<script>
(function () {
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'dark') {
document.documentElement.classList.add('dark-mode');
if (document.body) {
document.body.classList.add('dark-mode');
}
}
})();
</script>
<link rel="shortcut icon" type="image/x-icon" href="./favicon/favicon-dots2.ico" />
<!-- SVG.js library (required by HamburgerMorphing) -->
<script src="./js/vendor/svg.min.js"></script>
<style media="screen">
#divBody {
display: flex;
flex-direction: column;
padding: 20px;
overflow-y: auto;
}
#tblDB {
font-family: var(--font-family);
border: 2px solid var(--primary-color);
border-collapse: collapse;
white-space: nowrap;
width: 100%;
}
#tblDB th {
border: 1px solid var(--primary-color);
padding: 10px;
font-weight: bold;
background: var(--primary-color);
color: var(--secondary-color);
text-align: left;
cursor: pointer;
user-select: none;
position: relative;
padding-right: 30px;
}
#tblDB th:hover {
background: var(--accent-color);
}
#tblDB th .sort-icon {
position: absolute;
right: 5px;
top: 50%;
transform: translateY(-50%);
width: 20px;
height: 20px;
}
#tblDB th.sorted-asc .sort-icon,
#tblDB th.sorted-desc .sort-icon {
display: block;
}
#tblDB th:not(.sorted-asc):not(.sorted-desc) .sort-icon {
opacity: 0.3;
}
#tblDB td {
border: 1px dashed var(--muted-color);
padding: 2px;
max-width: 400px;
overflow: hidden;
text-overflow: ellipsis;
}
#tblDB tr:hover td {
background-color: var(--muted-color);
}
.divJsonPreview {
max-height: none;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 70%;
font-family: monospace;
background: var(--secondary-color);
border: 1px solid var(--muted-color);
padding: 5px;
}
.btnRefresh {
padding: 10px 20px;
border: 2px solid var(--primary-color);
background: transparent;
color: var(--primary-color);
font-family: var(--font-family);
cursor: pointer;
font-weight: bold;
width: 100%;
margin-top: 10px;
}
.btnRefresh:hover {
background: var(--accent-color);
color: var(--secondary-color);
}
.filter-row th {
background: var(--secondary-color) !important;
cursor: default !important;
padding: 6px;
}
.filter-row th:hover {
background: var(--secondary-color) !important;
}
.filter-input {
width: 100%;
box-sizing: border-box;
padding: 6px;
border: 1px solid var(--muted-color);
background: var(--secondary-color);
color: var(--primary-color);
font-family: var(--font-family);
font-size: 12px;
}
.divCommands {
display: flex;
gap: 6px;
}
.btnCmd {
padding: 4px 8px;
border: 1px solid var(--primary-color);
background: transparent;
color: var(--primary-color);
font-family: var(--font-family);
font-size: 12px;
cursor: pointer;
}
.btnCmd:hover {
background: var(--accent-color);
color: var(--secondary-color);
}
.kindSummaryTable {
width: 100%;
border-collapse: collapse;
border: 2px solid var(--primary-color);
}
.kindSummaryTable th,
.kindSummaryTable td {
border: 1px solid var(--muted-color);
padding: 8px;
text-align: left;
}
.kindSummaryTable th {
background: var(--primary-color);
color: var(--secondary-color);
}
.kindSummaryRow {
cursor: pointer;
}
.kindSummaryRow:hover td {
background: var(--muted-color);
}
.kindExpandedCell {
padding: 0 !important;
background: var(--secondary-color);
}
.kindExpandedSection {
padding: 10px;
border-top: 1px dashed var(--muted-color);
}
.paginationBar {
margin-top: 8px;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--muted-color);
}
.btnPage {
padding: 4px 8px;
border: 1px solid var(--primary-color);
background: transparent;
color: var(--primary-color);
cursor: pointer;
font-size: 12px;
font-family: var(--font-family);
}
.btnPage[disabled] {
opacity: 0.4;
cursor: default;
}
.btnPage.active {
background: var(--accent-color);
color: var(--secondary-color);
}
.rawJsonModal {
position: fixed;
inset: 0;
display: none;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.6);
z-index: 1000;
padding: 20px;
box-sizing: border-box;
}
.rawJsonModal.open {
display: flex;
}
.rawJsonModalContent {
background: var(--secondary-color);
color: var(--primary-color);
border: 2px solid var(--primary-color);
width: min(1000px, 100%);
max-height: 85vh;
display: flex;
flex-direction: column;
}
.rawJsonModalToolbar {
display: flex;
gap: 10px;
padding: 10px;
border-bottom: 1px solid var(--muted-color);
}
.rawJsonModalPre {
margin: 0;
padding: 12px;
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
font-family: monospace;
font-size: 12px;
}
</style>
</head>
<body>
<!-- ================================================================
HAMBURGER BUTTON (Fixed, separate from header)
================================================================ -->
<div id="divSvgHam" class="divHeaderButtons">
<!-- HamburgerMorphing will be injected here -->
</div>
<!-- ================================================================
HEADER
================================================================ -->
<div id="divHeader">
<div id="divHeaderFlexLeft">
<!-- Hamburger is now separate fixed element -->
</div>
<div id="divHeaderFlexCenter">
<div class="divHeaderText">DB</div>
</div>
<div id="divHeaderFlexRight">
<!-- No button in header right - logout is in sidenav footer -->
</div>
</div>
<!-- ================================================================
BODY
================================================================ -->
<div id="divBody">
<div id="divTable"></div>
</div>
<div id="rawJsonModal" class="rawJsonModal">
<div class="rawJsonModalContent">
<div class="rawJsonModalToolbar">
<button id="btnRawJsonCopy" class="btnCmd">Copy</button>
<button id="btnRawJsonClose" class="btnCmd">Close</button>
</div>
<pre id="preRawJson" class="rawJsonModalPre"></pre>
</div>
</div>
<!-- ================================================================
FOOTER
================================================================ -->
<div id="divFooter">
<div id="divFooterLeft" class="divFooterBox"></div>
<div id="divFooterCenter" class="divFooterBox"></div>
<div id="divFooterRight" class="divFooterBox"></div>
<div id="divFooterBalance" class="divFooterBox">0 sats</div>
</div>
<!-- ================================================================
SIDENAV
================================================================ -->
<div id="divSideNav">
<div id="divSideNavHeader">
<!-- No close button - use main hamburger to close -->
</div>
<div id="divSideNavBody">
<div id="divSideNavInfo">Database browser - view IndexedDB contents</div>
<button id="btnRefresh" class="btn btnRefresh">Refresh</button>
<div id="divFiles"></div>
</div>
<div id="divAiSection" class="sidenavSection">
<div id="divAiSectionTitle" class="sidenavSectionTitle">AI</div>
<div id="divAiList" class="sidenavSectionList">
<div id="divAiProvidersList">No saved providers yet.</div>
</div>
</div>
<div id="divRelaySection">
<div id="divRelaySectionTitle">
リレー
</div>
<div id="divRelayList">
Loading relays...
</div>
</div>
<div id="divBlossomSection">
<div id="divBlossomSectionTitle">ブロッサム</div>
<div id="divBlossomList">Loading blossom servers...</div>
</div>
<div id="divVersionBar">
<span id="versionDisplay">loading...</span>
<div id="divVersionBarButtons">
<button id="themeToggleButton" title="Toggle Dark/Light Mode">
<div id="themeToggleHamburgerContainer"></div>
</button>
<button id="logoutButton" title="Logout">
<div id="logoutHamburgerContainer"></div>
</button>
</div>
</div>
</div>
<!-- ================================================================
REQUIRED SCRIPTS
================================================================ -->
<script src="./nostr.bundle.js"></script>
<script src="/nostr-login-lite/nostr-lite.js"></script>
<script type="module">
/* ================================================================
IMPORTS
================================================================ */
import { initNDKPage, getPubkey, injectHeaderAvatar, disconnect } from './js/init-ndk.mjs';
import { HamburgerMorphing } from "./hamburger_morphing/hamburger.mjs";
import { initFooterRelayStatus, updateFooterRelayStatus, initSidenavRelaySection, updateSidenavRelaySection, setRelayActivityState } from './js/relay-ui.mjs';
import { initBlossomSection, updateBlossomSection } from './js/blossom-ui.mjs';
import { initAiSectionWithLocalConfig } from './js/ai-ui.mjs';
// Fetch version from JSON
const versionResponse = await fetch('./js/version.json');
const versionData = await versionResponse.json();
const VERSION = versionData.VERSION;
console.log(`[db.html ${VERSION}] Loading...`);
/* ================================================================
GLOBAL VARIABLES
================================================================ */
let updateIntervalId = null;
let currentPubkey = null;
// Hamburger menu
let hamburgerInstance = null;
let isNavOpen = false;
// Version bar buttons
let logoutHamburger = null;
let themeToggleHamburger = null;
let isDarkMode = false;
// IndexedDB
let db = null;
const DB_NAME = 'ndk-shared';
// Grouped table state
let currentEvents = [];
let kindGroups = new Map();
let expandedKinds = new Set();
let kindViewState = new Map();
let rowLookupByKey = new Map();
const DEFAULT_PAGE_SIZE = 50;
const PAGE_SIZE_OPTIONS = [25, 50, 100];
/* ================================================================
DOM VARIABLES
================================================================ */
const divBody = document.getElementById("divBody");
const divSideNav = document.getElementById("divSideNav");
const divSideNavBody = document.getElementById("divSideNavBody");
const divSideNavInfo = document.getElementById("divSideNavInfo");
const divFooterCenter = document.getElementById("divFooterCenter");
const divFooterRight = document.getElementById("divFooterRight");
const divTable = document.getElementById("divTable");
const btnRefresh = document.getElementById("btnRefresh");
const rawJsonModal = document.getElementById("rawJsonModal");
const preRawJson = document.getElementById("preRawJson");
const btnRawJsonCopy = document.getElementById("btnRawJsonCopy");
const btnRawJsonClose = document.getElementById("btnRawJsonClose");
/* ================================================================
HAMBURGER MENU
================================================================ */
function initHamburgerMenu() {
hamburgerInstance = new HamburgerMorphing('#divSvgHam', {
foreground: 'var(--primary-color)',
background: 'var(--secondary-color)',
hover: 'var(--accent-color)'
});
hamburgerInstance.animateTo('burger');
}
/* ================================================================
SIDENAV FUNCTIONS
================================================================ */
function openNav() {
divSideNav.style.zIndex = 3;
divSideNav.style.width = "clamp(400px, 50vw, 600px)";
isNavOpen = true;
if (hamburgerInstance) {
hamburgerInstance.animateTo('arrow_left');
}
if (divSideNavInfo) {
divSideNavInfo.textContent = "Database browser - view IndexedDB contents";
}
// Initialize version bar buttons when sidenav opens (lazy load)
if (!logoutHamburger) {
logoutHamburger = new HamburgerMorphing('#logoutHamburgerContainer', {
size: 24,
foreground: 'var(--primary-color)',
background: 'var(--secondary-color)',
hover: 'var(--accent-color)'
});
logoutHamburger.animateTo('x');
}
if (!themeToggleHamburger) {
themeToggleHamburger = new HamburgerMorphing('#themeToggleHamburgerContainer', {
size: 24,
foreground: 'var(--primary-color)',
background: 'var(--secondary-color)',
hover: 'var(--accent-color)'
});
const savedTheme = localStorage.getItem('theme');
isDarkMode = savedTheme === 'dark' || document.body.classList.contains('dark-mode');
const initialShape = isDarkMode ? 'moon' : 'circle';
themeToggleHamburger.animateTo(initialShape);
}
}
function closeNav() {
divSideNav.style.width = "0vw";
divSideNav.style.zIndex = -1;
isNavOpen = false;
if (hamburgerInstance) {
hamburgerInstance.animateTo('burger');
}
}
function toggleNav() {
if (isNavOpen) {
closeNav();
} else {
openNav();
}
}
/* ================================================================
INDEXEDDB FUNCTIONS
================================================================ */
async function openDatabase() {
return new Promise((resolve, reject) => {
// Open without version to get current version
const request = indexedDB.open(DB_NAME);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
async function getAllEvents() {
if (!db) {
db = await openDatabase();
}
// Debug: log available stores
const availableStores = Array.from(db.objectStoreNames);
console.log('[db.html] Available object stores:', availableStores);
// Try different possible store names
const possibleStores = ['events', 'Event', 'ndk_events', 'cache', 'nostr_events'];
const storeName = possibleStores.find(s => availableStores.includes(s)) || availableStores[0];
if (!storeName) {
throw new Error('No object stores found in database');
}
console.log('[db.html] Using store:', storeName);
return new Promise((resolve, reject) => {
const transaction = db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
const request = store.getAll();
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const results = request.result;
console.log('[db.html] Raw results from store:', results);
if (results.length > 0) {
console.log('[db.html] First event structure:', JSON.stringify(results[0], null, 2));
}
resolve(results);
};
});
}
function formatValue(value) {
if (value === null) return 'null';
if (value === undefined) return 'undefined';
if (typeof value === 'object') {
const json = JSON.stringify(value);
return json.length > 200 ? `${json.substring(0, 200)}...` : json;
}
const str = String(value);
return str.length > 200 ? `${str.substring(0, 200)}...` : str;
}
function escapeHtml(value) {
return String(value)
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"');
}
function normalizeForFilter(value) {
if (value === null || value === undefined) return '';
if (typeof value === 'object') return JSON.stringify(value);
return String(value);
}
function wildcardToRegex(pattern) {
const escaped = pattern
.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
.replaceAll('*', '.*');
return new RegExp(`^${escaped}$`, 'i');
}
function applyFilters(events, filters) {
const activeFilters = Object.entries(filters || {})
.filter(([, filterValue]) => String(filterValue || '').trim() !== '');
if (activeFilters.length === 0) {
return events;
}
return events.filter(evt => {
return activeFilters.every(([column, filterPattern]) => {
const value = normalizeForFilter(evt[column]);
const regex = wildcardToRegex(String(filterPattern).trim());
return regex.test(value);
});
});
}
function getPrettyRawJson(rawValue) {
if (rawValue === null || rawValue === undefined) {
return '';
}
if (typeof rawValue === 'string') {
try {
return JSON.stringify(JSON.parse(rawValue), null, 2);
} catch {
return rawValue;
}
}
return JSON.stringify(rawValue, null, 2);
}
function sortEvents(events, column, direction) {
return [...events].sort((a, b) => {
let valA = a[column];
let valB = b[column];
if (column === 'kind' || column === 'created_at') {
valA = Number(valA) || 0;
valB = Number(valB) || 0;
} else {
valA = String(valA || '').toLowerCase();
valB = String(valB || '').toLowerCase();
}
if (valA < valB) return direction === 'asc' ? -1 : 1;
if (valA > valB) return direction === 'asc' ? 1 : -1;
return 0;
});
}
function getPreferredColumns(events) {
const preferredColumns = ['kind', 'created_at', 'id', 'pubkey', 'content', 'tags', 'relay'];
const fields = new Set(preferredColumns);
events.forEach(evt => {
Object.keys(evt).forEach(key => {
if (key !== 'raw_event') {
fields.add(key);
}
});
});
return [
...preferredColumns,
...Array.from(fields).filter(col => !preferredColumns.includes(col))
];
}
function groupEventsByKind(events) {
const groups = new Map();
for (const evt of events) {
const kind = evt.kind ?? 'unknown';
if (!groups.has(kind)) {
groups.set(kind, []);
}
groups.get(kind).push(evt);
}
return groups;
}
function getKindState(kind) {
if (!kindViewState.has(kind)) {
kindViewState.set(kind, {
page: 1,
pageSize: DEFAULT_PAGE_SIZE,
sortColumn: 'created_at',
sortDirection: 'desc',
filters: {}
});
}
return kindViewState.get(kind);
}
function kindToKey(kind) {
return encodeURIComponent(String(kind));
}
function keyToKind(kindKey) {
const decoded = decodeURIComponent(String(kindKey));
const asNum = Number(decoded);
return Number.isFinite(asNum) && String(asNum) === decoded ? asNum : decoded;
}
function formatTimestamp(ts) {
const n = Number(ts);
if (!Number.isFinite(n) || n <= 0) return '-';
return new Date(n * 1000).toLocaleString();
}
function getKindTimeRange(events) {
let minTs = Number.POSITIVE_INFINITY;
let maxTs = Number.NEGATIVE_INFINITY;
events.forEach(evt => {
const n = Number(evt.created_at);
if (Number.isFinite(n) && n > 0) {
if (n < minTs) minTs = n;
if (n > maxTs) maxTs = n;
}
});
return {
oldest: Number.isFinite(minTs) ? minTs : null,
newest: Number.isFinite(maxTs) ? maxTs : null
};
}
window.viewRawEvent = function(rowKey) {
const evt = rowLookupByKey.get(String(rowKey));
if (!evt) return;
preRawJson.textContent = getPrettyRawJson(evt.raw_event);
rawJsonModal.classList.add('open');
}
window.copyRawEvent = async function(rowKey) {
const evt = rowLookupByKey.get(String(rowKey));
if (!evt) return;
const raw = getPrettyRawJson(evt.raw_event);
try {
await navigator.clipboard.writeText(raw);
} catch (error) {
console.error('[db.html] Failed to copy raw JSON:', error);
}
}
window.toggleKind = function(kindKey) {
const kind = keyToKind(kindKey);
if (expandedKinds.has(kind)) {
expandedKinds.delete(kind);
} else {
expandedKinds.add(kind);
getKindState(kind);
}
renderKindSummary();
}
window.handleKindSort = function(kindKey, column) {
const kind = keyToKind(kindKey);
const state = getKindState(kind);
if (state.sortColumn === column) {
state.sortDirection = state.sortDirection === 'asc' ? 'desc' : 'asc';
} else {
state.sortColumn = column;
state.sortDirection = 'asc';
}
state.page = 1;
renderKindSummary();
}
window.handleKindFilterInput = function(kindKey, column, value) {
const kind = keyToKind(kindKey);
const state = getKindState(kind);
state.filters[column] = value;
state.page = 1;
renderKindSummary();
}
window.handleKindPage = function(kindKey, delta) {
const kind = keyToKind(kindKey);
const state = getKindState(kind);
state.page = Math.max(1, state.page + Number(delta || 0));
renderKindSummary();
}
window.handleKindPageSize = function(kindKey, pageSize) {
const kind = keyToKind(kindKey);
const state = getKindState(kind);
state.pageSize = Number(pageSize) || DEFAULT_PAGE_SIZE;
state.page = 1;
renderKindSummary();
}
function renderExpandedKind(kind, events) {
const kindKey = kindToKey(kind);
const state = getKindState(kind);
const dataColumns = getPreferredColumns(events);
const filteredEvents = applyFilters(events, state.filters);
const sortedEvents = state.sortColumn
? sortEvents(filteredEvents, state.sortColumn, state.sortDirection)
: filteredEvents;
const total = sortedEvents.length;
const totalPages = Math.max(1, Math.ceil(total / state.pageSize));
if (state.page > totalPages) {
state.page = totalPages;
}
const startIndex = (state.page - 1) * state.pageSize;
const pageEvents = sortedEvents.slice(startIndex, startIndex + state.pageSize);
let html = '<div class="kindExpandedSection">';
html += '<table id="tblDB"><thead><tr>';
html += '<th>commands</th>';
dataColumns.forEach(col => {
const isSorted = state.sortColumn === col;
const sortArrow = isSorted ? (state.sortDirection === 'asc' ? ' ▲' : ' ▼') : '';
html += `<th onclick="handleKindSort('${kindKey}', '${col}')">${col}${sortArrow}</th>`;
});
html += '</tr>';
html += '<tr class="filter-row">';
html += '<th></th>';
dataColumns.forEach(col => {
const filterValue = state.filters[col] || '';
html += `<th><input class="filter-input" value="${escapeHtml(filterValue)}" oninput="handleKindFilterInput('${kindKey}', '${col}', this.value)" placeholder="* wildcard" /></th>`;
});
html += '</tr></thead><tbody>';
pageEvents.forEach((evt, idx) => {
const rowKey = `${kindKey}:${startIndex + idx}:${String(evt.id || '').slice(0, 32)}`;
rowLookupByKey.set(rowKey, evt);
html += '<tr>';
html += `<td><div class="divCommands"><button class="btnCmd" onclick="viewRawEvent('${rowKey}')">view</button><button class="btnCmd" onclick="copyRawEvent('${rowKey}')">copy</button></div></td>`;
dataColumns.forEach(col => {
html += `<td>${formatValue(evt[col])}</td>`;
});
html += '</tr>';
});
html += '</tbody></table>';
const shownStart = total === 0 ? 0 : startIndex + 1;
const shownEnd = Math.min(startIndex + pageEvents.length, total);
const canPrev = state.page > 1;
const canNext = state.page < totalPages;
html += '<div class="paginationBar">';
html += `<button class="btnPage" ${canPrev ? '' : 'disabled'} onclick="handleKindPage('${kindKey}', -1)">Prev</button>`;
html += `<span>Page ${state.page} / ${totalPages}</span>`;
html += `<button class="btnPage" ${canNext ? '' : 'disabled'} onclick="handleKindPage('${kindKey}', 1)">Next</button>`;
html += '<span>Show:</span>';
PAGE_SIZE_OPTIONS.forEach(size => {
const activeClass = state.pageSize === size ? 'active' : '';
html += `<button class="btnPage ${activeClass}" onclick="handleKindPageSize('${kindKey}', ${size})">${size}</button>`;
});
html += `<span>Showing ${shownStart}-${shownEnd} of ${total}</span>`;
html += '</div>';
html += '</div>';
return html;
}
function renderKindSummary() {
rowLookupByKey = new Map();
const sortedKinds = Array.from(kindGroups.keys()).sort((a, b) => {
const na = Number(a);
const nb = Number(b);
const aNum = Number.isFinite(na);
const bNum = Number.isFinite(nb);
if (aNum && bNum) return na - nb;
return String(a).localeCompare(String(b));
});
const totalEvents = currentEvents.length;
divFooterCenter.textContent = `${totalEvents.toLocaleString()} events across ${sortedKinds.length.toLocaleString()} kinds`;
let html = '<table class="kindSummaryTable">';
html += '<thead><tr><th></th><th>kind</th><th>count</th><th>newest</th><th>oldest</th></tr></thead><tbody>';
sortedKinds.forEach(kind => {
const events = kindGroups.get(kind) || [];
const kindKey = kindToKey(kind);
const expanded = expandedKinds.has(kind);
const range = getKindTimeRange(events);
html += `<tr class="kindSummaryRow" onclick="toggleKind('${kindKey}')">`;
html += `<td>${expanded ? '▼' : '▶'}</td>`;
html += `<td>${escapeHtml(kind)}</td>`;
html += `<td>${events.length.toLocaleString()}</td>`;
html += `<td>${formatTimestamp(range.newest)}</td>`;
html += `<td>${formatTimestamp(range.oldest)}</td>`;
html += '</tr>';
if (expanded) {
html += '<tr><td class="kindExpandedCell" colspan="5">';
html += renderExpandedKind(kind, events);
html += '</td></tr>';
}
});
html += '</tbody></table>';
divTable.innerHTML = html;
}
function normalizeDisplayId(id, kind) {
const rawId = String(id ?? '');
const prefix = `${kind}:`;
if (rawId.startsWith(prefix)) return rawId.slice(prefix.length);
return rawId;
}
async function loadEvents() {
try {
const events = await getAllEvents();
console.log(`[db.html] Loaded ${events.length} events`);
if (events.length === 0) {
divFooterCenter.textContent = '0 events across 0 kinds';
divTable.innerHTML = '<div style="text-align: center; padding: 50px;">No events in IndexedDB</div>';
return;
}
// Parse events - the 'event' field contains the full serialized event
currentEvents = events.map(evt => {
try {
const parsed = JSON.parse(evt.event);
const parsedKind = Array.isArray(parsed) ? parsed[0] : parsed.kind;
const parsedId = Array.isArray(parsed) ? parsed[6] : parsed.id;
const kind = evt.kind ?? parsedKind;
const content = Array.isArray(parsed) ? (parsed[5] || '') : (parsed.content || '');
const tags = Array.isArray(parsed) ? (parsed[4] || []) : (parsed.tags || []);
return {
id: normalizeDisplayId(parsedId ?? evt.id, kind),
kind,
pubkey: evt.pubkey,
created_at: evt.createdAt ?? evt.created_at,
content,
tags,
relay: evt.relay,
raw_event: evt.event
};
} catch (e) {
console.error('[db.html] Failed to parse event:', evt.id, e);
return {
id: normalizeDisplayId(evt.id, evt.kind),
kind: evt.kind,
pubkey: evt.pubkey,
created_at: evt.createdAt ?? evt.created_at,
content: 'PARSE ERROR',
tags: [],
relay: evt.relay,
raw_event: evt.event
};
}
});
kindGroups = groupEventsByKind(currentEvents);
renderKindSummary();
} catch (error) {
console.error('[db.html] Error loading events:', error);
divTable.innerHTML = `<div style="text-align: center; padding: 50px; color: red;">Error: ${error.message}</div>`;
}
}
async function initDatabase() {
try {
db = await openDatabase();
console.log('[db.html] Database opened, version:', db.version);
await loadEvents();
} catch (error) {
console.error('[db.html] Error opening database:', error);
divTable.innerHTML = `<div style="text-align: center; padding: 50px; color: red;">Error opening database: ${error.message}</div>`;
}
}
/* ================================================================
UPDATE FOOTER
================================================================ */
const UpdateFooter = async () => {
try {
await updateFooterRelayStatus();
await updateSidenavRelaySection();
await updateBlossomSection();
divFooterRight.innerHTML = '';
} catch (error) {
console.error('[db.html] Error updating footer:', error);
}
};
/* ================================================================
LOGOUT
================================================================ */
const Logout = async () => {
console.log("[db.html] Starting logout process...");
if (updateIntervalId) {
clearInterval(updateIntervalId);
updateIntervalId = null;
}
disconnect();
if (window.NOSTR_LOGIN_LITE && window.NOSTR_LOGIN_LITE.logout) {
await window.NOSTR_LOGIN_LITE.logout();
}
localStorage.clear();
sessionStorage.clear();
if (window.indexedDB) {
const databases = await window.indexedDB.databases();
for (const dbInfo of databases) {
if (dbInfo.name) {
window.indexedDB.deleteDatabase(dbInfo.name);
}
}
}
console.log("[db.html] Logged out, reloading page");
location.reload(true);
};
/* ================================================================
EVENT LISTENERS
================================================================ */
function setupEventListeners() {
// Hamburger
const divSvgHam = document.getElementById('divSvgHam');
if (divSvgHam) {
divSvgHam.addEventListener('click', toggleNav);
}
// Refresh button
if (btnRefresh) {
btnRefresh.addEventListener('click', () => {
loadEvents();
});
}
// Raw JSON modal
if (btnRawJsonClose) {
btnRawJsonClose.addEventListener('click', () => {
rawJsonModal.classList.remove('open');
});
}
if (btnRawJsonCopy) {
btnRawJsonCopy.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(preRawJson.textContent || '');
} catch (error) {
console.error('[db.html] Failed to copy modal raw JSON:', error);
}
});
}
// Version bar buttons
const themeToggleButton = document.getElementById('themeToggleButton');
const logoutButton = document.getElementById('logoutButton');
if (themeToggleButton) {
themeToggleButton.addEventListener('click', () => {
isDarkMode = !isDarkMode;
if (isDarkMode) {
localStorage.setItem('theme', 'dark');
} else {
localStorage.setItem('theme', 'light');
}
localStorage.setItem('sidenavWasOpen', isNavOpen ? 'true' : 'false');
window.location.reload();
});
}
if (logoutButton) {
logoutButton.addEventListener('click', async () => {
try {
await Logout();
} catch (error) {
console.error('Logout failed:', error);
}
});
}
}
/* ================================================================
INITIALIZATION
================================================================ */
(async function main() {
console.log("[db.html] Starting initialization...");
try {
// Initialize hamburger menu
initHamburgerMenu();
// Setup event listeners
setupEventListeners();
// Initialize NDK
await initNDKPage();
// Get authenticated pubkey
currentPubkey = await getPubkey();
await injectHeaderAvatar(currentPubkey);
console.log("[db.html] Authenticated as:", currentPubkey);
// Initialize relay UI
initFooterRelayStatus();
initSidenavRelaySection();
await initBlossomSection();
initAiSectionWithLocalConfig();
// Listen for relay activity
window.addEventListener('ndkRelayActivity', (event) => {
const { relayUrl, activity, stats } = event.detail;
setRelayActivityState(relayUrl, activity);
});
// Initialize database and load events automatically
await initDatabase();
// Restore sidenav state
const sidenavWasOpen = localStorage.getItem('sidenavWasOpen');
if (sidenavWasOpen === 'true') {
localStorage.removeItem('sidenavWasOpen');
openNav();
}
// Start update loop
updateIntervalId = setInterval(UpdateFooter, 1000);
// Update version display
document.getElementById('versionDisplay').textContent = VERSION;
console.log('[db.html] Initialization complete');
} catch (error) {
console.error('[db.html] Initialization failed:', error);
divBody.innerHTML = `<div style="text-align: center; padding: 50px;">
<div style="font-size: 24px; margin-bottom: 20px; color: red;">❌ Authentication Error</div>
<div style="font-size: 16px; color: #666;">${error.message}</div>
<div style="margin-top: 20px;">
<button onclick="location.reload()" style="padding: 10px 20px; font-size: 16px;">Retry</button>
</div>
</div>`;
}
})();
</script>
</body>
</html>