feat: show all events

This commit is contained in:
n 2025-04-08 16:30:41 +01:00
parent 10ebcbc917
commit 3483964353
8 changed files with 216 additions and 772 deletions

@ -37,7 +37,7 @@
<div style="margin-bottom: 10px;">
<label for="serverPubkey">Server Pubkey or Search Term:</label><br>
<div class="server-input-container">
<input type="text" id="serverPubkey" placeholder="npub, username, or NIP-05 identifier" value="npub1thq3fzcw393c0tpy60sz0dvvjz4tjrgtrudxsa68sldkf78fznksgp34w8" class="server-input">
<input type="text" id="serverPubkey" placeholder="npub, username, or NIP-05 identifier" class="server-input">
<button id="searchServerBtn" class="server-search-button">Search</button>
</div>
<div id="serverSearchResult" class="server-search-result" style="display: none;">

@ -28,25 +28,10 @@
<div class="info-box">
<p>View and manage your Nostr profile information. Connect with a NIP-07 extension or enter your keys manually.</p>
</div>
<h2>Your Nostr Identity</h2>
<div class="profile-section connection-status">
<div id="connectionStatus" class="connection-status-indicator">
Not connected to any extension
</div>
<button id="connectButton" class="connect-button">Connect with Extension</button>
<div class="manual-entry">
<details>
<summary>Manual Key Entry</summary>
<div class="manual-key-entry">
<label for="manualPubkey">Public Key (hex or npub):</label>
<input type="text" id="manualPubkey" placeholder="npub or hex pubkey">
<button id="setManualPubkeyBtn">Set Key</button>
</div>
</details>
</div>
<div id="connectionStatus" class="connection-status-indicator" hidden>
Not connected to any extension
</div>
</div>
<div id="profileContainer" class="profile-container hidden">

@ -46,6 +46,12 @@
<input type="text" id="relayUrl" value="wss://relay.degmods.com" placeholder="wss://relay.example.com">
<button id="connectRelayBtn" class="relay-connect-button">Connect</button>
</div>
<div class="filter-options">
<label class="filter-checkbox">
<input type="checkbox" id="showAllEvents" checked>
Show all kind 21120 events (not just addressed to me)
</label>
</div>
<div id="relayStatus" class="relay-status">Not connected</div>
</div>
</div>

