fix: layout

This commit is contained in:
n 2025-04-10 12:15:20 +01:00
parent 865710390a
commit a00417685c
10 changed files with 2358 additions and 172 deletions

@ -6,10 +6,11 @@
<title>HTTP Messages - SERVER</title>
<link rel="stylesheet" href="./styles.css">
<link rel="stylesheet" href="./styles/event-list.css">
<link rel="stylesheet" href="./styles/http-messages-table.css">
<script defer src="./server.bundle.js"></script>
<!-- Additional chunks will be loaded automatically -->
</head>
<body>
<body class="server-page">
<!-- Navigation bar container - content will be injected by navbar.ts -->
<div id="navbarContainer" class="top-nav">
<!-- Navbar content will be injected here -->
@ -95,50 +96,57 @@
<div id="rawInputStatus" class="status-message"></div>
</div>
</div>
<h2>Received Events</h2>
<h2>HTTP Messages</h2>
<div class="received-events">
<div class="events-container">
<div class="events-sidebar">
<div id="eventsList" class="events-list">
<div class="empty-state">
No events received yet. Use one of the methods above to receive events.
</div>
<!-- Events will be displayed here -->
<!-- Table-based view for HTTP messages with expandable rows -->
<div id="httpMessagesTableContainer"></div>
<!-- Keep the event details and responses sections for compatibility with existing code -->
<div style="display: none;">
<div id="eventDetails" class="event-details">
<div class="empty-state">
Select a request to view details
</div>
</div>
<div class="events-content">
<div id="eventDetails" class="event-details">
<div id="relatedResponses" class="related-responses-section">
<h3>Responses</h3>
<div id="responsesList" class="responses-list">
<div class="empty-state">
Select an event to view details
No responses available
</div>
<!-- Selected event details will be shown here -->
</div>
<!-- HTTP Response Modal -->
<div id="httpResponseModal" class="http-response-modal" style="display: none;">
<div class="http-response-container">
<div class="http-response-header">
<h3>HTTP Response</h3>
<button class="close-modal-btn">&times;</button>
</div>
<div class="http-response-tabs">
<button class="tab-btn active" data-tab="formatted-response">Formatted</button>
<button class="tab-btn" data-tab="raw-response">Raw</button>
</div>
<div class="http-response-content">
<div class="tab-content active" id="formatted-response">
<div class="http-formatted-container">
<!-- Formatted HTTP response will be shown here -->
</div>
</div>
<div class="tab-content" id="raw-response">
<pre><!-- Raw HTTP response will be shown here --></pre>
</div>
</div>
</div>
</div>
</div>
<!-- Move the 21121 Response JSON container to be completely hidden -->
<div id="response21121Json" class="response-json-container" style="display: none; position: absolute; visibility: hidden; z-index: -9999;">
<pre class="json-content">
<!-- 21121 response JSON content will be stored here but displayed in the modal -->
</pre>
</div>
<!-- HTTP Response Modal -->
<div id="httpResponseModal" class="http-response-modal" style="display: none;">
<div class="http-response-container">
<div class="http-response-header">
<h3>HTTP Response</h3>
<button class="close-modal-btn">&times;</button>
</div>
<div class="http-response-tabs">
<button class="tab-btn active" data-tab="formatted-response">Formatted</button>
<button class="tab-btn" data-tab="raw-response">Raw</button>
</div>
<div class="http-response-content">
<div class="tab-content active" id="formatted-response">
<div class="http-formatted-container">
<!-- Formatted HTTP response will be shown here -->
</div>
</div>
<div class="tab-content" id="raw-response">
<pre><!-- Raw HTTP response will be shown here --></pre>
</div>
</div>
</div>
</div>

@ -71,6 +71,15 @@ export class EventList {
this.container.classList.add(this.options.className);
}
// Check if this is the requests-only sidebar
const isRequestsSidebar = this.container.classList.contains('requests-only');
if (isRequestsSidebar) {
console.log('Initializing EventList in requests-only mode');
// Force filtering to only show 21120 events
this.eventTypeFilters.set('21120', true);
this.eventTypeFilters.set('21121', false);
}
// Create the UI structure
this.createUIStructure();
@ -805,6 +814,7 @@ export class EventList {
const eventItem = document.createElement('div');
eventItem.className = 'event-item';
eventItem.dataset.id = eventId;
eventItem.dataset.kind = event.kind.toString();
// Add tabindex for keyboard navigation
eventItem.tabIndex = 0;
@ -968,6 +978,11 @@ export class EventList {
// Add click handler to select this event
eventItem.addEventListener('click', () => {
this.eventManager.selectEvent(eventId);
// If this is a 21120 event (request), also show its responses
if (event.kind === 21120) {
this.displayResponsesForRequest(eventId);
}
});
// Add keyboard handling for accessibility
@ -976,6 +991,11 @@ export class EventList {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.eventManager.selectEvent(eventId);
// If this is a 21120 event (request), also show its responses
if (event.kind === 21120) {
this.displayResponsesForRequest(eventId);
}
}
// Navigate with arrow keys
@ -1144,4 +1164,105 @@ export class EventList {
this.allEventIds = [];
this.filteredEventIds = [];
}
}
/**
* Display response events for a given request event
* @param requestId The ID of the 21120 request event
*/
private displayResponsesForRequest(requestId: string): void {
// Find the responses list container
const responsesListContainer = document.getElementById('responsesList');
if (!responsesListContainer) return;
// Get related response events from EventManager
const responses = this.eventManager.getResponsesForRequest(requestId);
// Clear existing content
responsesListContainer.innerHTML = '';
// Show empty state if no responses
if (responses.length === 0) {
responsesListContainer.innerHTML = `
<div class="empty-state">
No responses available for this request
</div>
`;
return;
}
// Create a response item for each related response
responses.forEach(response => {
const responseItem = document.createElement('div');
responseItem.className = 'response-item';
responseItem.dataset.id = response.id;
// Format timestamp
const timestamp = new Date(response.event.created_at * 1000).toLocaleTimeString();
// Check if decrypted
const isDecrypted = response.decrypted;
// Extract status code if decrypted
let statusInfo = '';
if (isDecrypted && response.decryptedContent) {
const statusMatch = response.decryptedContent.match(/^HTTP\/[\d.]+ (\d+)/);
if (statusMatch) {
const statusCode = statusMatch[1];
let statusClass = '';
if (statusCode.startsWith('2')) statusClass = 'status-success';
else if (statusCode.startsWith('3')) statusClass = 'status-redirect';
else if (statusCode.startsWith('4')) statusClass = 'status-client-error';
else if (statusCode.startsWith('5')) statusClass = 'status-server-error';
statusInfo = `<span class="status-code ${statusClass}">Status: ${statusCode}</span>`;
}
}
// Set HTML content
responseItem.innerHTML = `
<div class="response-header">
<div class="response-time">${timestamp}</div>
${statusInfo}
<div class="encryption-status ${isDecrypted ? 'decrypted' : 'encrypted'}">
${isDecrypted ? '🔓 Decrypted' : '🔒 Encrypted'}
</div>
</div>
<div class="response-id">ID: ${response.id.substring(0, 8)}...</div>
`;
// Add click handler to select this response event
responseItem.addEventListener('click', () => {
this.eventManager.selectEvent(response.id);
});
// Add to container
responsesListContainer.appendChild(responseItem);
});
// Also update JSON display for most recent response
if (responses.length > 0) {
const mostRecentResponse = responses[0];
this.displayResponseJson(mostRecentResponse.event);
}
}
/**
* Display raw JSON for a 21121 response event
* @param event The 21121 response event
*/
private displayResponseJson(event: NostrEvent): void {
const jsonContainer = document.getElementById('response21121Json');
if (!jsonContainer) return;
// Make container visible
jsonContainer.style.display = 'block';
// Get the pre element
const pre = jsonContainer.querySelector('pre.json-content');
if (!pre) return;
// Format JSON with indentation
pre.textContent = JSON.stringify(event, null, 2);
}
}

