parent
865710390a
commit
a00417685c
@ -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">×</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">×</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);
|
||||
}
|
||||
}
|
||||
|
593
client/src/components/HttpMessagesTable.ts
Normal file
593
client/src/components/HttpMessagesTable.ts
Normal file
@ -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 = '×';
|
||||
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);
|
||||
}
|
566
client/styles/http-messages-table.css
Normal file
566
client/styles/http-messages-table.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user