Remove outdated CSS and JavaScript files from dist directory.

- Deleted `index-zhtd763e.css` and `index-zqddcpy5.js` to streamline the build artifacts.
- Simplified repository by removing unused generated files to maintain a clean structure.
This commit is contained in:
2025-09-21 14:36:49 +01:00
parent a5b6943320
commit facf03783f
10 changed files with 514 additions and 231 deletions

160
app/web/dist/index-bnzmmj1a.js vendored Normal file

File diff suppressed because one or more lines are too long

1
app/web/dist/index-cepjm5g7.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
*,:before,:after{box-sizing:border-box;border:0 solid #e5e7eb}html,body,#root{height:100%}html{-webkit-text-size-adjust:100%;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Noto Sans,Ubuntu,Cantarell,Helvetica Neue,Helvetica,Arial,"\"Apple Color Emoji\"","\"Segoe UI Emoji\"";line-height:1.5}body{margin:0}button,input{font:inherit;color:inherit}img{display:block;max-width:100%;height:auto}.sticky{position:sticky}.relative{position:relative}.absolute{position:absolute}.top-0{top:0}.left-0{left:0}.inset-0{inset:0}.z-50{z-index:50}.z-10{z-index:10}.block{display:block}.flex{display:flex}.items-center{align-items: center}.justify-start{justify-content:flex-start}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.flex-grow{flex-grow:1}.shrink-0{flex-shrink:0}.overflow-hidden{overflow:hidden}.w-full{width:100%}.w-auto{width:auto}.w-16{width:4rem}.h-full{height:100%}.h-16{height:4rem}.aspect-square{aspect-ratio:1}.max-w-3xl{max-width:48rem}.p-0{padding:0}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-6{padding:1.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.mr-0{margin-right:0}.mr-2{margin-right:.5rem}.mt-2{margin-top:.5rem}.mt-5{margin-top:1.25rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mx-auto{margin-left:auto;margin-right:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.border-0{border-width:0}.border-2{border-width:2px}.border-white{border-color:#fff}.border{border-width:1px}.border-gray-300{border-color:#d1d5db}.bg-white{background-color:#fff}.bg-gray-100{background-color:#f3f4f6}.bg-blue-600{background-color:#2563eb}.hover\:bg-blue-700:hover{background-color:#1d4ed8}.bg-red-600{background-color:#dc2626}.hover\:bg-red-700:hover{background-color:#b91c1c}.bg-cyan-100{background-color:#cffafe}.bg-green-100{background-color:#d1fae5}.bg-red-100{background-color:#fee2e2}.bg-cover{background-size:cover}.bg-center{background-position:50%}.bg-transparent{background-color:#0000}.text-left{text-align:left}.text-white{color:#fff}.text-gray-500{color:#6b7280}.hover\:text-gray-800:hover{color:#1f2937}.text-green-800{color:#065f46}.text-red-800{color:#991b1b}.text-cyan-800{color:#155e75}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-2xl{font-size:1.5rem;line-height:2rem}.font-bold{font-weight:700}.opacity-70{opacity:.7}.shadow{--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;box-shadow:var(--tw-shadow)}.cursor-pointer{cursor:pointer}.box-border{box-sizing:border-box}.hover\:bg-transparent:hover{background-color:#0000}body{margin:0;padding:0;font-family:Arial,sans-serif}.container{background:#f9f9f9;border-radius:8px;margin-top:20px;padding:30px}.form-group{margin-bottom:20px}label{display:block;margin-bottom:5px;font-weight:700}input,textarea{border:1px solid #ddd;border-radius:4px;width:100%;padding:10px}button{color:#fff;cursor:pointer;background:#007cba;border:none;border-radius:4px;padding:12px 20px}button:hover{background:#005a87}.danger-button{background:#dc3545}.danger-button:hover{background:#c82333}.status{border-radius:4px;margin-top:20px;margin-bottom:20px;padding:10px}.success{color:#155724;background:#d4edda}.error{color:#721c24;background:#f8d7da}.info{color:#0c5460;background:#d1ecf1}.header-panel{position:sticky;z-index:1000;display:flex;overflow:hidden;background-color:#f8f9fa;background-position:50%;background-size:cover;align-items: center;width:100%;height:60px;top:0;left:0;box-shadow:0 2px 4px #0000001a}.header-content{display:flex;box-sizing:border-box;align-items: center;width:100%;height:100%;margin:0 auto;padding:0 0 0 12px}.header-left{display:flex;justify-content:flex-start;align-items: center;height:100%}.header-center{display:flex;position:relative;overflow:hidden;flex-grow:1;justify-content:flex-start;align-items: center}.header-right{display:flex;justify-content:flex-end;align-items: center;height:100%}.header-logo{aspect-ratio:1;object-fit:cover;border-radius:0;flex-shrink:0;width:auto;height:100%}.user-avatar{object-fit:cover;border:2px solid #fff;border-radius:50%;width:40px;height:40px;margin-right:10px;box-shadow:0 1px 3px #0003}.user-profile{display:flex;position:relative;z-index:1;align-items: center}.user-info{text-align:left;font-size:1.2em;font-weight:700}.user-name{display:block;font-size:1em;font-weight:700}.profile-banner{position:absolute;z-index:-1;opacity:.7;width:100%;height:100%;top:0;left:0}.logout-button{color:#6c757d;cursor:pointer;display:flex;background:0 0;border:none;flex-shrink:0;justify-content:center;align-items: center;width:48px;height:100%;margin-left:10px;margin-right:0;padding:0;font-size:20px}.logout-button:hover{color:#343a40;background:0 0}

File diff suppressed because one or more lines are too long

View File

@@ -5,8 +5,25 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Nostr Relay</title>
<link rel="stylesheet" crossorigin href="./index-zhtd763e.css"><script type="module" crossorigin src="./index-zqddcpy5.js"></script></head>
<body class="bg-white">
<link rel="stylesheet" crossorigin href="./index-cepjm5g7.css"><script type="module" crossorigin src="./index-bnzmmj1a.js"></script></head>
<body>
<script>
// Apply system theme preference immediately to avoid flash of wrong theme
function applyTheme(isDark) {
document.body.classList.remove('bg-white', 'bg-gray-900');
document.body.classList.add(isDark ? 'bg-gray-900' : 'bg-white');
}
// Set initial theme
applyTheme(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches);
// Listen for theme changes
if (window.matchMedia) {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
applyTheme(e.matches);
});
}
</script>
<div id="root"></div>
</body>