@ -0,0 +1,593 @@
/**
* HttpMessagesTable Component
* Handles the table view for HTTP messages with simple rows
* Shows only basic information (sender npub, timestamp, event ID) in each row
* Includes a modal dialog to show detailed information when a row is clicked
*/
import { EventChangeType, EventKind } from '../services/EventManager';
import type { EventManager } from '../services/EventManager';
import { nip19 } from 'nostr-tools';
import { HttpFormatter } from '../services/HttpFormatter';
export class HttpMessagesTable {
private container: HTMLElement | null = null;
private eventManager: EventManager;
private tableBody: HTMLElement | null = null;
private unregisterListener: (() => void) | null = null;
private modal: HTMLElement | null = null;
private modalContent: HTMLElement | null = null;
private currentEventId: string | null = null;
/**
* Create a new HttpMessagesTable component
*/
constructor(eventManager: EventManager, containerId: string) {
this.eventManager = eventManager;
this.container = document.getElementById(containerId);
}
/**
* Initialize the component and render the initial UI
*/
public initialize(): void {
if (!this.container) {
console.error('HTTP messages table container not found');
return;
}
// Create the table structure
this.createTableStructure();
// Create the modal dialog
this.createModalDialog();
// Register for event changes
this.unregisterListener = this.eventManager.registerListener((eventId, changeType) => {
const event = this.eventManager.getEvent(eventId);
switch (changeType) {
case EventChangeType.Added:
// If it's a 21120 event, render it
if (event && event.event.kind === EventKind.HttpRequest) {
this.renderEventRow(eventId);
}
// If it's a 21121 event, update indicators for related request
if (event && event.event.kind === EventKind.HttpResponse) {
this.updateResponseIndicators();
// If the modal is currently open, refresh its content
this.refreshModalIfNeeded(eventId);
}
break;
case EventChangeType.Removed:
this.removeEventRow(eventId);
break;
case EventChangeType.Updated:
this.updateEventRow(eventId);
// If it's a 21121 event update, refresh indicators
if (event && event.event.kind === EventKind.HttpResponse) {
this.updateResponseIndicators();
// If the modal is currently open, refresh its content
this.refreshModalIfNeeded(eventId);
}
break;
case EventChangeType.Selected:
this.highlightSelectedRow(eventId);
break;
}
});
// Render existing events
this.renderExistingEvents();
}
/**
* Create the table structure
*/
private createTableStructure(): void {
if (!this.container) { return; }
const tableHtml = `
<table class="http-messages-table">
<thead>
<tr>
<th>Sender</th>
<th>Time</th>
<th>Event ID</th>
<th>Response</th>
</tr>
</thead>
<tbody id="httpMessagesTableBody">
<tr class="table-empty-state">
<td colspan="4">No HTTP messages received yet. Use one of the methods above to receive events.</td>
</tr>
</tbody>
</table>
`;
this.container.innerHTML = tableHtml;
this.tableBody = document.getElementById('httpMessagesTableBody');
}
/**
* Render all existing events from the EventManager
*/
private renderExistingEvents(): void {
if (!this.tableBody) { return; }
// Clear existing content
this.tableBody.innerHTML = '';
// Get all events from the EventManager
const events = this.eventManager.getAllEvents();
// Filter to only show kind 21120 events
const filteredEvents = events.filter(e => e.event.kind === 21120);
// If no events, show the empty state
if (filteredEvents.length === 0) {
this.tableBody.innerHTML = `
<tr class="table-empty-state">
<td colspan="4">No HTTP messages received yet. Use one of the methods above to receive events.</td>
</tr>
`;
return;
}
// Sort events by received time (newest first)
filteredEvents.sort((a, b) => b.receivedAt - a.receivedAt);
// Render each event
filteredEvents.forEach(event => {
this.renderEventRow(event.id);
});
}
/**
* Render a single event row
*/
private renderEventRow(eventId: string): HTMLElement | null {
if (!this.tableBody) { return null; }
// Get the event from EventManager
const managedEvent = this.eventManager.getEvent(eventId);
if (!managedEvent || managedEvent.event.kind !== 21120) { return null; }
const event = managedEvent.event;
// Check if the row already exists
const existingRow = document.getElementById(`event-row-${eventId}`);
if (existingRow) {
// If it exists, just update it
this.updateEventRow(eventId);
return existingRow as HTMLElement;
}
// Create a new row for the event
const row = document.createElement('tr');
row.id = `event-row-${eventId}`;
row.dataset.eventId = eventId;
row.className = 'event-row';
// Format sender (pubkey) as npub
const senderPubkey = event.pubkey;
let npubSender;
try {
// Use nip19 from nostr-tools to encode the pubkey to npub format
const fullNpub = nip19.npubEncode(senderPubkey);
// Shorten the npub for display
npubSender = `${fullNpub.substring(0, 8)}...${fullNpub.substring(fullNpub.length - 4)}`;
} catch (e) {
console.error('Error encoding npub:', e);
npubSender = senderPubkey.substring(0, 8) + '...';
}
// Format timestamp
const timestamp = new Date(event.created_at * 1000).toLocaleTimeString();
// Format event ID
const shortEventId = event.id ? (event.id.substring(0, 8) + '...') : 'Unknown ID';
// Set row HTML
// Check if this event has responses
const hasResponses = this.eventManager.hasRelatedEvents(eventId);
const responseIndicator = hasResponses
? '<span class="response-indicator has-response" title="Has 21121 response">●</span>'
: '<span class="response-indicator no-response" title="No 21121 response">○</span>';
row.innerHTML = `
<td class="sender-cell" title="${senderPubkey}">${npubSender}</td>
<td class="time-cell">${timestamp}</td>
<td class="event-id-cell" title="${event.id}">${shortEventId}</td>
<td class="response-cell">${responseIndicator}</td>
`;
// Add click handler for row selection
row.addEventListener('click', () => {
// Select the event in the event manager and highlight the row
this.eventManager.selectEvent(eventId);
this.highlightSelectedRow(eventId);
// Show the modal with event details
this.showModal(eventId);
});
// Remove any empty state row if present
const emptyStateRow = this.tableBody.querySelector('.table-empty-state');
if (emptyStateRow) {
emptyStateRow.remove();
}
// Add to table at the top for new events
if (this.tableBody.firstChild) {
this.tableBody.insertBefore(row, this.tableBody.firstChild);
} else {
this.tableBody.appendChild(row);
}
return row;
}
/**
* Create the modal dialog structure
*/
private createModalDialog(): void {
// Create modal element
this.modal = document.createElement('div');
this.modal.className = 'http-messages-modal';
this.modal.style.display = 'none';
// Create close button
const closeButton = document.createElement('button');
closeButton.className = 'modal-close-button';
closeButton.innerHTML = '&times;';
closeButton.addEventListener('click', () => this.hideModal());
// Create modal content
this.modalContent = document.createElement('div');
this.modalContent.className = 'modal-content';
// Create tabs
const tabsContainer = document.createElement('div');
tabsContainer.className = 'modal-tabs';
const tabs = [
{ id: 'tab-21120-json', label: '21120 Raw JSON' },
{ id: 'tab-21120-http', label: '21120 HTTP Request' },
{ id: 'tab-21121-http', label: '21121 HTTP Response' },
{ id: 'tab-21121-json', label: '21121 Raw JSON' }
];
// Create tab elements
tabs.forEach(tab => {
const tabButton = document.createElement('button');
tabButton.className = 'modal-tab';
tabButton.id = tab.id;
tabButton.textContent = tab.label;
tabButton.dataset.tabId = tab.id + '-content';
tabButton.addEventListener('click', (e) => this.switchTab(e));
tabsContainer.appendChild(tabButton);
});
// Create tab content containers
const tabContentsContainer = document.createElement('div');
tabContentsContainer.className = 'modal-tab-contents';
tabs.forEach(tab => {
const tabContent = document.createElement('div');
tabContent.className = 'modal-tab-content';
tabContent.id = tab.id + '-content';
tabContentsContainer.appendChild(tabContent);
});
// Assemble the modal
this.modalContent.appendChild(tabsContainer);
this.modalContent.appendChild(tabContentsContainer);
this.modal.appendChild(closeButton);
this.modal.appendChild(this.modalContent);
// Add modal to document body
document.body.appendChild(this.modal);
// Add event listener to close modal when clicking outside
window.addEventListener('click', (event) => {
if (event.target === this.modal) {
this.hideModal();
}
});
}
/**
* Show the modal dialog with event details
* @param eventId The ID of the event to show details for
*/
private showModal(eventId: string): void {
if (!this.modal) return;
this.currentEventId = eventId;
// Load data for all tabs
this.loadEventData(eventId);
// Show the modal
this.modal.style.display = 'block';
// Select the first tab by default
const firstTab = this.modal?.querySelector('.modal-tab') as HTMLElement;
if (firstTab) {
firstTab.click();
}
}
/**
* Hide the modal dialog
*/
private hideModal(): void {
if (!this.modal) return;
this.modal.style.display = 'none';
this.currentEventId = null;
}
/**
* Switch between tabs in the modal
* @param event The click event
*/
private switchTab(event: Event): void {
const clickedTab = event.currentTarget as HTMLElement;
if (!clickedTab || !clickedTab.dataset.tabId) return;
// Hide all tab contents
const tabContents = document.querySelectorAll('.modal-tab-content');
tabContents.forEach(content => {
(content as HTMLElement).style.display = 'none';
});
// Remove active class from all tabs
const tabs = document.querySelectorAll('.modal-tab');
tabs.forEach(tab => {
tab.classList.remove('active');
});
// Show selected tab content
const selectedContent = document.getElementById(clickedTab.dataset.tabId);
if (selectedContent) {
selectedContent.style.display = 'block';
}
// Add active class to clicked tab
clickedTab.classList.add('active');
}
/**
* Load event data for the modal tabs
* @param eventId The ID of the 21120 event to load data for
*/
private loadEventData(eventId: string): void {
// Get the 21120 event data
const requestEvent = this.eventManager.getEvent(eventId);
if (!requestEvent || requestEvent.event.kind !== EventKind.HttpRequest) {
console.error('Invalid or missing request event');
return;
}
// Load 21120 Raw JSON tab content
const requestJsonTab = document.getElementById('tab-21120-json-content');
if (requestJsonTab) {
requestJsonTab.innerHTML = `<pre class="json-content">${JSON.stringify(requestEvent.event, null, 2)}</pre>`;
}
// Load 21120 HTTP Request tab content
const requestHttpTab = document.getElementById('tab-21120-http-content');
if (requestHttpTab) {
// Get the HTTP request content from the event
const requestContent = requestEvent.decryptedContent || requestEvent.event.content;
const formattedRequest = HttpFormatter.formatHttpContent(requestContent, true, false);
requestHttpTab.innerHTML = formattedRequest;
}
// Try to find related 21121 response event
const responseEventIds = this.eventManager.getRelatedEventIds(eventId);
if (responseEventIds.length > 0) {
// Get the first response
const responseEvent = this.eventManager.getEvent(responseEventIds[0]);
if (responseEvent && responseEvent.event.kind === EventKind.HttpResponse) {
// Load 21121 HTTP Response tab content
const responseHttpTab = document.getElementById('tab-21121-http-content');
if (responseHttpTab) {
const responseContent = responseEvent.decryptedContent || responseEvent.event.content;
const formattedResponse = HttpFormatter.formatHttpContent(responseContent, false, true);
responseHttpTab.innerHTML = formattedResponse;
}
// Load 21121 Raw JSON tab content
const responseJsonTab = document.getElementById('tab-21121-json-content');
if (responseJsonTab) {
// First check if there's a pre-processed JSON in the response21121Json container
const responseJsonContainer = document.getElementById('response21121Json');
const preElement = responseJsonContainer?.querySelector('pre.json-content');
if (preElement && preElement.textContent && preElement.textContent.trim() !== '') {
// Use the pre-processed JSON if available
responseJsonTab.innerHTML = `<pre class="json-content">${preElement.textContent}</pre>`;
} else {
// Otherwise, format it here
responseJsonTab.innerHTML = `<pre class="json-content">${JSON.stringify(responseEvent.event, null, 2)}</pre>`;
}
}
} else {
this.setNoResponseContent();
}
} else {
this.setNoResponseContent();
}
}
/**
* Set content for when no response is available
*/
private setNoResponseContent(): void {
// Set empty content for response tabs
const responseHttpTab = document.getElementById('tab-21121-http-content');
if (responseHttpTab) {
responseHttpTab.innerHTML = '<div class="no-response">No HTTP response available for this request</div>';
}
const responseJsonTab = document.getElementById('tab-21121-json-content');
if (responseJsonTab) {
responseJsonTab.innerHTML = '<div class="no-response">No 21121 event available for this request</div>';
}
}
/**
* Update an existing event row
*/
private updateEventRow(eventId: string): void {
const row = document.getElementById(`event-row-${eventId}`);
if (!row) {
// If row doesn't exist, create it
this.renderEventRow(eventId);
return;
}
const managedEvent = this.eventManager.getEvent(eventId);
if (!managedEvent || managedEvent.event.kind !== 21120) { return; }
const event = managedEvent.event;
// Update timestamp
const timeCell = row.querySelector('.time-cell');
if (timeCell) {
timeCell.textContent = new Date(event.created_at * 1000).toLocaleTimeString();
}
// Update response indicator
const responseCell = row.querySelector('.response-cell');
if (responseCell) {
const hasResponses = this.eventManager.hasRelatedEvents(eventId);
const responseIndicator = hasResponses
? '<span class="response-indicator has-response" title="Has 21121 response">●</span>'
: '<span class="response-indicator no-response" title="No 21121 response">○</span>';
responseCell.innerHTML = responseIndicator;
}
}
/**
* Remove an event row
*/
private removeEventRow(eventId: string): void {
const row = document.getElementById(`event-row-${eventId}`);
if (row) { row.remove(); }
// No need to handle expandable rows or expandedRowId anymore
// Check if we need to show the empty state
if (this.tableBody && !this.tableBody.querySelector('.event-row')) {
this.tableBody.innerHTML = `
<tr class="table-empty-state">
<td colspan="4">No HTTP messages received yet. Use one of the methods above to receive events.</td>
</tr>
`;
}
}
/**
* Highlight the selected row
*/
private highlightSelectedRow(eventId: string): void {
// Remove selected class from all rows
const rows = document.querySelectorAll('.event-row');
rows.forEach(row => row.classList.remove('selected'));
// Add selected class to the specified row
const selectedRow = document.getElementById(`event-row-${eventId}`);
if (selectedRow) {
selectedRow.classList.add('selected');
// This highlights the row to indicate selection
// Detailed content will be shown in a separate panel by the event manager
}
}
/**
* Clean up resources when component is destroyed
*/
public dispose(): void {
// Unregister event manager listener
if (this.unregisterListener) {
this.unregisterListener();
this.unregisterListener = null;
}
// Remove modal from document
if (this.modal && document.body.contains(this.modal)) {
document.body.removeChild(this.modal);
this.modal = null;
}
}
/**
* Refresh modal content if it's currently open and the event is related to the displayed request
* @param responseEventId The ID of the response event that was added or updated
*/
private refreshModalIfNeeded(responseEventId: string): void {
// Only proceed if modal is open and we have a current event
if (!this.modal || this.modal.style.display === 'none' || !this.currentEventId) {
return;
}
// Get the request event ID that this response is for
const requestEventId = this.eventManager.getRequestIdForResponse(responseEventId);
// If the response is for the currently displayed request, refresh the modal
if (requestEventId === this.currentEventId) {
this.loadEventData(this.currentEventId);
// Show notification in the modal that content was updated
const notification = document.createElement('div');
notification.className = 'modal-update-notification';
notification.textContent = 'Response data updated!';
this.modalContent?.appendChild(notification);
// Remove notification after 3 seconds
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 3000);
}
}
/**
* Update response indicators for all rows
* This can be called when a new 21121 response is received
*/
public updateResponseIndicators(): void {
if (!this.tableBody) return;
const rows = this.tableBody.querySelectorAll('.event-row');
rows.forEach(row => {
const eventId = row.getAttribute('data-event-id');
if (eventId) {
const hasResponses = this.eventManager.hasRelatedEvents(eventId);
const indicatorElement = row.querySelector('.response-indicator');
if (indicatorElement) {
if (hasResponses) {
indicatorElement.classList.add('has-response');
indicatorElement.classList.remove('no-response');
indicatorElement.setAttribute('title', 'Has 21121 response');
indicatorElement.textContent = '●';
} else {
indicatorElement.classList.add('no-response');
indicatorElement.classList.remove('has-response');
indicatorElement.setAttribute('title', 'No 21121 response');
indicatorElement.textContent = '○';
}
}
}
});
}
}