@ -422,6 +422,29 @@ function handleCopyEvent(): void {
initNostrLogin();
document.addEventListener('DOMContentLoaded', function(): void {
// Add event listener for "Enter" key on the serverPubkey input field
const serverPubkeyInput = document.getElementById('serverPubkey') as HTMLInputElement;
if (serverPubkeyInput) {
serverPubkeyInput.addEventListener('keydown', async (event) => {
if (event.key === 'Enter') {
event.preventDefault();
await handleServerSearch();
// After search, check if we found a NIP-05 address that resolved to exactly one result
const resultDiv = document.getElementById('serverSearchResult');
if (resultDiv && resultDiv.querySelector('.search-results-list')) {
const resultItems = resultDiv.querySelectorAll('.search-result-item');
if (resultItems.length === 1) {
const npub = resultItems[0].getAttribute('data-npub');
if (npub) {
serverPubkeyInput.value = npub;
resultDiv.style.display = 'none';
}
}
}
}
});
}
// Set up the convert button click handler
const convertButton = document.getElementById('convertButton');
const searchButton = document.getElementById('searchServerBtn');

@ -57,55 +57,19 @@ async function connectWithExtension(): Promise<void> {
}
}
// Set manual pubkey
function setManualPubkey(pubkeyInput: string): void {
try {
let hexPubkey: string;
// Handle npub format
if (pubkeyInput.startsWith('npub')) {
try {
const decoded = nostrTools.nip19.decode(pubkeyInput);
if (decoded.type === 'npub') {
hexPubkey = decoded.data as string;
} else {
throw new Error('Invalid npub format');
}
} catch (error) {
updateConnectionStatus(`Invalid npub format: ${error}`, false);
return;
}
} else {
// Assume hex format
if (!/^[0-9a-f]{64}$/i.test(pubkeyInput)) {
updateConnectionStatus('Invalid hex pubkey format. Must be 64 hex characters.', false);
return;
}
hexPubkey = pubkeyInput;
}
currentPubkey = hexPubkey;
updateConnectionStatus(`Using manually entered pubkey: ${hexPubkey.substring(0, 8)}...`, true);
showProfile(hexPubkey);
} catch (error) {
console.error('Error setting manual pubkey:', error);
updateConnectionStatus(`Error: ${error instanceof Error ? error.message : String(error)}`, false);
}
}
// Manual pubkey functionality removed as users are automatically logged in
// Update connection status UI
function updateConnectionStatus(message: string, isConnected: boolean): void {
if (!connectionStatus) {return;}
// Just update the status message but keep it hidden
connectionStatus.textContent = message;
connectionStatus.className = `connection-status-indicator ${isConnected ? 'connected' : 'not-connected'}`;
// Always show the profile container regardless of connection state
if (profileContainer) {
if (isConnected) {
profileContainer.classList.remove('hidden');
} else {
profileContainer.classList.add('hidden');
}
profileContainer.classList.remove('hidden');
}
}
@ -373,26 +337,7 @@ document.addEventListener('DOMContentLoaded', () => {
statsResponsesReceived = document.getElementById('responsesReceived');
statsRelaysConnected = document.getElementById('relaysConnected');
// Connect button
const connectButton = document.getElementById('connectButton');
if (connectButton) {
connectButton.addEventListener('click', connectWithExtension);
}
// Manual pubkey button
const setManualPubkeyBtn = document.getElementById('setManualPubkeyBtn');
const manualPubkeyInput = document.getElementById('manualPubkey') as HTMLInputElement;
if (setManualPubkeyBtn && manualPubkeyInput) {
setManualPubkeyBtn.addEventListener('click', () => {
const pubkeyValue = manualPubkeyInput.value.trim();
if (pubkeyValue) {
setManualPubkey(pubkeyValue);
} else {
alert('Please enter a pubkey');
}
});
}
// Auto-connect, no manual button needed
// Copy npub button
const copyNpubBtn = document.getElementById('copyNpubBtn');
@ -406,10 +351,23 @@ document.addEventListener('DOMContentLoaded', () => {
refreshProfileBtn.addEventListener('click', refreshProfile);
}
// Try to connect automatically if extension is available
// Try to connect automatically
if (window.nostr) {
// If extension is available, try to connect with it
connectWithExtension().catch(error => {
console.error('Error auto-connecting:', error);
console.error('Error auto-connecting with extension:', error);
// Show profile container even if connection fails
if (profileContainer) {
profileContainer.classList.remove('hidden');
updateProfileWithDefaults();
}
});
} else {
// Even without an extension, show the profile container with default information
console.log('No Nostr extension available, showing default profile');
if (profileContainer) {
profileContainer.classList.remove('hidden');
updateProfileWithDefaults();
}
}
});

@ -186,9 +186,7 @@ function updateRelayStatus(message: string, className: string): void {
}
// Subscribe to events
async function subscribeToEvents(options: {
pubkeyFilter?: string;
}): Promise<void> {
async function subscribeToEvents(): Promise<void> {
if (!relayPool || !activeRelayUrl) {
console.error('Cannot subscribe: Relay pool or URL not set');
return;
@ -206,13 +204,9 @@ async function subscribeToEvents(options: {
console.log('Creating subscription for kind 21120 events');
// For now, we're going to fetch ALL kind 21120 events from the relay
// to ensure we're getting data
// Get the logged-in user's pubkey if available
const loggedInPubkey = getLoggedInPubkey();
console.log('Creating subscription for kind 21120 events addressed to the user');
// Define the filter type properly
interface NostrFilter {
kinds: number[];
@ -225,8 +219,12 @@ async function subscribeToEvents(options: {
kinds: [21120], // HTTP Messages event kind
};
// If the user is logged in, filter for events addressed to them
if (loggedInPubkey) {
// Get the status of the "Show all events" checkbox
const showAllEventsCheckbox = document.getElementById('showAllEvents') as HTMLInputElement;
const showAllEvents = showAllEventsCheckbox ? showAllEventsCheckbox.checked : false;
// If "Show all events" is not checked and the user is logged in, filter for events addressed to them
if (!showAllEvents && loggedInPubkey) {
let pubkeyHex = loggedInPubkey;
// Convert npub to hex if needed
@ -245,34 +243,20 @@ async function subscribeToEvents(options: {
filter['#p'] = [pubkeyHex];
console.log(`Filtering for events addressed to user: ${pubkeyHex}`);
} else {
console.log('No user logged in, showing all kind 21120 events');
// If "Show all events" is checked or no user is logged in
console.log('Showing all kind 21120 events');
// Explicitly remove any p-tag filter that might have been set previously
if (filter['#p']) {
delete filter['#p'];
}
}
// Log the filter we're using
console.log('Using filter:', JSON.stringify(filter));
// If a specific pubkey filter is provided in options, use it for p-tag filtering
// This replaces the logged-in user's pubkey filter with the specified one
if (options.pubkeyFilter) {
let pubkey = options.pubkeyFilter;
// Convert npub to hex if needed
if (pubkey.startsWith('npub')) {
try {
const hexPubkey = convertNpubToHex(pubkey);
if (hexPubkey) {
pubkey = hexPubkey;
}
} catch (error) {
console.error("Failed to convert npub to hex:", error);
}
}
// Set the p-tag filter to the specified pubkey
// This replaces any existing p-tag filter set by the logged-in user
filter['#p'] = [pubkey];
console.log(`Overriding p-tag filter to: ${pubkey}`);
}
// We're skipping the pubkeyFilter override on the server tab
// since we always want to use the logged-in user's pubkey based on checkbox state
// Skip using nostr-tools SimplePool for subscription to avoid issues
// Instead, create a direct WebSocket connection
@ -514,24 +498,6 @@ function showEventDetails(eventId: string): void {
// Ensure event has an ID (should already be verified)
const eventIdForDisplay = event.id ? event.id.substring(0, 8) : 'unknown';
const fullEventId = event.id || 'unknown';
// Generate tags HTML with better formatting
let tagsHtml = '';
event.tags.forEach((tag: string[]) => {
if (tag.length >= 2) {
// For p and e tags, add additional explanation
if (tag[0] === 'p') {
tagsHtml += `<li><strong>p:</strong> ${tag[1]} (p-tag: recipient/target)</li>`;
} else if (tag[0] === 'e') {
tagsHtml += `<li><strong>e:</strong> ${tag[1]} (e-tag: reference to another event)</li>`;
} else {
tagsHtml += `<li><strong>${tag[0]}:</strong> ${tag[1]}</li>`;
}
} else if (tag.length === 1) {
tagsHtml += `<li><strong>${tag[0]}</strong></li>`;
}
});
// Determine if this is a request or response
const isRequest = event.tags.some(tag => tag[0] === 'p');
@ -539,57 +505,60 @@ function showEventDetails(eventId: string): void {
const eventTypeLabel = isRequest ? 'HTTP Request' : (isResponse ? 'HTTP Response' : 'Unknown Type');
// Get the decrypted or original content
// Since we now encrypt just the HTTP request directly, this is just the HTTP content
const httpContent = receivedEvent.decrypted ?
(receivedEvent.decryptedContent || event.content) :
event.content;
// Display the event details
// Create raw JSON representation of the event
const rawJson = JSON.stringify(event, null, 2);
// Display the event details with a tabbed interface
eventDetails.innerHTML = `
<div class="event-detail-header">
<h3>${eventTypeLabel} (ID: ${eventIdForDisplay}...)</h3>
<div class="event-timestamp">${new Date(event.created_at * 1000).toLocaleString()}</div>
</div>
<div class="event-detail-metadata">
<div class="event-detail-item">
<strong>ID:</strong> <span class="metadata-value">${fullEventId}</span>
</div>
<div class="event-detail-item">
<strong>From:</strong> <span class="metadata-value">${event.pubkey}</span>
</div>
<div class="event-detail-item">
<strong>Created:</strong> <span class="metadata-value">${new Date(event.created_at * 1000).toLocaleString()}</span>
</div>
<div class="event-detail-item">
<strong>Tags:</strong>
<ul class="event-tags">${tagsHtml}</ul>
</div>
<div class="event-detail-tabs">
<button class="tab-btn" data-tab="raw-json">Raw JSON</button>
<button class="tab-btn active" data-tab="http-content">HTTP Content</button>
</div>
<div class="event-detail-content">
<div class="content-row">
<div class="event-metadata-column">
<div class="content-header">
<strong>Event Metadata</strong>
</div>
<div class="metadata-content">
<div>ID: ${fullEventId}</div>
<div>Pubkey: ${event.pubkey}</div>
<div>Created: ${new Date(event.created_at * 1000).toLocaleString()}</div>
${receivedEvent.decrypted ? '<div class="decrypted-badge">Decrypted</div>' : ''}
</div>
</div>
<div class="http-content-column">
<div class="content-header">
<strong>HTTP ${eventTypeLabel}</strong>
</div>
<pre class="http-content">${httpContent}</pre>
</div>
<div class="tab-content" id="raw-json">
<pre class="json-content">${rawJson}</pre>
</div>
<div class="tab-content active" id="http-content">
<pre class="http-content">${httpContent}</pre>
${!receivedEvent.decrypted ? '<div class="decryption-status">Attempting decryption...</div>' : ''}
</div>
</div>
`;
// Add event listeners for tab buttons
const tabButtons = eventDetails.querySelectorAll('.tab-btn');
tabButtons.forEach(button => {
button.addEventListener('click', () => {
// Get the target tab
const targetTabId = (button as HTMLElement).dataset.tab;
if (!targetTabId) {
return;
}
// Remove active class from all buttons and content sections
tabButtons.forEach(btn => btn.classList.remove('active'));
// Use non-null assertion since we already checked eventDetails isn't null at the beginning
eventDetails!.querySelectorAll('.tab-content').forEach(section => section.classList.remove('active'));
// Add active class to clicked button and corresponding content section
button.classList.add('active');
const targetSection = document.getElementById(targetTabId);
if (targetSection) {
targetSection.classList.add('active');
}
});
});
}
// Auto-decrypt event using NIP-44
@ -981,6 +950,7 @@ document.addEventListener('DOMContentLoaded', () => {
eventDetails = document.getElementById('eventDetails');
const connectRelayBtn = document.getElementById('connectRelayBtn');
const showAllEventsCheckbox = document.getElementById('showAllEvents') as HTMLInputElement;
// Tab functionality
setupTabNavigation();
@ -991,6 +961,34 @@ document.addEventListener('DOMContentLoaded', () => {
// Raw text input setup
setupRawTextInput();
// Setup event listener for the "Show all events" checkbox
if (showAllEventsCheckbox) {
showAllEventsCheckbox.addEventListener('change', async () => {
// Only resubscribe if we're already connected to a relay
if (relayPool && activeRelayUrl) {
console.log(`Checkbox changed to: ${showAllEventsCheckbox.checked ? 'show all events' : 'only show events addressed to me'}`);
try {
// No need to get the pubkey since we're not filtering based on it anymore
// Update status to indicate resubscription is in progress
updateRelayStatus('Updating subscription...', 'connecting');
// Resubscribe with the current checkbox state
await subscribeToEvents();
console.log(`Subscription updated with new filter settings`);
updateRelayStatus('Subscription updated ✓', 'connected');
} catch (error) {
console.error("Failed to update subscription:", error);
updateRelayStatus(`Subscription update failed: ${error instanceof Error ? error.message : String(error)}`, 'error');
}
} else {
console.log("Checkbox changed but no active relay connection");
}
});
}
// Connect to relay and automatically start subscription for logged-in user
if (connectRelayBtn) {
connectRelayBtn.addEventListener('click', async () => {
@ -1013,7 +1011,6 @@ document.addEventListener('DOMContentLoaded', () => {
if (success) {
// Get the logged-in user's pubkey from localStorage as the default
const userPubkey = getLoggedInPubkey();
const pubkeyFilter = userPubkey || '';
// Log the pubkey we're using for subscription
if (userPubkey) {
@ -1027,9 +1024,7 @@ document.addEventListener('DOMContentLoaded', () => {
// Update status to indicate subscription is in progress
updateRelayStatus('Subscribing...', 'connecting');
await subscribeToEvents({
pubkeyFilter
});
await subscribeToEvents();
// If no error was thrown, the subscription was successful
console.log(`Subscription initiated to ${activeRelayUrl}`);

@ -1,601 +0,0 @@
/* Styles for the HTTP to Nostr converter */
/* CSS Variables for themes */
:root {
/* Light theme (default) */
--bg-primary: #f8f9fa;
--bg-secondary: #ffffff;
--bg-tertiary: #f1f8ff;
--bg-info: #e2f0fd;
--text-primary: #212529;
--text-secondary: #495057;
--text-tertiary: #6c757d;
--border-color: #dee2e6;
--accent-color: #0d6efd;
--button-primary: #0d6efd;
--button-hover: #0b5ed7;
--button-success: #28a745;
--button-success-hover: #218838;
--button-login: #6c3483;
--button-login-hover: #5b2c6f;
--info-border: #0d6efd;
--code-bg: #f8f9fa;
}
/* Dark theme */
[data-theme="dark"] {
--bg-primary: #121212;
--bg-secondary: #1e1e1e;
--bg-tertiary: #252836;
--bg-info: #1a2634;
--text-primary: #e0e0e0;
--text-secondary: #b0b0b0;
--text-tertiary: #909090;
--border-color: #333333;
--accent-color: #3f87ff;
--button-primary: #3f87ff;
--button-hover: #2970e3;
--button-success: #2a9745;
--button-success-hover: #218838;
--button-login: #7c44a3;
--button-login-hover: #6c378f;
--info-border: #3f87ff;
--code-bg: #252525;
}
/* General layout */
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
transition: background-color 0.3s ease, color 0.3s ease;
}
/* Headings */
h1 {
color: var(--text-primary);
margin-bottom: 20px;
border-bottom: 2px solid var(--border-color);
padding-bottom: 10px;
}
h2 {
color: var(--text-secondary);
margin-top: 25px;
margin-bottom: 15px;
}
/* Top Navigation Bar */
.top-nav {
background-color: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
height: 50px;
display: flex;
width: 100%;
}
body {
padding-top: 70px; /* Added padding to account for fixed navbar */
}
.nav-left {
display: flex;
height: 100%;
margin-left: 20px;
}
.nav-right {
position: absolute;
top: 0;
right: 20px;
height: 50px;
display: flex;
align-items: center;
gap: 15px;
}
/* Theme toggle button in the nav bar - aligned with icons */
.theme-toggle-btn {
display: flex;
align-items: center;
justify-content: center;
background-color: transparent;
color: var(--text-primary);
border: none;
cursor: pointer;
font-size: 16px;
transition: all 0.3s ease;
padding: 0;
margin: 0;
}
.theme-toggle-btn:hover {
color: var(--accent-color);
}
/* Ensure icons are properly sized */
#themeIcon {
font-size: 18px;
}
/* Navigation Links - Tab Style */
.nav-link {
text-decoration: none;
color: var(--text-secondary);
font-weight: 600;
transition: all 0.3s ease;
height: 100%;
display: flex;
align-items: center;
padding: 0 25px;
position: relative;
}
.nav-link:hover {
color: var(--accent-color);
}
.nav-link.active {
color: var(--accent-color);
}
.nav-link.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 3px;
background-color: var(--accent-color);
}
/* Icon navigation links */
.nav-icon {
padding: 0 10px;
}
/* Legacy theme toggle styles for backward compatibility */
.theme-toggle-container {
display: none; /* Hide the old toggle container */
}
.theme-toggle {
display: none; /* Hide the old toggle */
}
.theme-toggle-text {
font-size: 14px;
}
/* Info box */
.info-box {
background-color: var(--bg-info);
border-left: 4px solid var(--info-border);
padding: 10px 15px;
margin-bottom: 20px;
border-radius: 0 4px 4px 0;
}
/* Form elements */
input[type="text"], textarea {
width: 100%;
padding: 8px 10px;
margin-bottom: 15px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 14px;
background-color: var(--bg-secondary);
color: var(--text-primary);
}
textarea {
height: 150px;
font-family: monospace;
}
/* Login container */
.login-container {
margin: 20px 0;
padding: 15px;
background-color: var(--bg-secondary);
border-radius: 4px;
border: 1px solid var(--border-color);
text-align: center;
}
.login-status {
margin-top: 10px;
min-height: 20px;
}
#nostr-login-container {
margin: 10px auto;
display: flex;
justify-content: center;
}
/* Buttons */
button {
background-color: var(--button-primary);
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
}
button:hover {
background-color: var(--button-hover);
}
/* Server input section */
.server-input-container {
display: flex;
gap: 8px;
margin-top: 5px;
}
.server-input {
flex-grow: 1;
margin-bottom: 0;
}
.server-search-button {
padding: 8px 15px;
}
.server-search-result {
margin-top: 10px;
padding: 10px;
background-color: var(--bg-secondary);
border-radius: 4px;
border: 1px solid var(--border-color);
}
/* Output section */
#output {
margin-top: 30px;
padding: 15px;
background-color: var(--bg-secondary);
border-radius: 4px;
border: 1px solid var(--border-color);
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
pre {
background-color: var(--code-bg);
padding: 15px;
border-radius: 4px;
overflow-x: auto;
font-family: 'Courier New', Courier, monospace;
font-size: 14px;
line-height: 1.4;
border: 1px solid var(--border-color);
}
/* Publish container */
.publish-container {
margin-top: 20px;
margin-bottom: 20px;
padding: 15px;
background-color: var(--bg-tertiary);
border-radius: 4px;
border: 1px solid var(--accent-color);
}
.publish-input-container {
display: flex;
gap: 8px;
margin-top: 10px;
margin-bottom: 10px;
}
.publish-input {
flex-grow: 1;
padding: 8px 10px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 14px;
background-color: var(--bg-secondary);
color: var(--text-primary);
}
.publish-button {
background-color: var(--button-success);
color: white;
border: none;
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
}
.publish-button:hover {
background-color: var(--button-success-hover);
}
.publish-result {
margin-top: 10px;
padding: 10px;
background-color: var(--bg-secondary);
border-radius: 4px;
border: 1px solid var(--border-color);
}
/* QR code container */
.qr-container {
margin-top: 20px;
text-align: center;
}
#qrCode {
margin: 0 auto;
max-width: 100%;
background-color: var(--bg-secondary);
padding: 15px;
border-radius: 4px;
}
.qr-info {
margin-top: 8px;
font-size: 14px;
color: var(--text-tertiary);
text-align: center;
}
.qr-frame-container {
width: 300px;
height: 300px;
margin: 0 auto;
position: relative;
background-color: white; /* QR codes need white background */
}
.qr-frame {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
transition: opacity 0.5s ease;
}
.qr-controls {
margin: 15px auto;
text-align: center;
}
.qr-controls button {
margin: 0 5px;
padding: 8px 15px;
}
/* Search results styling */
.search-results-list {
max-height: 300px;
overflow-y: auto;
background-color: var(--bg-secondary);
}
.search-result-item {
padding: 10px;
border-bottom: 1px solid var(--border-color);
display: flex;
flex-direction: column;
gap: 5px;
position: relative;
}
.search-result-item:last-child {
border-bottom: none;
}
.result-name {
font-weight: bold;
}
.result-npub {
font-family: monospace;
font-size: 12px;
color: var(--text-tertiary);
}
.result-nip05 {
font-size: 12px;
color: var(--accent-color);
}
.use-npub-btn {
align-self: flex-end;
padding: 5px 10px;
font-size: 12px;
margin-top: 5px;
}
/* Error and success messages */
.error-message {
color: #cc0000;
padding: 10px;
background-color: rgba(204, 0, 0, 0.1);
border-radius: 4px;
margin: 10px 0;
}
.success-message {
color: #008800;
padding: 10px;
background-color: rgba(0, 136, 0, 0.1);
border-radius: 4px;
margin: 10px 0;
}
/* Responsive adjustments */
@media (max-width: 768px) {
body {
padding-top: 60px;
padding-left: 10px;
padding-right: 10px;
}
.top-nav {
height: 45px;
}
.nav-left {
margin-left: 5px;
}
.nav-right {
right: 5px;
height: 45px;
gap: 10px;
}
.nav-link {
padding: 0 15px;
}
.content {
padding: 10px 0;
}
}
/* Tab Interface Styles */
.tabs {
display: flex;
border-bottom: 2px solid var(--border-color);
margin-bottom: 20px;
}
.tab-button {
padding: 10px 20px;
background: none;
border: none;
border-bottom: 2px solid transparent;
cursor: pointer;
font-weight: 500;
color: var(--text-secondary);
margin-right: 10px;
margin-bottom: -2px;
transition: all 0.3s ease;
}
.tab-button:hover {
color: var(--accent-color);
}
.tab-button.active {
color: var(--accent-color);
border-bottom: 2px solid var(--accent-color);
}
/* Tab content sections */
#relay-connection-section,
#qr-code-scanner-section,
#raw-event-input-section {
display: none;
margin-top: 20px;
margin-bottom: 20px;
}
#relay-connection-section.active,
#qr-code-scanner-section.active,
#raw-event-input-section.active {
display: block;
}
/* QR Scanner Styles */
.qr-scanner-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
}
.qr-viewport {
width: 100%;
max-width: 500px;
height: 300px;
position: relative;
overflow: hidden;
border: 3px solid var(--accent-color);
border-radius: 8px;
background-color: var(--bg-tertiary);
}
.qr-viewport video {
width: 100%;
height: 100%;
object-fit: cover;
}
.qr-controls {
display: flex;
gap: 10px;
margin-top: 10px;
}
.action-button {
padding: 8px 15px;
background-color: var(--button-primary);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.action-button:hover {
background-color: var(--button-hover);
}
.action-button:disabled {
background-color: var(--bg-tertiary);
color: var(--text-tertiary);
cursor: not-allowed;
}
.status-message {
margin-top: 10px;
padding: 8px 12px;
border-radius: 4px;
background-color: var(--bg-tertiary);
color: var(--text-secondary);
font-size: 14px;
}
/* Raw Input Styles */
.raw-input-container {
display: flex;
flex-direction: column;
gap: 10px;
}
.raw-input-container textarea {
width: 100%;
min-height: 200px;
padding: 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
background-color: var(--bg-tertiary);
color: var(--text-primary);
font-family: 'Courier New', monospace;
font-size: 14px;
resize: vertical;
}
.raw-input-container button {
align-self: flex-start;
}

@ -525,11 +525,27 @@ footer {
.filter-options {
margin: 15px 0;
padding: 8px 10px;
background-color: var(--bg-tertiary);
border-radius: 4px;
display: flex;
align-items: center;
}
.filter-options label {
margin-right: 15px;
display: inline-block;
cursor: pointer;
}
.filter-checkbox {
display: flex;
align-items: center;
gap: 8px;
}
.filter-checkbox input[type="checkbox"] {
margin-right: 5px;
}
.key-input {
@ -1001,11 +1017,73 @@ footer {
max-height: 150px;
overflow-y: auto;
}
.event-detail-content {
margin-top: 20px;
}
/* Event detail tabs */
.event-detail-tabs {
display: flex;
border-bottom: 2px solid var(--border-color);
margin-bottom: 15px;
}
.event-detail-tabs .tab-btn {
padding: 8px 16px;
background: none;
border: none;
border-bottom: 2px solid transparent;
cursor: pointer;
font-weight: 500;
color: var(--text-secondary);
margin-right: 10px;
margin-bottom: -2px;
transition: all 0.3s ease;
}
.event-detail-tabs .tab-btn:hover {
color: var(--accent-color);
}
.event-detail-tabs .tab-btn.active {
color: var(--accent-color);
border-bottom: 2px solid var(--accent-color);
}
.event-detail-content .tab-content {
display: none;
}
.event-detail-content .tab-content.active {
display: block;
}
.json-content {
margin-top: 10px;
padding: 15px;
border-radius: 4px;
background-color: var(--bg-tertiary);
min-height: 200px;
max-height: 400px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.4;
white-space: pre-wrap;
border-left: 4px solid var(--accent-color);
}
.decryption-status {
margin-top: 10px;
padding: 8px;
border-radius: 4px;
background-color: #fff3cd;
color: #856404;
border: 1px solid #ffeeba;
text-align: center;
font-style: italic;
}
.content-header {
margin-bottom: 10px;
color: var(--accent-color);