View File

@@ -39,26 +39,49 @@ img{display:block;max-width:100%;height:auto}
.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}
.border-0{border-width:0}.border-2{border-width:2px}
.border-white{border-color:#fff}
.border{border-width:1px}.border-gray-300{border-color:#d1d5db}
.border{border-width:1px}.border-gray-300{border-color:#d1d5db}.border-gray-600{border-color:#4b5563}
.border-red-500{border-color:#ef4444}.border-red-700{border-color:#b91c1c}
/* Colors / Backgrounds */
.bg-white{background-color:#fff}
.bg-gray-100{background-color:#f3f4f6}
.bg-gray-200{background-color:#e5e7eb}
.bg-gray-300{background-color:#d1d5db}
.bg-gray-600{background-color:#4b5563}
.bg-gray-700{background-color:#374151}
.bg-gray-800{background-color:#1f2937}
.bg-gray-900{background-color:#111827}
.bg-blue-500{background-color:#3b82f6}
.bg-blue-600{background-color:#2563eb}.hover\:bg-blue-700:hover{background-color:#1d4ed8}
.hover\:bg-blue-600:hover{background-color:#2563eb}
.bg-red-600{background-color:#dc2626}.hover\:bg-red-700:hover{background-color:#b91c1c}
.bg-cyan-100{background-color:#cffafe}
.bg-green-100{background-color:#d1fae5}
.bg-red-100{background-color:#fee2e2}
.bg-red-50{background-color:#fef2f2}
.bg-green-900{background-color:#064e3b}
.bg-red-900{background-color:#7f1d1d}
.bg-cyan-900{background-color:#164e63}
.bg-cover{background-size:cover}.bg-center{background-position:center}
.bg-transparent{background-color:transparent}
/* Text */
.text-left{text-align:left}
.text-white{color:#fff}
.text-gray-300{color:#d1d5db}
.text-gray-500{color:#6b7280}.hover\:text-gray-800:hover{color:#1f2937}
.hover\:text-gray-100:hover{color:#f3f4f6}
.text-gray-700{color:#374151}
.text-gray-800{color:#1f2937}
.text-gray-900{color:#111827}
.text-gray-100{color:#f3f4f6}
.text-green-800{color:#065f46}
.text-green-100{color:#dcfce7}
.text-red-800{color:#991b1b}
.text-red-200{color:#fecaca}
.text-red-100{color:#fee2e2}
.text-cyan-800{color:#155e75}
.text-cyan-100{color:#cffafe}
.text-base{font-size:1rem;line-height:1.5rem}
.text-lg{font-size:1.125rem;line-height:1.75rem}
.text-2xl{font-size:1.5rem;line-height:2rem}
@@ -78,5 +101,12 @@ img{display:block;max-width:100%;height:auto}
/* Utilities */
.hover\:bg-transparent:hover{background-color:transparent}
.hover\:bg-gray-200:hover{background-color:#e5e7eb}
.hover\:bg-gray-600:hover{background-color:#4b5563}
.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}
.focus\:ring-blue-200:focus{--tw-ring-color:rgba(191, 219, 254, var(--tw-ring-opacity))}
.focus\:ring-blue-500:focus{--tw-ring-color:rgba(59, 130, 246, var(--tw-ring-opacity))}
.disabled\:opacity-50:disabled{opacity:.5}
.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}
/* Height for avatar images in header already inherit from container */

View File

@@ -6,7 +6,24 @@
<title>Nostr Relay</title>
<link rel="stylesheet" href="tailwind.min.css" />
</head>
<body class="bg-white">
<body>
<script>
// Apply system theme preference immediately to avoid flash of wrong theme
function applyTheme(isDark) {
document.body.classList.remove('bg-white', 'bg-gray-900');
document.body.classList.add(isDark ? 'bg-gray-900' : 'bg-white');
}
// Set initial theme
applyTheme(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches);
// Listen for theme changes
if (window.matchMedia) {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
applyTheme(e.matches);
});
}
</script>
<div id="root"></div>
<script type="module" src="/src/index.jsx"></script>
</body>

View File

@@ -39,26 +39,49 @@ img{display:block;max-width:100%;height:auto}
.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}
.border-0{border-width:0}.border-2{border-width:2px}
.border-white{border-color:#fff}
.border{border-width:1px}.border-gray-300{border-color:#d1d5db}
.border{border-width:1px}.border-gray-300{border-color:#d1d5db}.border-gray-600{border-color:#4b5563}
.border-red-500{border-color:#ef4444}.border-red-700{border-color:#b91c1c}
/* Colors / Backgrounds */
.bg-white{background-color:#fff}
.bg-gray-100{background-color:#f3f4f6}
.bg-gray-200{background-color:#e5e7eb}
.bg-gray-300{background-color:#d1d5db}
.bg-gray-600{background-color:#4b5563}
.bg-gray-700{background-color:#374151}
.bg-gray-800{background-color:#1f2937}
.bg-gray-900{background-color:#111827}
.bg-blue-500{background-color:#3b82f6}
.bg-blue-600{background-color:#2563eb}.hover\:bg-blue-700:hover{background-color:#1d4ed8}
.hover\:bg-blue-600:hover{background-color:#2563eb}
.bg-red-600{background-color:#dc2626}.hover\:bg-red-700:hover{background-color:#b91c1c}
.bg-cyan-100{background-color:#cffafe}
.bg-green-100{background-color:#d1fae5}
.bg-red-100{background-color:#fee2e2}
.bg-red-50{background-color:#fef2f2}
.bg-green-900{background-color:#064e3b}
.bg-red-900{background-color:#7f1d1d}
.bg-cyan-900{background-color:#164e63}
.bg-cover{background-size:cover}.bg-center{background-position:center}
.bg-transparent{background-color:transparent}
/* Text */
.text-left{text-align:left}
.text-white{color:#fff}
.text-gray-300{color:#d1d5db}
.text-gray-500{color:#6b7280}.hover\:text-gray-800:hover{color:#1f2937}
.hover\:text-gray-100:hover{color:#f3f4f6}
.text-gray-700{color:#374151}
.text-gray-800{color:#1f2937}
.text-gray-900{color:#111827}
.text-gray-100{color:#f3f4f6}
.text-green-800{color:#065f46}
.text-green-100{color:#dcfce7}
.text-red-800{color:#991b1b}
.text-red-200{color:#fecaca}
.text-red-100{color:#fee2e2}
.text-cyan-800{color:#155e75}
.text-cyan-100{color:#cffafe}
.text-base{font-size:1rem;line-height:1.5rem}
.text-lg{font-size:1.125rem;line-height:1.75rem}
.text-2xl{font-size:1.5rem;line-height:2rem}
@@ -78,5 +101,12 @@ img{display:block;max-width:100%;height:auto}
/* Utilities */
.hover\:bg-transparent:hover{background-color:transparent}
.hover\:bg-gray-200:hover{background-color:#e5e7eb}
.hover\:bg-gray-600:hover{background-color:#4b5563}
.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}
.focus\:ring-blue-200:focus{--tw-ring-color:rgba(191, 219, 254, var(--tw-ring-opacity))}
.focus\:ring-blue-500:focus{--tw-ring-color:rgba(59, 130, 246, var(--tw-ring-opacity))}
.disabled\:opacity-50:disabled{opacity:.5}
.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}
/* Height for avatar images in header already inherit from container */

View File

@@ -5,6 +5,9 @@ function App() {
const [status, setStatus] = useState('Ready to authenticate');
const [statusType, setStatusType] = useState('info');
const [profileData, setProfileData] = useState(null);
// Theme state for dark/light mode
const [isDarkMode, setIsDarkMode] = useState(false);
const [checkingAuth, setCheckingAuth] = useState(true);
@@ -26,6 +29,28 @@ function App() {
return () => window.removeEventListener('resize', updatePadding);
}, []);
// Effect to detect and track system theme preference
useEffect(() => {
// Check if the browser supports prefers-color-scheme
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
// Set the initial theme based on system preference
setIsDarkMode(darkModeMediaQuery.matches);
// Add listener to respond to system theme changes
const handleThemeChange = (e) => {
setIsDarkMode(e.matches);
};
// Modern browsers
darkModeMediaQuery.addEventListener('change', handleThemeChange);
// Cleanup listener on component unmount
return () => {
darkModeMediaQuery.removeEventListener('change', handleThemeChange);
};
}, []);
useEffect(() => {
// Check authentication status on page load
(async () => {
@@ -81,14 +106,16 @@ function App() {
function statusClassName() {
const base = 'mt-5 mb-5 p-3 rounded';
// Return theme-appropriate status classes
switch (statusType) {
case 'success':
return base + ' bg-green-100 text-green-800';
return base + ' ' + getThemeClasses('bg-green-100 text-green-800', 'bg-green-900 text-green-100');
case 'error':
return base + ' bg-red-100 text-red-800';
return base + ' ' + getThemeClasses('bg-red-100 text-red-800', 'bg-red-900 text-red-100');
case 'info':
default:
return base + ' bg-cyan-100 text-cyan-800';
return base + ' ' + getThemeClasses('bg-cyan-100 text-cyan-800', 'bg-cyan-900 text-cyan-100');
}
}
@@ -368,25 +395,108 @@ function App() {
}
}
// =========================
// Export Specific Pubkeys UI state and handlers (admin)
// =========================
const [exportPubkeys, setExportPubkeys] = useState([{ value: '' }]);
function isHex64(str) {
if (!str) return false;
const s = String(str).trim();
return /^[0-9a-fA-F]{64}$/.test(s);
}
function normalizeHex(str) {
return String(str || '').trim();
}
function addExportPubkeyField() {
// Add new field at the end of the list so it appears downwards
setExportPubkeys((arr) => [...arr, { value: '' }]);
}
function removeExportPubkeyField(idx) {
setExportPubkeys((arr) => arr.filter((_, i) => i !== idx));
}
function changeExportPubkey(idx, val) {
const v = normalizeHex(val);
setExportPubkeys((arr) => arr.map((item, i) => (i === idx ? { value: v } : item)));
}
function validExportPubkeys() {
return exportPubkeys
.map((p) => normalizeHex(p.value))
.filter((v) => v.length > 0 && isHex64(v));
}
function canExportSpecific() {
// Enable only if every opened field is non-empty and a valid 64-char hex
if (!exportPubkeys || exportPubkeys.length === 0) return false;
return exportPubkeys.every((p) => {
const v = normalizeHex(p.value);
return v.length === 64 && isHex64(v);
});
}
function handleExportSpecific() {
const vals = validExportPubkeys();
if (!vals.length) return;
const qs = vals.map((v) => `pubkey=${encodeURIComponent(v)}`).join('&');
try {
window.location.href = `/api/export?${qs}`;
} catch (_) {}
}
// Theme utility functions for conditional styling
function getThemeClasses(lightClass, darkClass) {
return isDarkMode ? darkClass : lightClass;
}
// Get background color class for container panels
function getPanelBgClass() {
return getThemeClasses('bg-gray-200', 'bg-gray-800');
}
// Get text color class for standard text
function getTextClass() {
return getThemeClasses('text-gray-700', 'text-gray-300');
}
// Get background color for buttons
function getButtonBgClass() {
return getThemeClasses('bg-gray-100', 'bg-gray-700');
}
// Get text color for buttons
function getButtonTextClass() {
return getThemeClasses('text-gray-500', 'text-gray-300');
}
// Get hover classes for buttons
function getButtonHoverClass() {
return getThemeClasses('hover:text-gray-800', 'hover:text-gray-100');
}
// Prevent UI flash: wait until we checked auth status
if (checkingAuth) {
return null;
}
return (
<>
<div className={`min-h-screen ${getThemeClasses('bg-gray-100', 'bg-gray-900')}`}>
{user?.permission ? (
<>
{/* Logged in view with user profile */}
<div className="sticky top-0 left-0 w-full bg-gray-100 z-50 h-16 flex items-center overflow-hidden">
<div className={`sticky top-0 left-0 w-full ${getThemeClasses('bg-gray-100', 'bg-gray-900')} z-50 h-16 flex items-center overflow-hidden`}>
<div className="flex items-center h-full w-full box-border">
<div className="relative overflow-hidden flex flex-grow items-center justify-start h-full">
{profileData?.banner && (
<div className="absolute inset-0 opacity-70 bg-cover bg-center" style={{ backgroundImage: `url(${profileData.banner})` }}></div>
)}
<div className="relative z-10 p-2 flex items-center h-full">
{profileData?.picture && <img src={profileData.picture} alt="User Avatar" className="h-full aspect-square w-auto rounded-full object-cover border-2 border-white mr-2 shadow box-border" />}
<div>
{profileData?.picture && <img src={profileData.picture} alt="User Avatar" className={`h-full aspect-square w-auto rounded-full object-cover border-2 ${getThemeClasses('border-white', 'border-gray-600')} mr-2 shadow box-border`} />}
<div className={getTextClass()}>
<div className="font-bold text-base block">
{profileData?.display_name || profileData?.name || user.pubkey.slice(0, 8)}
{profileData?.name && profileData?.display_name && ` (${profileData.name})`}
@@ -398,32 +508,34 @@ function App() {
</div>
</div>
<div className="flex items-center justify-end shrink-0 h-full">
<button className="bg-transparent text-gray-500 border-0 text-2xl cursor-pointer flex items-center justify-center h-full aspect-square shrink-0 hover:bg-transparent hover:text-gray-800" onClick={logout}></button>
<button className={`bg-transparent ${getButtonTextClass()} border-0 text-2xl cursor-pointer flex items-center justify-center h-full aspect-square shrink-0 hover:bg-transparent ${getButtonHoverClass()}`} onClick={logout}></button>
</div>
</div>
</div>
{/* Hidden file input for import (admin) */}
<input
type="file"
ref={fileInputRef}
onChange={handleImportChange}
accept=".json,.jsonl,text/plain,application/x-ndjson,application/json"
style={{ display: 'none' }}
/>
<div className="p-2 w-full bg-white border-b border-gray-200">
<div className="text-lg font-bold flex items-center">Welcome</div>
<p>here you can configure all the things</p>
</div>
{/* Dashboard content container - stacks vertically and fills remaining space */}
<div className="flex-grow overflow-y-auto p-4">
{/* Hidden file input for import (admin) */}
<input
type="file"
ref={fileInputRef}
onChange={handleImportChange}
accept=".json,.jsonl,text/plain,application/x-ndjson,application/json"
style={{ display: 'none' }}
/>
<div className={`m-2 p-2 w-full ${getPanelBgClass()} rounded-lg`}>
<div className={`text-lg font-bold flex items-center ${getTextClass()}`}>Welcome</div>
<p className={getTextClass()}>here you can configure all the things</p>
</div>
{/* Export only my events */}
<div className="p-2 bg-white rounded w-full">
<div className="w-full flex items-center justify-between">
<div className="pr-2 w-full">
<div className="text-base font-bold mb-1">Export My Events</div>
<p className="text-sm w-full text-gray-700">Download your own events as line-delimited JSON (JSONL/NDJSON). Only events you authored will be included.</p>
{/* Export only my events */}
<div className={`m-2 p-2 ${getPanelBgClass()} rounded-lg w-full`}>
<div className="w-full flex items-center justify-end p-2 bg-gray-900 rounded-lg">
<div className="pr-2 m-2 w-full">
<div className={`text-base font-bold mb-1 ${getTextClass()}`}>Export My Events</div>
<p className={`text-sm w-full ${getTextClass()}`}>Download your own events as line-delimited JSON (JSONL/NDJSON). Only events you authored will be included.</p>
</div>
<button
className="bg-gray-100 text-gray-500 border-0 text-2xl cursor-pointer flex items-center justify-center h-full aspect-square shrink-0 hover:bg-transparent hover:text-gray-800"
className={`${getButtonBgClass()} ${getButtonTextClass()} border-0 text-2xl cursor-pointer flex items-center justify-center h-full aspect-square shrink-0 hover:bg-transparent ${getButtonHoverClass()}`}
onClick={() => { window.location.href = '/api/export/mine'; }}
aria-label="Download my events as JSONL"
title="Download my events"
@@ -433,48 +545,121 @@ function App() {
</div>
</div>
{user.permission === "admin" && (
{user.permission === "admin" && (
<>
<div className="p-2 w-full rounded">
<div className="flex items-center justify-between">
<div className="pr-2 w-full">
<div className="text-base font-bold mb-1">Export All Events (admin)</div>
<p className="text-sm text-gray-700">Download all stored events as line-delimited JSON (JSONL/NDJSON). This may take a while on large databases.</p>
<div className={`m-2 p-2 ${getPanelBgClass()} rounded-lg w-full`}>
<div className="flex items-center justify-between p-2 m-4 bg-gray-900 round">
<div className="pr-2 w-full">
<div className={`text-base font-bold mb-1 ${getTextClass()}`}>Export All Events (admin)</div>
<p className={`text-sm ${getTextClass()}`}>Download all stored events as line-delimited JSON (JSONL/NDJSON). This may take a while on large databases.</p>
</div>
<button
className={`${getButtonBgClass()} ${getButtonTextClass()} border-0 text-2xl cursor-pointer flex m-2 items-center justify-center h-full aspect-square shrink-0 hover:bg-transparent ${getButtonHoverClass()}`}
onClick={() => { window.location.href = '/api/export'; }}
aria-label="Download all events as JSONL"
title="Download all events"
>
</button>
</div>
<button
className="bg-gray-100 text-gray-500 border-0 text-2xl cursor-pointer flex items-center justify-center h-full aspect-square shrink-0 hover:bg-transparent hover:text-gray-800"
onClick={() => { window.location.href = '/api/export'; }}
aria-label="Download all events as JSONL"
title="Download all events"
>
</button>
</div>
</div>
<div className="p-2 w-full rounded">
<div className="flex items-center justify-between">
<div className="pr-2 w-full">
<div className="text-base font-bold mb-1">Import Events (admin)</div>
<p className="text-sm text-gray-700">Upload events in line-delimited JSON (JSONL/NDJSON) to import into the database.</p>
{/* Export specific pubkeys (admin) */}
<div className={`m-2 p-2 ${getPanelBgClass()} rounded-lg w-full`}>
<div className="w-full flex items-start justify-between gap-4 m-2 p-2 bg-gray-900 rounded-lg">
{/* Left: title and help text */}
<div className="flex-1 pr-2 w-full">
<div className={`text-base font-bold mb-1 ${getTextClass()}`}>Export Specific Pubkeys (admin)</div>
<p className={`text-sm ${getTextClass()}`}>Enter one or more author pubkeys (64-character hex). Only valid entries will be exported.</p>
{/* Right: controls (buttons stacked vertically + list below) */}
<div className="flex flex-col items-end gap-2 self-end justify-end p-2">
<button
className={`${getButtonBgClass()} ${getTextClass()} text-base p-4 rounded m-2 ${getThemeClasses('hover:bg-gray-200', 'hover:bg-gray-600')}`}
onClick={addExportPubkeyField}
title="Add another pubkey"
type="button"
>
+ Add
</button>
</div>
<div className="flex flex-col items-end gap-2 min-w-[320px] justify-end p-2">
<div className="gap-2 justify-end">
{exportPubkeys.map((item, idx) => {
const v = (item?.value || '').trim();
const valid = v.length === 0 ? true : isHex64(v);
return (
<div key={idx} className="flex items-center gap-2 ">
<input
type="text"
inputMode="text"
autoComplete="off"
spellCheck="false"
className={`flex-1 text-sm px-2 py-1 border rounded outline-none ${valid
? getThemeClasses('border-gray-300 bg-white text-gray-900 focus:ring-2 focus:ring-blue-200', 'border-gray-600 bg-gray-700 text-gray-100 focus:ring-2 focus:ring-blue-500')
: getThemeClasses('border-red-500 bg-red-50 text-red-800', 'border-red-700 bg-red-900 text-red-200')}`}
placeholder="e.g., 64-hex pubkey"
value={v}
onChange={(e) => changeExportPubkey(idx, e.target.value)}
/>
<button
className={`${getButtonBgClass()} ${getTextClass()} px-2 py-1 rounded ${getThemeClasses('hover:bg-gray-200', 'hover:bg-gray-600')}`}
onClick={() => removeExportPubkeyField(idx)}
title="Remove this pubkey"
type="button"
>
</button>
</div>
);
})}
</div>
</div>
<div className="flex justify-end items-end gap-2 self-end">
<button
className={`${getThemeClasses('bg-blue-600', 'bg-blue-500')} text-white px-3 py-1 rounded disabled:opacity-50 disabled:cursor-not-allowed ${canExportSpecific() ? getThemeClasses('hover:bg-blue-700', 'hover:bg-blue-600') : ''}`}
onClick={handleExportSpecific}
disabled={!canExportSpecific()}
title={canExportSpecific() ? 'Download events for specified pubkeys' : 'Enter a valid 64-character hex pubkey in every field'}
type="button"
>
Export
</button>
</div>
</div>
</div>
</div>
<div className={`m-2 p-2 ${getPanelBgClass()} rounded-lg w-full`}>
<div className="flex items-center justify-between p-2 bg-gray-900 rounded-lg">
<div className="pr-2 w-full">
<div className={`text-base font-bold mb-1 ${getTextClass()}`}>Import Events (admin)</div>
<p className={`text-sm ${getTextClass()}`}>Upload events in line-delimited JSON (JSONL/NDJSON) to import into the database.</p>
</div>
<button
className={`${getButtonBgClass()} ${getButtonTextClass()} border-0 text-2xl cursor-pointer flex items-center justify-center h-full aspect-square shrink-0 hover:bg-transparent ${getButtonHoverClass()}`}
onClick={handleImportButton}
aria-label="Import events from JSONL"
title="Import events"
>
</button>
</div>
<button
className="bg-gray-100 text-gray-500 border-0 text-2xl cursor-pointer flex items-center justify-center h-full aspect-square shrink-0 hover:bg-transparent hover:text-gray-800"
onClick={handleImportButton}
aria-label="Import events from JSONL"
title="Import events"
>
</button>
</div>
</div>
</>
)}
)}
{/* Empty flex grow box to ensure background fills entire viewport */}
<div className={`flex-grow ${getThemeClasses('bg-gray-100', 'bg-gray-900')}`}></div>
</div>
</>
) : (
// Not logged in view - shows the login form
<div className="w-full h-full flex items-center justify-center">
<div
className="bg-gray-100"
className={getThemeClasses('bg-gray-100', 'bg-gray-900')}
style={{ width: '800px', maxWidth: '100%', boxSizing: 'border-box', padding: `${loginPadding}px` }}
>
<div className="flex items-center gap-3 mb-3">
@@ -489,22 +674,23 @@ function App() {
e.currentTarget.src = "/docs/orly.png";
}}
/>
<h1 ref={titleRef} className="text-2xl font-bold p-2">ORLY🦉 Dashboard Login</h1>
<h1 ref={titleRef} className={`text-2xl font-bold p-2 ${getTextClass()}`}>ORLY🦉 Dashboard Login</h1>
</div>
<p className="mb-4">Authenticate to this Nostr relay using your browser extension.</p>
<p className={`mb-4 ${getTextClass()}`}>Authenticate to this Nostr relay using your browser extension.</p>
<div className={statusClassName()}>
{status}
</div>
<div className="mb-5">
<button className="bg-blue-600 text-white px-5 py-3 rounded hover:bg-blue-700" onClick={loginWithExtension}>Login with Browser Extension (NIP-07)</button>
<button className={`${getThemeClasses('bg-blue-600', 'bg-blue-500')} text-white px-5 py-3 rounded ${getThemeClasses('hover:bg-blue-700', 'hover:bg-blue-600')}`} onClick={loginWithExtension}>Login with Browser Extension (NIP-07)</button>
</div>
</div>
</div>
)}
</>
<div className={`flex-grow ${getThemeClasses('bg-gray-100', 'bg-gray-900')}`}></div>
</div>
);
}

View File

@@ -70,11 +70,14 @@ func (d *D) QueryEvents(c context.Context, f *filter.F) (
// Process each successfully fetched event and apply filters
for serialValue, ev := range fetchedEvents {
idHex := idHexToSerial[serialValue]
// Convert serial value back to Uint40 for expiration handling
ser := new(types.Uint40)
if err = ser.Set(serialValue); err != nil {
log.T.F("QueryEvents: error converting serial %d: %v", serialValue, err)
log.T.F(
"QueryEvents: error converting serial %d: %v", serialValue,
err,
)
continue
}
@@ -241,7 +244,7 @@ func (d *D) QueryEvents(c context.Context, f *filter.F) (
// For replaceable events, we need to check if there are any
// e-tags that reference events with the same kind and pubkey
for _, eTag := range eTags {
if eTag.Len() < 2 {
if eTag.Len() != 64 {
continue
}
// Get the event ID from the e-tag