@ -14,6 +14,9 @@ import { HttpRequestExecutor } from './HttpRequestExecutor';
import { ResponseViewer } from './ResponseViewer';
import { EventList } from './EventList';
import { EventDetail } from './EventDetail';
import { HttpMessagesTable } from './HttpMessagesTable';
import { Nostr21121EventHandler } from '../services/Nostr21121EventHandler';
import { NostrService } from '../services/NostrService';
/**
@ -38,14 +41,17 @@ export class ServerUI {
private relayService: NostrRelayService;
private cacheService: NostrCacheService;
private nostrEventService: NostrEventService;
private nostrService: NostrService;
private httpService: HttpService;
private httpClient: HttpClient;
private nostr21121EventHandler: Nostr21121EventHandler;
// UI components
private eventList: EventList;
private eventDetail: EventDetail;
private httpRequestExecutor: HttpRequestExecutor;
private responseViewer: ResponseViewer;
private httpMessagesTable: HttpMessagesTable;
/**
* Create a new ServerUI component
@ -77,6 +83,16 @@ export class ServerUI {
updateStatusCallback
);
// Create a NostrService instance for the 21121 event handler
this.nostrService = new NostrService(updateStatusCallback);
// Initialize the Nostr21121EventHandler for automatic responses
this.nostr21121EventHandler = new Nostr21121EventHandler(
this.nostrService,
this.eventManager,
this.httpClient
);
// Initialize UI components
this.eventList = new EventList(this.eventManager, {
container: this.options.eventListContainer
@ -86,6 +102,9 @@ export class ServerUI {
container: this.options.eventDetailContainer
});
// Initialize the HTTP Messages Table
this.httpMessagesTable = new HttpMessagesTable(this.eventManager, 'httpMessagesTableContainer');
// Initialize HTTP components
this.httpRequestExecutor = new HttpRequestExecutor({
eventManager: this.eventManager,
@ -111,9 +130,14 @@ export class ServerUI {
// Initialize UI components
this.eventList.initialize();
this.eventDetail.initialize();
this.httpMessagesTable.initialize();
this.httpRequestExecutor.initialize();
this.responseViewer.initialize();
// Initialize the 21121 event handler for automatic responses
this.nostr21121EventHandler.initialize();
console.log('Nostr21121EventHandler initialized for automatic responses');
// Set up event listeners
this.setupEventListeners();
@ -304,6 +328,7 @@ export class ServerUI {
// Clean up UI components
this.eventList.dispose();
this.eventDetail.dispose();
this.httpMessagesTable.dispose();
// No dispose method needed for HttpRequestExecutor and ResponseViewer
// as they don't have persistent resources to clean up
@ -317,6 +342,9 @@ export class ServerUI {
}
// Close WebSocket connections
this.relayService.getWebSocketManager().close();
// Anything specific to the 21121 event handler cleanup could go here
// Currently there's no explicit dispose method needed
}
/**

@ -92,13 +92,13 @@ export class EventDetailsRenderer {
<span class="event-id-display">ID: ${event.id?.substring(0, 8) || 'Unknown'}...</span>
</div>
<div class="event-type-info">
<div class="event-type-info" style="padding-left: 15px;">
<span class="event-kind">Kind: ${event.kind}</span>
<span class="event-type">${isRequest ? 'HTTP Request' : (isResponse ? 'HTTP Response' : 'Unknown')}</span>
<span class="event-time">Time: ${eventTime}</span>
</div>
<div class="event-metadata">
<div class="event-metadata" style="padding-left: 15px;">
<div class="pubkey">Pubkey: ${event.pubkey}</div>
<div class="tags">
<h3>Tags</h3>
@ -106,46 +106,98 @@ export class EventDetailsRenderer {
</div>
</div>
<div id="related-events-container">
<div id="related-events-container" style="padding-left: 15px;">
<div class="loading-indicator">
<div class="spinner"></div>
<span>Loading related events...</span>
</div>
</div>
<div class="http-actions" id="http-actions-${eventId}">
<div class="http-actions" id="http-actions-${eventId}" style="padding-left: 15px;">
<!-- Action buttons will be added here -->
</div>
<div class="http-content-tabs">
<div class="tab-buttons">
<button class="tab-btn" data-tab="raw-http">Raw HTTP</button>
<button class="tab-btn active" data-tab="formatted-http">Formatted HTTP</button>
<!-- Enhanced tabbed interface for event details -->
<div class="event-details-tabs" style="margin-top: 20px;">
<div class="tab-buttons" style="border-bottom: 2px solid var(--border-color); margin-bottom: 15px; padding-bottom: 2px;">
${isRequest ? `
<button class="tab-btn" data-tab="request-json" style="padding: 8px 15px; border: none; background: none; border-bottom: 2px solid transparent; cursor: pointer; font-weight: 500; margin-right: 10px; margin-bottom: -2px; transition: all 0.3s ease;">21120 Raw JSON</button>
<button class="tab-btn active" data-tab="request-http" style="padding: 8px 15px; border: none; background: none; border-bottom: 2px solid var(--accent-color); cursor: pointer; font-weight: 500; margin-right: 10px; margin-bottom: -2px; transition: all 0.3s ease; color: var(--accent-color);">21120 HTTP Request</button>
` : ''}
${isResponse ? `
<button class="tab-btn" data-tab="response-json" style="padding: 8px 15px; border: none; background: none; border-bottom: 2px solid transparent; cursor: pointer; font-weight: 500; margin-right: 10px; margin-bottom: -2px; transition: all 0.3s ease;">21121 Raw JSON</button>
<button class="tab-btn active" data-tab="response-http" style="padding: 8px 15px; border: none; background: none; border-bottom: 2px solid var(--accent-color); cursor: pointer; font-weight: 500; margin-right: 10px; margin-bottom: -2px; transition: all 0.3s ease; color: var(--accent-color);">21121 HTTP Response</button>
` : ''}
${!isRequest && !isResponse ? `
<button class="tab-btn active" data-tab="event-json" style="padding: 8px 15px; border: none; background: none; border-bottom: 2px solid var(--accent-color); cursor: pointer; font-weight: 500; margin-right: 10px; margin-bottom: -2px; transition: all 0.3s ease; color: var(--accent-color);">Event Raw JSON</button>
` : ''}
</div>
<div class="tab-content" id="raw-http">
<div class="loading-container" id="raw-loading-${eventId}">
<!-- Request JSON tab content -->
${isRequest ? `
<div class="tab-content" id="request-json">
<h3 style="padding-left: 20px; margin-top: 20px; margin-bottom: 15px; color: var(--accent-color);">21120 Raw JSON</h3>
<pre class="json-content" style="margin: 20px; padding: 15px; background-color: var(--bg-tertiary); border-left: 4px solid var(--accent-color); border-radius: 4px;">
${JSON.stringify(event, null, 2)}
</pre>
</div>
` : ''}
<!-- Request HTTP tab content -->
${isRequest ? `
<div class="tab-content active" id="request-http">
<h3 style="padding-left: 20px; margin-top: 20px; margin-bottom: 15px; color: var(--accent-color);">21120 HTTP Request</h3>
<div class="loading-container" id="raw-loading-${eventId}" style="padding: 20px; text-align: center;">
<div class="spinner"></div>
<span>Loading content...</span>
</div>
<pre class="http-content" id="raw-content-${eventId}" style="display: none;"></pre>
<div class="decryption-status" id="decryption-status-raw-${eventId}">
<pre class="http-content" id="raw-content-${eventId}" style="display: none; margin: 20px; padding: 15px; border-left: 4px solid var(--accent-color); border-radius: 4px; background-color: var(--bg-tertiary);"></pre>
<div class="http-formatted-container" id="formatted-content-${eventId}" style="display: none; margin: 20px; padding: 15px; border-radius: 4px; background-color: var(--bg-tertiary);">
</div>
<div class="decryption-status" id="decryption-status-raw-${eventId}" style="margin: 20px; padding: 10px; text-align: center; border-radius: 4px;">
<div class="spinner"></div>
<span>Processing encryption...</span>
</div>
</div>
<div class="tab-content active" id="formatted-http">
<div class="loading-container" id="formatted-loading-${eventId}">
` : ''}
<!-- Response JSON tab content -->
${isResponse ? `
<div class="tab-content" id="response-json">
<h3 style="padding-left: 20px; margin-top: 20px; margin-bottom: 15px; color: var(--accent-color);">21121 Raw JSON</h3>
<pre class="json-content" style="margin: 20px; padding: 15px; background-color: var(--bg-tertiary); border-left: 4px solid var(--accent-color); border-radius: 4px;">
${JSON.stringify(event, null, 2)}
</pre>
</div>
` : ''}
<!-- Response HTTP tab content -->
${isResponse ? `
<div class="tab-content active" id="response-http">
<h3 style="padding-left: 20px; margin-top: 20px; margin-bottom: 15px; color: var(--accent-color);">21121 HTTP Response</h3>
<div class="loading-container" id="raw-loading-${eventId}" style="padding: 20px; text-align: center;">
<div class="spinner"></div>
<span>Formatting content...</span>
<span>Loading content...</span>
</div>
<div class="http-formatted-container" id="formatted-content-${eventId}" style="display: none;">
<pre class="http-content" id="raw-content-${eventId}" style="display: none; margin: 20px; padding: 15px; border-left: 4px solid var(--accent-color); border-radius: 4px; background-color: var(--bg-tertiary);"></pre>
<div class="http-formatted-container" id="formatted-content-${eventId}" style="display: none; margin: 20px; padding: 15px; border-radius: 4px; background-color: var(--bg-tertiary);">
</div>
<div class="decryption-status" id="decryption-status-formatted-${eventId}">
<div class="decryption-status" id="decryption-status-raw-${eventId}" style="margin: 20px; padding: 10px; text-align: center; border-radius: 4px;">
<div class="spinner"></div>
<span>Processing encryption...</span>
</div>
</div>
` : ''}
<!-- Generic event JSON tab content -->
${!isRequest && !isResponse ? `
<div class="tab-content active" id="event-json">
<h3 style="padding-left: 20px; margin-top: 20px; margin-bottom: 15px; color: var(--accent-color);">Event Raw JSON</h3>
<pre class="json-content" style="margin: 20px; padding: 15px; background-color: var(--bg-tertiary); border-left: 4px solid var(--accent-color); border-radius: 4px;">
${JSON.stringify(event, null, 2)}
</pre>
</div>
` : ''}
</div>
`;
@ -160,25 +212,63 @@ export class EventDetailsRenderer {
if (!this.eventDetails) return;
const tabButtons = this.eventDetails.querySelectorAll('.tab-btn');
if (tabButtons.length === 0) return;
tabButtons.forEach(button => {
button.addEventListener('click', (e) => {
// Remove active class from all buttons and content
tabButtons.forEach(btn => btn.classList.remove('active'));
// Remove active class from all buttons in the same tab group
const tabGroup = button.closest('.tab-buttons');
if (tabGroup) {
const groupButtons = tabGroup.querySelectorAll('.tab-btn');
groupButtons.forEach(btn => btn.classList.remove('active'));
} else {
// Fallback to the old behavior
tabButtons.forEach(btn => btn.classList.remove('active'));
}
const tabContents = this.eventDetails!.querySelectorAll('.tab-content');
tabContents.forEach(content => content.classList.remove('active'));
// Find the parent tab container
let tabContainer = button.closest('.event-details-tabs');
if (!tabContainer) {
tabContainer = button.closest('.http-content-tabs');
}
// Add active class to clicked button
button.classList.add('active');
// Show corresponding content
const tabId = (button as HTMLElement).dataset.tab || '';
const tabContent = this.eventDetails!.querySelector(`#${tabId}`);
if (tabContent) {
tabContent.classList.add('active');
if (tabContainer) {
// Remove active class from all tab contents in this container
const tabContents = tabContainer.querySelectorAll('.tab-content');
tabContents.forEach(content => content.classList.remove('active'));
// Add active class to clicked button
button.classList.add('active');
// Show corresponding content
const tabId = (button as HTMLElement).dataset.tab || '';
const tabContent = tabContainer.querySelector(`#${tabId}`);
if (tabContent) {
tabContent.classList.add('active');
}
}
});
});
// Ensure at least one tab is active in each tab group
const tabGroups = this.eventDetails.querySelectorAll('.tab-buttons');
tabGroups.forEach(group => {
const hasActiveButton = group.querySelector('.tab-btn.active');
if (!hasActiveButton) {
const firstButton = group.querySelector('.tab-btn');
if (firstButton) {
firstButton.classList.add('active');
const tabId = (firstButton as HTMLElement).dataset.tab || '';
const tabContainer = group.closest('.event-details-tabs') || group.closest('.http-content-tabs');
if (tabContainer) {
const tabContent = tabContainer.querySelector(`#${tabId}`);
if (tabContent) {
tabContent.classList.add('active');
}
}
}
}
});
}
/**
@ -221,6 +311,22 @@ export class EventDetailsRenderer {
// 5. Update formatted content (most expensive operation)
this.updateFormattedContent(eventId, httpContent, isRequest, isResponse || is21121Event, receivedEvent.decrypted);
// 6. For 21121 response events, display the raw JSON
if (is21121Event) {
this.displayResponse21121Json(event);
} else if (isRequest) {
// For request events, check if there are related responses
const relatedIds = event.id ? (this.relatedEvents.get(event.id) || []) : [];
if (relatedIds.length > 0) {
// Get the first related response event
const responseId = relatedIds[0];
const responseEvent = this.receivedEvents.get(responseId)?.event;
if (responseEvent && responseEvent.kind === 21121) {
this.displayResponse21121Json(responseEvent);
}
}
}
} catch (error) {
console.error("Error loading event details:", error);
this.showErrorState(eventId, String(error));
@ -316,26 +422,37 @@ export class EventDetailsRenderer {
content: string,
decrypted: boolean
): void {
// Get elements
const loadingElement = document.getElementById(`raw-loading-${eventId}`);
const contentElement = document.getElementById(`raw-content-${eventId}`);
const statusElement = document.getElementById(`decryption-status-raw-${eventId}`);
// Get elements - look for multiple possible IDs due to new tabbed structure
const loadingElements = document.querySelectorAll(`[id^="raw-loading-${eventId}"]`);
const contentElements = document.querySelectorAll(`[id^="raw-content-${eventId}"]`);
const statusElements = document.querySelectorAll(`[id^="decryption-status-raw-${eventId}"]`);
if (!loadingElement || !contentElement || !statusElement) return;
// Update content
contentElement.textContent = content;
// Update encryption status
if (decrypted) {
statusElement.innerHTML = '<div class="decryption-status success">Decryption successful ✓</div>';
} else {
statusElement.innerHTML = '<div class="decryption-status error">Decryption failed or not attempted</div>';
if (loadingElements.length === 0 || contentElements.length === 0 || statusElements.length === 0) {
console.warn('Could not find all required elements for updating raw content');
return;
}
// Hide loading, show content
loadingElement.style.display = 'none';
contentElement.style.display = 'block';
// Update all instances of the content
contentElements.forEach(element => {
element.textContent = content;
(element as HTMLElement).style.display = 'block';
});
// Update all encryption status elements
statusElements.forEach(element => {
if (decrypted) {
element.innerHTML = '<div class="decryption-status success">Decryption successful ✓</div>';
} else {
element.innerHTML = '<div class="decryption-status error">Decryption failed or not attempted</div>';
}
});
// Hide all loading elements
loadingElements.forEach(element => {
(element as HTMLElement).style.display = 'none';
});
console.log(`[EventDetailsRenderer] Updated raw content for event ${eventId}`);
}
/**
@ -348,26 +465,56 @@ export class EventDetailsRenderer {
isResponse: boolean,
decrypted: boolean
): void {
// Get elements
const loadingElement = document.getElementById(`formatted-loading-${eventId}`);
const contentElement = document.getElementById(`formatted-content-${eventId}`);
const statusElement = document.getElementById(`decryption-status-formatted-${eventId}`);
// Get elements - look for multiple possible IDs due to new tabbed structure
const loadingElements = document.querySelectorAll(`[id^="formatted-loading-${eventId}"]`);
const contentElements = document.querySelectorAll(`[id^="formatted-content-${eventId}"]`);
const statusElements = document.querySelectorAll(`[id^="decryption-status-formatted-${eventId}"]`);
if (!loadingElement || !contentElement || !statusElement) return;
// Format and update content
contentElement.innerHTML = HttpFormatter.formatHttpContent(content, isRequest, isResponse);
// Update encryption status
if (decrypted) {
statusElement.innerHTML = '<div class="decryption-status success">Decryption successful ✓</div>';
} else {
statusElement.innerHTML = '<div class="decryption-status error">Decryption failed or not attempted</div>';
// If we don't find the standard elements, look for the ones in the new structure
if (contentElements.length === 0) {
console.log('[EventDetailsRenderer] Using fallback to update formatted content');
// Try to find the formatted container in the HTTP tab
let tabContainer;
if (isRequest) {
tabContainer = document.getElementById('request-http');
} else if (isResponse) {
tabContainer = document.getElementById('response-http');
}
if (tabContainer) {
const formattedContainer = tabContainer.querySelector('.http-formatted-container');
if (formattedContainer) {
formattedContainer.innerHTML = HttpFormatter.formatHttpContent(content, isRequest, isResponse);
(formattedContainer as HTMLElement).style.display = 'block';
console.log('[EventDetailsRenderer] Updated formatted content using fallback');
}
}
return;
}
// Hide loading, show content
loadingElement.style.display = 'none';
contentElement.style.display = 'block';
// Format and update all instances of the content
contentElements.forEach(element => {
element.innerHTML = HttpFormatter.formatHttpContent(content, isRequest, isResponse);
(element as HTMLElement).style.display = 'block';
});
// Update all encryption status elements
statusElements.forEach(element => {
if (decrypted) {
element.innerHTML = '<div class="decryption-status success">Decryption successful ✓</div>';
} else {
element.innerHTML = '<div class="decryption-status error">Decryption failed or not attempted</div>';
}
});
// Hide all loading elements
loadingElements.forEach(element => {
(element as HTMLElement).style.display = 'none';
});
console.log(`[EventDetailsRenderer] Updated formatted content for event ${eventId}`);
}
/**
@ -387,6 +534,30 @@ export class EventDetailsRenderer {
}
}
/**
* Display 21121 response event's raw JSON
* @param event The 21121 response event
*/
private displayResponse21121Json(event: NostrEvent): void {
// Get the JSON container
const jsonContainer = document.getElementById('response21121Json');
if (!jsonContainer) return;
// Get the pre element within the container
const preElement = jsonContainer.querySelector('pre.json-content');
if (!preElement) return;
// Format the JSON prettily
const formattedJson = JSON.stringify(event, null, 2);
preElement.textContent = formattedJson;
// Show the container
jsonContainer.style.display = 'block';
// Add a log for debugging
console.log('[EventDetailsRenderer] Displayed 21121 response JSON', event.id);
}
/**
* Get the event details element
* @returns The event details element or null

@ -1,24 +1,173 @@
/**
* Nostr21121EventHandler.ts
* Handles NIP-21121 HTTP response events
* Automatically processes incoming 21120 requests to generate 21121 responses
*/
import { NostrEvent } from '../relay';
import { NostrService } from './NostrService';
import { EventManager, EventChangeType, EventKind } from './EventManager';
import { HttpClient } from './HttpClient';
import { ToastNotifier } from './ToastNotifier';
import { Nostr21121Service } from './Nostr21121Service';
/**
* Class for handling NIP-21121 HTTP response events
*/
export class Nostr21121EventHandler {
private nostrService: NostrService;
private eventManager: EventManager;
private httpClient: HttpClient;
private nostr21121Service: Nostr21121Service;
private responseEventMap: Map<string, string[]> = new Map();
/**
* Constructor
* @param nostrService The NostrService instance
* @param eventManager The EventManager instance
* @param httpClient The HttpClient instance
*/
constructor(nostrService: NostrService) {
constructor(
nostrService: NostrService,
eventManager: EventManager,
httpClient: HttpClient
) {
this.nostrService = nostrService;
this.eventManager = eventManager;
this.httpClient = httpClient;
// Initialize the 21121 service
// We pass the nostrService as relayService and null as cacheService
this.nostr21121Service = new Nostr21121Service(this.nostrService, null);
}
/**
* Initialize the handler
*/
public initialize(): void {
console.log('[Nostr21121EventHandler] Initialized - Auto-response mode enabled');
// Set up event listeners
this.setupEventListeners();
}
/**
* Set up event listeners
*/
private setupEventListeners(): void {
// Listen for new 21120 events
this.eventManager.registerListener((eventId, changeType) => {
if (changeType === EventChangeType.Added) {
this.handleNewEvent(eventId);
}
});
}
/**
* Handle a new event
* @param eventId The ID of the new event
*/
private async handleNewEvent(eventId: string): Promise<void> {
const managedEvent = this.eventManager.getEvent(eventId);
if (!managedEvent) return;
const event = managedEvent.event;
// Check if this is a 21120 event
if (event.kind === EventKind.HttpRequest) {
console.log(`[Nostr21121EventHandler] Processing new 21120 event: ${eventId}`);
// Check if we already have a 21121 response for this request
const existingResponses = this.eventManager.getResponsesForRequest(eventId);
if (existingResponses.length > 0) {
console.log(`[Nostr21121EventHandler] Found existing 21121 response for ${eventId}. Skipping.`);
return;
}
// Process the 21120 event to generate a 21121 response
this.processRequestEvent(event);
}
}
/**
* Process a 21120 request event to generate a 21121 response
* @param event The 21120 request event
*/
private async processRequestEvent(event: NostrEvent): Promise<void> {
try {
if (!event.id) {
console.error(`[Nostr21121EventHandler] Event has no ID, cannot process`);
return;
}
console.log(`[Nostr21121EventHandler] Processing 21120 request event ${event.id}`);
// Check if the event is decrypted by checking for decryptedContent
const managedEvent = this.eventManager.getEvent(event.id);
if (!managedEvent || !managedEvent.decrypted || !managedEvent.decryptedContent) {
console.log(`[Nostr21121EventHandler] Event ${event.id} is not decrypted yet. Cannot process.`);
return;
}
const httpRequest = managedEvent.decryptedContent;
console.log(`[Nostr21121EventHandler] Executing HTTP request for event ${event.id}`);
console.log(`[Nostr21121EventHandler] Request: ${httpRequest.split('\n')[0]}`);
// Execute the HTTP request
const httpResponse = await this.httpClient.sendHttpRequest(httpRequest);
console.log(`[Nostr21121EventHandler] Got HTTP response: ${httpResponse.split('\n')[0]}`);
// Get relay information for publishing
const relayService = this.nostrService.getRelayService();
const relayUrl = relayService.getActiveRelayUrl();
if (!relayUrl) {
console.error('[Nostr21121EventHandler] No active relay URL available');
ToastNotifier.show('Failed to create 21121 response: No active relay', 'error', 5000);
return;
}
// Get server private key (in a real implementation)
// Here we just use a placeholder as the actual key handling is beyond scope
const serverPrivateKey = 'placeholder_private_key';
// Create a 21121 response event
console.log(`[Nostr21121EventHandler] Creating 21121 response for request ${event.id}`);
const response = await this.nostr21121Service.createAndPublish21121Event(
event,
httpResponse,
serverPrivateKey,
relayUrl
);
if (!response || !response.id) {
console.error('[Nostr21121EventHandler] Failed to create response event');
ToastNotifier.show('Failed to create 21121 response event', 'error', 5000);
return;
}
console.log(`[Nostr21121EventHandler] Created 21121 response: ${response.id}`);
// Add the relationship to our map
this.addRelatedEvent(event.id, response.id);
// Add the event to EventManager
this.eventManager.addEvent(response);
// Display the raw JSON in the response JSON container
this.displayRawResponseJson(response);
// Display success notification
ToastNotifier.show('Created 21121 response event', 'success', 3000);
// Debug log the response event
console.log(`[Nostr21121EventHandler] Response event:`, JSON.stringify(response, null, 2));
} catch (error: any) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`[Nostr21121EventHandler] Error processing request:`, error);
ToastNotifier.show(`Failed to create 21121 response: ${errorMessage}`, 'error', 5000);
}
}
/**
@ -33,6 +182,16 @@ export class Nostr21121EventHandler {
return null;
}
// First check our local map
const related = this.getRelatedEvents(requestEventId);
if (related.length > 0) {
const responseId = related[0]; // Get the first response
const managedEvent = this.eventManager.getEvent(responseId);
if (managedEvent) {
return managedEvent.event;
}
}
// Get active relay
const relayService = this.nostrService.getRelayService();
const relayUrl = relayService.getActiveRelayUrl();
@ -42,16 +201,8 @@ export class Nostr21121EventHandler {
return null;
}
// Simplified implementation - instead of actual relay queries
console.log(`Searching for response event for request ${requestEventId} on relay ${relayUrl}`);
// Simulate searching for events - in a real implementation this would query relays
return new Promise((resolve) => {
setTimeout(() => {
console.log('No response event found - this is a simplified implementation');
resolve(null);
}, 1000);
});
// Try to find a response event using the 21121 service
return await this.nostr21121Service.findResponseForRequest(requestEventId, relayUrl);
} catch (error) {
console.error('Error finding 21121 response:', error);
return null;
@ -80,6 +231,37 @@ export class Nostr21121EventHandler {
return this.responseEventMap.get(eventId) || [];
}
/**
* Display raw response JSON in the UI
* @param response The 21121 response event to display
*/
private displayRawResponseJson(response: NostrEvent): void {
// Get the JSON container
const jsonContainer = document.getElementById('response21121Json');
if (!jsonContainer) {
console.warn('[Nostr21121EventHandler] response21121Json container not found');
return;
}
// Get the pre element within the container
const preElement = jsonContainer.querySelector('pre.json-content');
if (!preElement) {
console.warn('[Nostr21121EventHandler] json-content element not found');
return;
}
// Format the JSON prettily
const formattedJson = JSON.stringify(response, null, 2);
preElement.textContent = formattedJson;
// We don't automatically show the container anymore as it appears in the wrong place
// The response should be shown in the modal when a row is clicked instead
// jsonContainer.style.display = 'block';
// Add a log for debugging
console.log('[Nostr21121EventHandler] Updated 21121 response JSON content', response.id);
}
/**
* Check if an event has related 21121 responses
* @param eventId The event ID to check

@ -201,6 +201,10 @@ export class NostrEventService {
contentLength: decryptedContent?.length,
eventId: receivedEvent.id?.substring(0, 8) + '...'
});
// After successful decryption, check if we already have a 21121 response
// If not, create one by executing the HTTP request
this.checkAndCreateResponse(receivedEvent, decryptedContent);
} catch (decryptError) {
console.error("Failed to decrypt event content:", decryptError);
console.error("Decryption error details:", {
@ -335,6 +339,225 @@ export class NostrEventService {
* @param statusMessage The status message
* @param statusClass The CSS class for styling the status
*/
/**
* Check if we already have a 21121 response for a 21120 request
* If not, automatically create one by executing the HTTP request
* @param requestEvent The 21120 request event
* @param decryptedContent The decrypted HTTP request content
*/
private async checkAndCreateResponse(requestEvent: NostrEvent, decryptedContent?: string): Promise<void> {
if (!requestEvent.id || !decryptedContent) {
console.log("Cannot process request: missing ID or decrypted content");
return;
}
try {
console.log(`Checking for existing 21121 response for request ${requestEvent.id.substring(0, 8)}...`);
// Check if we already have a response for this request in the EventManager
const hasResponse = this.eventManager.hasRelatedEvents(requestEvent.id);
if (hasResponse) {
console.log(`Found existing 21121 response for request ${requestEvent.id.substring(0, 8)}`);
return;
}
console.log(`No existing 21121 response found for request ${requestEvent.id.substring(0, 8)}, executing HTTP request...`);
// Get server's private key for signing the response
const serverNsec = localStorage.getItem('serverNsec');
if (!serverNsec) {
console.error("Cannot create 21121 response: Server private key (nsec) not found");
return;
}
// Get the relay URL
const relayUrl = this.relayService.getActiveRelayUrl();
if (!relayUrl) {
console.error("Cannot create 21121 response: No active relay connection");
return;
}
// Create HttpClient dynamically
const { HttpService } = await import('./HttpService');
const { HttpClient } = await import('./HttpClient');
const httpService = new HttpService();
const httpClient = new HttpClient(httpService);
try {
// Execute the HTTP request
console.log("Executing HTTP request...");
const httpResponse = await httpClient.sendHttpRequest(decryptedContent);
console.log("HTTP request executed successfully, creating 21121 response...");
// Import crypto utilities for encryption
const cryptoUtils = await import('../utils/crypto-utils');
const nostrTools = await import('nostr-tools');
// Generate a random key for content encryption
const randomKey = Array.from(crypto.getRandomValues(new Uint8Array(32)))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
// Encrypt the response content with the random key
const encryptedContent = await cryptoUtils.encryptWithWebCrypto(httpResponse, randomKey);
// Get the server pubkey from the nsec
const decoded = nostrTools.nip19.decode(serverNsec);
if (decoded.type !== 'nsec') {
throw new Error("Invalid server nsec format");
}
const serverPrivateKeyBytes = decoded.data as Uint8Array;
const serverPubkey = nostrTools.getPublicKey(serverPrivateKeyBytes);
// Create tags for the 21121 event
const tags: string[][] = [
["e", requestEvent.id as string],
["key", randomKey], // Store the key directly for now, we'll handle encryption differently
["expiration", (Math.floor(Date.now() / 1000) + 3600).toString()] // 1 hour expiration
];
// Create the 21121 event
const eventBody = {
kind: EventKind.HttpResponse,
created_at: Math.floor(Date.now() / 1000),
tags: tags,
content: encryptedContent,
pubkey: serverPubkey
};
// Compute the event ID (hash)
const id = nostrTools.getEventHash(eventBody as any);
// Sign the event
let sig: string;
// Try to use window.nostr if available, otherwise use a simulated signature
if (window.nostr && window.nostr.signEvent) {
try {
const signResult = await window.nostr.signEvent(eventBody);
sig = typeof signResult === 'object' && signResult.sig
? signResult.sig
: 'simulated_signature_for_21121_response';
} catch (signError) {
console.error("Error signing with window.nostr:", signError);
sig = 'simulated_signature_for_21121_response';
}
} else {
sig = 'simulated_signature_for_21121_response';
}
// Create the complete signed event
const responseEvent: NostrEvent = {
...eventBody,
id,
sig
};
// Ensure ID is defined for logging
const shortId = responseEvent.id ? responseEvent.id.substring(0, 8) + '...' : 'undefined';
console.log("Created 21121 response event:", {
id: shortId,
tags: responseEvent.tags.length,
contentLength: responseEvent.content.length
});
// Display the raw JSON in the console for debugging
console.log("21121 raw JSON:", JSON.stringify(responseEvent, null, 2));
// Create a DOM element to display the raw JSON in the UI
this.displayRawJsonInUI(responseEvent);
// Publish the event to the relay if we have valid IDs
if (responseEvent.id && requestEvent.id) {
const relayPool = this.relayService.getRelayPool();
if (relayPool) {
try {
// Use relayPool.send instead of publish for better type compatibility
const pub = relayPool.publish([relayUrl], responseEvent as any);
await pub;
console.log(`Published 21121 response event to ${relayUrl}`);
// Add the event to our EventManager for tracking
this.processEvent(responseEvent);
// Store relationship between request and response
this.eventManager.associateResponseWithRequest(responseEvent.id, requestEvent.id);
} catch (pubError) {
console.error("Error publishing 21121 response:", pubError);
}
} else {
console.error("Cannot publish 21121 response: Relay pool not available");
}
}
// Show success notification
const event = new CustomEvent('21121-response-created', {
detail: {
requestId: requestEvent.id,
responseId: responseEvent.id,
responseJson: JSON.stringify(responseEvent, null, 2)
}
});
document.dispatchEvent(event);
} catch (error) {
console.error("Error creating 21121 response:", error);
}
} catch (error) {
console.error("Error checking for existing 21121 response:", error);
}
}
/**
* Display the raw JSON of a 21121 event in the UI
* @param event The 21121 event to display
*/
private displayRawJsonInUI(event: NostrEvent): void {
try {
// Create or get a container for displaying the JSON
let jsonContainer = document.getElementById('response21121Json');
if (!jsonContainer) {
// Create the container if it doesn't exist
jsonContainer = document.createElement('div');
jsonContainer.id = 'response21121Json';
jsonContainer.className = 'response-json-container';
// Add a header
const header = document.createElement('h3');
header.textContent = '21121 Response JSON';
jsonContainer.appendChild(header);
// Create a pre element for the JSON
const pre = document.createElement('pre');
pre.className = 'json-content';
jsonContainer.appendChild(pre);
// Add to the page in a suitable location (find a good container)
const container = document.querySelector('.event-details') ||
document.querySelector('.content') ||
document.body;
if (container) {
container.appendChild(jsonContainer);
}
}
// Update the JSON content
const pre = jsonContainer.querySelector('pre');
if (pre) {
pre.textContent = JSON.stringify(event, null, 2);
}
// Make the container visible
jsonContainer.style.display = 'block';
} catch (uiError) {
console.error("Error displaying 21121 JSON in UI:", uiError);
}
}
private updateStatus(statusMessage: string, statusClass: string): void {
if (this.statusCallback) {
this.statusCallback(statusMessage, statusClass);

@ -403,13 +403,27 @@ body[data-theme="dark"] {
/* General layout */
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 900px;
margin: 0 auto;
padding: 20px;
margin: 0;
padding: 0;
background-color: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
transition: all 0.3s ease;
width: 100%;
min-height: 100vh;
max-width: 100vw;
overflow-x: hidden;
border: none;
}
/* Content container for wrapping everything except the navbar */
.content {
padding: 0 20px;
width: 100%;
max-width: 100%;
box-sizing: border-box;
border: none;
background-color: var(--bg-primary);
}
/* Make sure all elements using CSS vars also transition smoothly */
@ -448,11 +462,13 @@ h3 {
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
height: 50px;
display: flex;
width: 100%;
width: 100vw;
max-width: 100%;
}
body {
padding-top: 70px; /* Added padding to account for fixed navbar */
overflow-x: hidden; /* Prevent horizontal scrolling */
}
.nav-left {
@ -634,7 +650,7 @@ code {
/* Content */
.content {
margin-bottom: 40px;
margin-bottom: 0;
}
/* Footer */
@ -649,7 +665,7 @@ footer {
/* Responsive adjustments */
@media (max-width: 768px) {
body {
padding-top: 60px;
padding-top: 45px;
padding-left: 10px;
padding-right: 10px;
}
@ -1095,40 +1111,54 @@ footer {
display: none !important;
}
/* Receiver page styles */
/* Receiver page styles - Enhanced */
.relay-connection {
margin-bottom: 20px;
padding: 15px;
margin-bottom: 25px;
padding: 20px;
background-color: var(--bg-secondary);
display: flex;
flex-direction: column;
gap: 15px;
gap: 18px;
border-radius: 10px;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
border: 1px solid var(--border-color);
}
/* Server info styles */
/* Server info styles - Enhanced */
.server-info-container {
margin-bottom: 10px;
padding-bottom: 15px;
margin-bottom: 15px;
padding-bottom: 18px;
border-bottom: 1px solid var(--border-color);
transition: all 0.3s ease;
}
.server-npub-container {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
gap: 12px;
padding: 5px;
background-color: var(--bg-tertiary);
border-radius: 6px;
}
/* Server section styles */
/* Server section styles - Enhanced */
.server-section {
margin-bottom: 20px;
padding: 15px;
margin-bottom: 25px;
padding: 20px;
background-color: var(--bg-secondary);
border-radius: 8px;
border-radius: 10px;
border: 1px solid var(--border-color);
display: flex;
flex-direction: column;
gap: 15px;
gap: 18px;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.server-section:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.12);
}
.server-npub-container label {
@ -1192,64 +1222,88 @@ footer {
.relay-input-container {
display: flex;
align-items: center;
margin-bottom: 10px;
margin-bottom: 15px;
background-color: var(--bg-tertiary);
border-radius: 8px;
padding: 12px 15px;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
}
.relay-input-container label {
margin-right: 10px;
min-width: 80px;
margin-right: 15px;
min-width: 85px;
font-weight: 600;
color: var(--text-secondary);
}
.relay-input-container input {
flex: 1;
margin-right: 10px;
padding: 8px;
margin-right: 15px;
padding: 10px 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
border-radius: 6px;
background-color: var(--bg-secondary);
font-size: 0.95em;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.relay-input-container input:focus {
border-color: var(--accent-color);
box-shadow: 0 0 0 2px rgba(var(--accent-color-rgb, 13, 110, 253), 0.25);
outline: none;
}
.relay-connect-button {
background-color: var(--button-primary);
color: white;
border: none;
border-radius: 4px;
padding: 8px 15px;
border-radius: 6px;
padding: 10px 18px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.relay-connect-button:hover {
background-color: var(--button-hover);
transform: translateY(-2px);
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.15);
}
.relay-status {
margin-top: 10px;
padding: 5px 10px;
border-radius: 4px;
margin-top: 12px;
padding: 8px 15px;
border-radius: 6px;
display: inline-block;
font-weight: 500;
text-align: center;
transition: all 0.3s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.relay-status.connected {
background-color: var(--bg-info);
background-color: rgba(40, 167, 69, 0.15);
color: var(--button-success);
border: 1px solid var(--button-success);
}
.relay-status.connecting {
background-color: var(--bg-info);
color: var(--text-secondary);
border: 1px solid var(--border-color);
background-color: rgba(255, 193, 7, 0.15);
color: #ffc107;
border: 1px solid #ffc107;
}
.relay-status.error {
background-color: var(--bg-info);
background-color: rgba(231, 76, 60, 0.15);
color: #e74c3c;
border: 1px solid #e74c3c;
}
.relay-status.notice {
background-color: var(--bg-info);
color: var(--text-secondary);
border: 1px solid var(--border-color);
color: var(--accent-color);
border: 1px solid var(--accent-color);
}
.subscription-settings {
@ -1262,27 +1316,39 @@ footer {
.filter-options {
margin: 15px 0;
padding: 8px 10px;
padding: 12px 15px;
background-color: var(--bg-tertiary);
border-radius: 4px;
border-radius: 8px;
display: flex;
align-items: center;
border: 1px solid var(--border-color);
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
}
.filter-options label {
margin-right: 15px;
display: inline-block;
cursor: pointer;
font-weight: 500;
transition: color 0.2s ease;
}
.filter-options label:hover {
color: var(--accent-color);
}
.filter-checkbox {
display: flex;
align-items: center;
gap: 8px;
gap: 10px;
}
.filter-checkbox input[type="checkbox"] {
margin-right: 5px;
width: 18px;
height: 18px;
accent-color: var(--accent-color);
cursor: pointer;
}
.key-input {
@ -1565,29 +1631,114 @@ footer {
color: var(--text-tertiary);
cursor: not-allowed;
}
/* Events container for side-by-side layout */
.events-container {
/* Main App Layout with sidebar */
.app-layout {
display: flex;
gap: 20px;
margin-top: 15px;
gap: 0;
margin: 0;
width: 100%;
background-color: var(--bg-secondary);
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--border-color);
max-width: 100%;
}
.events-sidebar {
/* Sidebar for 21120 requests */
.requests-sidebar {
flex: 0 0 300px;
display: flex;
flex-direction: column;
border-right: 1px solid var(--border-color);
border-radius: 0;
background-color: var(--bg-secondary);
overflow: hidden;
}
.events-content {
.sidebar-header {
padding: 15px;
border-bottom: 1px solid var(--border-color);
background-color: var(--bg-tertiary);
display: flex;
justify-content: space-between;
align-items: center;
}
.sidebar-header h3 {
margin: 0;
font-size: 16px;
color: var(--text-primary);
}
.sidebar-controls {
display: flex;
gap: 10px;
}
.icon-button {
background: none;
border: none;
font-size: 16px;
cursor: pointer;
color: var(--text-secondary);
transition: color 0.2s;
}
.icon-button:hover {
color: var(--accent-color);
}
/* Main content area */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
padding: 0 20px;
gap: 20px;
overflow-y: auto;
padding-right: 10px;
}
.events-list {
max-height: 600px;
overflow-y: auto;
/* Related responses section */
.related-responses-section {
border: 1px solid var(--border-color);
border-radius: 8px;
background-color: var(--bg-secondary);
overflow: hidden;
margin-top: 20px;
}
.related-responses-section h3 {
margin: 0;
padding: 15px;
border-bottom: 1px solid var(--border-color);
background-color: var(--bg-tertiary);
font-size: 16px;
color: var(--text-primary);
}
.responses-list {
padding: 15px;
max-height: 300px;
overflow-y: auto;
}
/* Events list styling - updated for sidebar */
.events-list {
flex: 1;
overflow-y: auto;
background-color: var(--bg-secondary);
}
/* When events list is in the requests-only mode */
.events-list.requests-only .event-item:not([data-kind="21120"]) {
display: none;
}
.events-list.requests-only {
max-height: none; /* Allow it to fill the sidebar */
}
.event-item {
padding: 15px;
border-bottom: 1px solid var(--border-color);
@ -1903,26 +2054,51 @@ footer {
.json-content {
margin-top: 10px;
padding: 15px;
border-radius: 4px;
padding: 20px;
border-radius: 6px;
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;
font-family: 'Consolas', 'Courier New', monospace;
font-size: 14px;
line-height: 1.5;
white-space: pre-wrap;
border-left: 4px solid var(--accent-color);
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
color: var(--text-primary);
}
/* Syntax highlighting for JSON */
.json-string {
color: #25c2a0;
}
.json-number {
color: #f5a623;
}
.json-boolean {
color: #7e57c2;
}
.json-null {
color: #bc4749;
}
.json-key {
color: #0088cc;
font-weight: 500;
}
/* Event debug output for 31120 events */
.event-debug-output {
margin: 15px 0;
padding: 15px;
padding: 18px;
background-color: var(--bg-tertiary);
border-radius: 8px;
border-radius: 10px;
border: 1px solid var(--accent-color);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.event-debug-output h3 {
@ -2108,24 +2284,30 @@ footer {
}
/* Dark mode is already handled by CSS variables */
/* Responsive adjustments for the events container */
/* Responsive adjustments for the app layout */
@media (max-width: 768px) {
.events-container {
.app-layout {
flex-direction: column;
height: auto;
}
.events-sidebar {
.requests-sidebar {
flex: auto;
width: 100%;
height: 300px;
}
.events-list {
max-height: 300px;
.main-content {
width: 100%;
}
.event-details {
min-height: 400px;
}
.responses-list {
max-height: 200px;
}
}
/* Clear events button */

@ -367,4 +367,116 @@
.status-server-error {
background-color: rgba(156, 39, 176, 0.1);
color: #9c27b0;
}
/* Response Items in the related responses section */
.response-item {
padding: 12px;
border-bottom: 1px solid var(--border-color);
cursor: pointer;
transition: background-color 0.2s ease;
}
.response-item:last-child {
border-bottom: none;
}
.response-item:hover {
background-color: var(--bg-tertiary);
}
.response-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.response-time {
font-size: 12px;
color: var(--text-secondary);
}
.response-id {
font-size: 12px;
color: var(--accent-color);
font-family: monospace;
}
.status-code {
font-size: 12px;
padding: 2px 6px;
border-radius: 4px;
background-color: var(--bg-tertiary);
font-weight: bold;
}
.status-code.status-success {
background-color: rgba(40, 167, 69, 0.1);
color: #28a745;
}
.status-code.status-redirect {
background-color: rgba(255, 193, 7, 0.1);
color: #ffc107;
}
.status-code.status-client-error {
background-color: rgba(255, 152, 0, 0.1);
color: #ff9800;
}
.status-code.status-server-error {
background-color: rgba(231, 76, 60, 0.1);
color: #e74c3c;
}
.encryption-status {
font-size: 12px;
padding: 2px 6px;
border-radius: 4px;
}
.encryption-status.encrypted {
background-color: rgba(255, 152, 0, 0.1);
color: #ff9800;
}
.encryption-status.decrypted {
background-color: rgba(40, 167, 69, 0.1);
color: #28a745;
}
/* 21121 Response JSON Container */
.response-json-container {
margin: 15px 0;
padding: 15px;
border: 1px solid var(--border-color);
border-radius: 4px;
background-color: var(--bg-secondary);
max-width: 100%;
overflow: auto;
}
.response-json-container h3 {
margin-top: 0;
color: var(--text-primary);
font-size: 16px;
border-bottom: 1px solid var(--border-color);
padding-bottom: 8px;
margin-bottom: 12px;
}
.response-json-container pre.json-content {
background-color: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 3px;
padding: 10px;
max-height: 300px;
overflow: auto;
font-family: monospace;
font-size: 13px;
white-space: pre-wrap;
word-break: break-word;
color: var(--text-primary);
}

@ -0,0 +1,566 @@
/* Table styles for HTTP Messages section */
.http-messages-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
margin-bottom: 25px;
background-color: var(--bg-secondary);
border-radius: 10px;
overflow: hidden;
border: 1px solid var(--border-color);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.http-messages-table thead th {
background-color: var(--accent-color);
color: white;
font-weight: 600;
text-align: left;
padding: 14px 18px;
border-bottom: 2px solid rgba(255, 255, 255, 0.1);
position: relative;
}
/* Add subtle highlight to table header */
.http-messages-table thead:after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 1px;
background: linear-gradient(to right, transparent, rgba(255, 255, 255, 0.3), transparent);
}
.http-messages-table tbody tr {
border-bottom: 1px solid var(--border-color);
transition: background-color 0.2s ease;
cursor: pointer;
}
.http-messages-table tbody tr:hover {
background-color: var(--bg-tertiary);
}
.http-messages-table tbody tr.selected {
background-color: var(--bg-tertiary);
border-left: 4px solid var(--accent-color);
}
.http-messages-table tbody tr:last-child {
border-bottom: none;
}
.http-messages-table td {
padding: 14px 18px;
vertical-align: middle;
transition: all 0.25s ease;
}
.http-messages-table tbody tr:hover td {
color: var(--text-primary);
}
.http-messages-table .sender-cell {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: 'Consolas', 'Courier New', monospace;
color: var(--text-secondary);
background-color: rgba(0, 0, 0, 0.02);
transition: all 0.25s ease;
}
.http-messages-table tr:hover .sender-cell {
color: var(--accent-color);
}
.http-messages-table .time-cell {
white-space: nowrap;
color: var(--text-tertiary);
font-size: 0.9em;
}
.http-messages-table .event-id-cell {
font-family: 'Consolas', 'Courier New', monospace;
color: var(--accent-color);
font-size: 0.9em;
font-weight: 500;
padding: 6px 8px;
background-color: rgba(var(--accent-color-rgb, 13, 110, 253), 0.05);
border-radius: 4px;
display: inline-block;
}
.http-messages-table .response-cell {
text-align: center;
width: 80px;
}
.response-indicator {
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 14px;
width: 24px;
height: 24px;
line-height: 1;
text-align: center;
border-radius: 50%;
transition: all 0.3s ease;
}
.response-indicator.has-response {
color: white;
background-color: var(--accent-color);
font-weight: bold;
box-shadow: 0 2px 5px rgba(var(--accent-color-rgb, 13, 110, 253), 0.4);
}
.response-indicator.no-response {
color: var(--text-tertiary);
opacity: 0.7;
background-color: var(--bg-tertiary);
border: 1px solid var(--border-color);
}
.http-messages-table tr:hover .response-indicator.has-response {
transform: scale(1.1);
}
/* Expandable content styles */
.expandable-content {
display: none;
padding: 0;
background-color: var(--bg-tertiary);
border-top: 1px solid var(--border-color);
box-shadow: inset 0 4px 6px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.expandable-content.expanded {
display: table-row;
animation: expandFade 0.3s ease-in-out;
}
@keyframes expandFade {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.expandable-cell {
padding: 0 !important;
}
.expandable-inner {
padding: 20px;
}
/* Tab styles inside expandable content */
.details-tabs {
border-bottom: 1px solid var(--border-color);
display: flex;
padding: 10px 20px 0;
background-color: var(--bg-tertiary);
flex-wrap: wrap;
gap: 5px;
}
.details-tab {
padding: 10px 15px;
cursor: pointer;
border: 1px solid var(--border-color);
border-bottom: none;
margin-right: 5px;
transition: all 0.3s ease;
border-radius: 4px 4px 0 0;
background-color: var(--bg-secondary);
font-weight: 500;
position: relative;
bottom: -1px;
outline: none;
}
.details-tab:hover {
color: var(--accent-color);
background-color: var(--bg-tertiary);
}
.details-tab.active {
color: var(--accent-color);
border-bottom-color: var(--bg-tertiary);
background-color: var(--bg-tertiary);
z-index: 2;
}
.details-tab-content {
display: none;
padding: 20px;
border-top: 1px solid var(--border-color);
background-color: var(--bg-tertiary);
}
.details-tab-content.active {
display: block;
}
/* Styling for the HTTP Response and Request tabs */
.details-tab-content h3 {
margin-top: 0;
color: var(--accent-color);
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid var(--border-color);
}
.details-tab-content pre {
background-color: var(--bg-secondary);
padding: 18px;
border-radius: 6px;
max-height: 300px;
overflow-y: auto;
border: 1px solid var(--border-color);
font-family: 'Consolas', 'Courier New', monospace;
font-size: 14px;
line-height: 1.5;
color: var(--text-primary);
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
}
/* Loading indicator and no responses message */
.loading-response, .no-responses {
padding: 20px;
text-align: center;
color: var(--text-tertiary);
background-color: var(--bg-secondary);
border-radius: 8px;
border: 1px solid var(--border-color);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
font-style: italic;
}
/* Empty state */
.table-empty-state {
padding: 30px;
text-align: center;
color: var(--text-tertiary);
font-style: italic;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.http-messages-table {
display: block;
overflow-x: auto;
}
.http-messages-table td.event-id-cell {
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
/* Modal styles */
.http-messages-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
overflow: auto;
}
.modal-content {
position: relative;
background-color: var(--bg-secondary);
margin: 5% auto;
padding: 0;
width: 90%;
max-width: 1000px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
max-height: 85vh;
display: flex;
flex-direction: column;
}
.modal-close-button {
position: absolute;
top: 10px;
right: 15px;
font-size: 24px;
font-weight: bold;
color: var(--text-tertiary);
background: none;
border: none;
cursor: pointer;
z-index: 2;
}
.modal-close-button:hover {
color: var(--accent-color);
}
/* Modal tabs */
.modal-tabs {
display: flex;
padding: 15px 15px 0;
border-bottom: 1px solid var(--border-color);
background-color: var(--bg-tertiary);
border-radius: 8px 8px 0 0;
flex-wrap: wrap;
gap: 5px;
}
.modal-tab {
padding: 10px 15px;
cursor: pointer;
border: 1px solid var(--border-color);
border-bottom: none;
margin-right: 5px;
transition: all 0.3s ease;
border-radius: 4px 4px 0 0;
background-color: var(--bg-secondary);
color: var(--text-primary);
font-weight: 500;
position: relative;
bottom: -1px;
outline: none;
}
.modal-tab:hover {
color: var(--accent-color);
background-color: var(--bg-tertiary);
}
.modal-tab.active {
color: var(--accent-color);
border-bottom-color: var(--bg-tertiary);
background-color: var(--bg-tertiary);
z-index: 2;
}
/* Modal tab contents */
.modal-tab-contents {
padding: 20px;
background-color: var(--bg-tertiary);
overflow-y: auto;
flex: 1;
border-radius: 0 0 8px 8px;
}
.modal-tab-content {
display: none;
max-height: 60vh;
overflow-y: auto;
}
.modal-tab-content pre {
background-color: var(--bg-secondary);
padding: 15px;
border-radius: 4px;
overflow-x: auto;
border: 1px solid var(--border-color);
margin: 0;
}
.json-content {
font-family: 'Consolas', 'Courier New', monospace;
font-size: 14px;
white-space: pre-wrap;
line-height: 1.5;
color: var(--text-primary);
}
/* Add syntax highlighting for JSON content */
.json-content .string { color: #25c2a0; }
.json-content .number { color: #f5a623; }
.json-content .boolean { color: #7e57c2; }
.json-content .null { color: #bc4749; }
.json-content .key { color: #0088cc; font-weight: 500; }
/* Modal update notification */
.modal-update-notification {
position: absolute;
top: 15px;
left: 50%;
transform: translateX(-50%);
background-color: var(--accent-color);
color: white;
padding: 8px 16px;
border-radius: 4px;
font-weight: 500;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
z-index: 3;
animation: fadeInOut 3s ease-in-out;
}
@keyframes fadeInOut {
0% { opacity: 0; transform: translateX(-50%) translateY(-10px); }
15% { opacity: 1; transform: translateX(-50%) translateY(0); }
85% { opacity: 1; transform: translateX(-50%) translateY(0); }
100% { opacity: 0; transform: translateX(-50%) translateY(-10px); }
}
.no-response {
padding: 15px;
text-align: center;
color: var(--text-tertiary);
background-color: var(--bg-secondary);
border-radius: 4px;
border: 1px solid var(--border-color);
margin: 20px 0;
}
/* HTTP content formatting - Enhanced */
.http-first-line {
font-weight: bold;
margin-bottom: 12px;
padding: 12px 15px;
border-bottom: 1px solid var(--border-color);
background-color: rgba(var(--accent-color-rgb, 13, 110, 253), 0.05);
border-radius: 6px 6px 0 0;
}
.http-method {
color: #0088cc;
font-weight: bold;
margin-right: 12px;
background-color: rgba(0, 136, 204, 0.1);
padding: 2px 8px;
border-radius: 4px;
}
.http-path {
color: var(--text-primary);
margin-right: 12px;
font-family: 'Consolas', 'Courier New', monospace;
}
.http-version {
color: var(--text-tertiary);
background-color: var(--bg-tertiary);
padding: 2px 6px;
border-radius: 4px;
font-size: 0.9em;
}
.http-status {
padding: 3px 8px;
border-radius: 6px;
margin: 0 12px;
font-weight: 600;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.http-status.success {
background-color: rgba(40, 167, 69, 0.9);
color: white;
}
.http-status.error {
background-color: rgba(220, 53, 69, 0.9);
color: white;
}
.http-status.redirect {
background-color: rgba(253, 126, 20, 0.9);
color: white;
}
.http-status-text {
color: var(--text-primary);
font-weight: 500;
}
.http-headers {
margin-bottom: 20px;
padding: 15px;
border-bottom: 1px dashed var(--border-color);
background-color: var(--bg-secondary);
border-radius: 4px;
}
.http-header {
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px dotted rgba(var(--border-color-rgb, 222, 226, 230), 0.5);
display: flex;
flex-wrap: wrap;
}
.http-header:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
.http-header-name {
color: #0088cc;
font-weight: 600;
margin-right: 10px;
min-width: 120px;
font-family: 'Consolas', 'Courier New', monospace;
}
.http-header-value {
color: var(--text-primary);
flex: 1;
font-family: 'Consolas', 'Courier New', monospace;
word-break: break-all;
}
.http-body {
font-family: 'Consolas', 'Courier New', monospace;
white-space: pre-wrap;
word-break: break-all;
padding: 15px;
background-color: var(--bg-secondary);
border-radius: 4px;
border: 1px solid var(--border-color);
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
}
.http-body.json {
color: var(--accent-color);
}
/* Add a label before the body content */
.http-body-label {
display: block;
margin-bottom: 10px;
font-weight: 600;
color: var(--text-secondary);
border-bottom: 1px dotted var(--border-color);
padding-bottom: 8px;
}
/* Responsive modal */
@media (max-width: 768px) {
.modal-content {
width: 95%;
margin: 10% auto;
}
.modal-tabs {
flex-direction: column;
padding: 10px 10px 0;
}
.modal-tab {
margin-bottom: 5px;
width: 100%;
text-align: left;
}
}