5 Commits

Author SHA1 Message Date
Your Name
26c0a71292 Remove nostr_login_lite/ and Nostr_NIPs/ from tracking 2025-09-24 18:32:05 -04:00
Your Name
41fcbfd9ca Left right visuals on visualizer 2025-09-24 18:14:59 -04:00
Your Name
6d5079561a Everything working pretty well 2025-09-24 18:05:22 -04:00
Your Name
11f24766e5 . 2025-09-24 14:39:33 -04:00
Your Name
f10ee66972 Working on ui 2025-09-24 14:05:19 -04:00
4 changed files with 227 additions and 131 deletions

Submodule nostr_login_lite deleted from 3109a93163

View File

@@ -418,6 +418,8 @@ small {
border-radius: var(--border-radius); border-radius: var(--border-radius);
filter: grayscale(100%); filter: grayscale(100%);
transition: filter 0.3s ease; transition: filter 0.3s ease;
object-fit: cover;
object-position: center;
} }
#thrower-banner:hover { #thrower-banner:hover {
@@ -431,6 +433,8 @@ small {
border: var(--border-width) solid var(--primary-color); border: var(--border-width) solid var(--primary-color);
filter: grayscale(100%); filter: grayscale(100%);
transition: filter 0.3s ease; transition: filter 0.3s ease;
object-fit: cover;
object-position: center;
} }
#thrower-icon:hover { #thrower-icon:hover {
@@ -449,6 +453,8 @@ small {
border: var(--border-width) solid var(--primary-color); border: var(--border-width) solid var(--primary-color);
filter: grayscale(100%); filter: grayscale(100%);
transition: filter 0.3s ease; transition: filter 0.3s ease;
object-fit: cover;
object-position: center;
} }
#profile-picture:hover { #profile-picture:hover {

View File

@@ -542,7 +542,7 @@
<div class="thrower-details-section collapsed" id="details-${index}"> <div class="thrower-details-section collapsed" id="details-${index}">
${thrower.icon ? ` ${thrower.icon ? `
<div style="margin-bottom: 15px;"> <div style="margin-bottom: 15px;">
<img src="${thrower.icon}" style="width: 50px; height: 50px; border-radius: var(--border-radius); border: var(--border-width) solid var(--primary-color); filter: grayscale(100%); transition: filter 0.3s ease;" onmouseover="this.style.filter='grayscale(0%) saturate(50%)'" onmouseout="this.style.filter='grayscale(100%)'"> <img src="${thrower.icon}" style="width: 50px; height: 50px; border-radius: var(--border-radius); border: var(--border-width) solid var(--primary-color); filter: grayscale(100%); transition: filter 0.3s ease; object-fit: cover; object-position: center;" onmouseover="this.style.filter='grayscale(0%) saturate(50%)'" onmouseout="this.style.filter='grayscale(100%)'">
</div> </div>
` : ''} ` : ''}
@@ -762,16 +762,17 @@
select.removeChild(select.lastChild); select.removeChild(select.lastChild);
} }
// Add discovered throwers // Add discovered throwers with real-time online status check
discoveredThrowers.forEach(thrower => { discoveredThrowers.forEach(thrower => {
const option = document.createElement('option'); const option = document.createElement('option');
option.value = thrower.pubkey; option.value = thrower.pubkey;
// Always check current online status when populating
const onlineStatus = isThrowerOnline(thrower) ? '🟢' : '🔴'; const onlineStatus = isThrowerOnline(thrower) ? '🟢' : '🔴';
option.textContent = `${onlineStatus} ${thrower.name} (${thrower.pubkey.substring(0, 8)}...)`; option.textContent = `${onlineStatus} ${thrower.name} (${thrower.pubkey.substring(0, 8)}...)`;
select.appendChild(option); select.appendChild(option);
}); });
console.log('INFO', `Populated thrower dropdown for bounce ${bounceId} with ${discoveredThrowers.length} throwers`); console.log('INFO', `Populated thrower dropdown for bounce ${bounceId} with ${discoveredThrowers.length} throwers (with current online status)`);
} }
// Update all existing thrower dropdowns with current online status // Update all existing thrower dropdowns with current online status
@@ -829,6 +830,35 @@
input.value = ''; input.value = '';
clearRelayDropdown(bounceId); clearRelayDropdown(bounceId);
} }
// Check if bounce inputs are valid and update button state
updateCreateBounceButtonState(bounceId);
}
// Check if bounce is ready to be created and update button state
function updateCreateBounceButtonState(bounceId) {
const button = document.getElementById(`create-bounce-btn-${bounceId}`);
if (!button) return;
const throwerPubkey = getThrowerPubkeyForBounce(bounceId);
const relayInput = document.getElementById(`bounce-relays-${bounceId}`);
const hasRelays = relayInput && relayInput.value.trim().length > 0;
const isValid = throwerPubkey && hasRelays;
if (isValid) {
// Enable button
button.disabled = false;
button.style.color = '';
button.style.borderColor = '';
button.style.cursor = '';
} else {
// Disable button
button.disabled = true;
button.style.color = 'var(--muted-color)';
button.style.borderColor = 'var(--muted-color)';
button.style.cursor = 'not-allowed';
}
} }
// Handle manual thrower pubkey input // Handle manual thrower pubkey input
@@ -865,6 +895,9 @@
} else { } else {
clearRelayDropdown(bounceId); clearRelayDropdown(bounceId);
} }
// Update button state after thrower input change
updateCreateBounceButtonState(bounceId);
} }
// Get thrower pubkey for a bounce (from dropdown or manual input) // Get thrower pubkey for a bounce (from dropdown or manual input)
@@ -904,7 +937,7 @@
return null; return null;
} }
// Populate relay dropdown with relays the selected thrower can read from // Populate relay dropdown with relays the selected thrower can throw to (write to)
function populateRelayDropdown(bounceId, throwerPubkey) { function populateRelayDropdown(bounceId, throwerPubkey) {
const relaySelect = document.getElementById(`relay-select-${bounceId}`); const relaySelect = document.getElementById(`relay-select-${bounceId}`);
const relayInput = document.getElementById(`bounce-relays-${bounceId}`); const relayInput = document.getElementById(`bounce-relays-${bounceId}`);
@@ -927,17 +960,15 @@
return; return;
} }
// Get relays the thrower can read from (read or both) // Get relays the thrower can write to (write or both) - these are the relays it can throw to
const readableRelays = thrower.relayList.relays.filter(r => r.type === 'read' || r.type === 'both');
// Get relays the thrower can write to (write or both)
const writableRelays = thrower.relayList.relays.filter(r => r.type === 'write' || r.type === 'both'); const writableRelays = thrower.relayList.relays.filter(r => r.type === 'write' || r.type === 'both');
if (readableRelays.length === 0) { if (writableRelays.length === 0) {
relaySelect.innerHTML = '<option value="">-- This thrower cannot read from any relays --</option><option value="__manual__">⊕ Add manually (enter relay URL)</option>'; relaySelect.innerHTML = '<option value="">-- This thrower cannot throw to any relays --</option><option value="__manual__">⊕ Add manually (enter relay URL)</option>';
// Show manual option since thrower has no readable relays // Show manual option since thrower has no writable relays
const manualOption = relaySelect.querySelector('option[value="__manual__"]'); const manualOption = relaySelect.querySelector('option[value="__manual__"]');
manualOption.classList.remove('hidden'); manualOption.classList.remove('hidden');
console.log('WARN', `Thrower ${thrower.name} cannot read from any relays`); console.log('WARN', `Thrower ${thrower.name} cannot throw to any relays`);
return; return;
} }
@@ -949,8 +980,8 @@
relaySelect.appendChild(allRelaysOption); relaySelect.appendChild(allRelaysOption);
} }
// Add readable relays to dropdown // Add writable relays to dropdown (these are the relays the thrower can throw to)
readableRelays.forEach(relay => { writableRelays.forEach(relay => {
const option = document.createElement('option'); const option = document.createElement('option');
option.value = relay.url; option.value = relay.url;
option.textContent = relay.url; option.textContent = relay.url;
@@ -967,7 +998,7 @@
relayInput.value = ''; relayInput.value = '';
relayManualDiv.classList.add('hidden'); relayManualDiv.classList.add('hidden');
console.log('INFO', `Populated relay dropdown for bounce ${bounceId} with ${readableRelays.length} readable relays and ${writableRelays.length} writable relays from ${thrower.name}`); console.log('INFO', `Populated relay dropdown for bounce ${bounceId} with ${writableRelays.length} writable relays that ${thrower.name} can throw to`);
} }
// Clear relay dropdown // Clear relay dropdown
@@ -1023,6 +1054,9 @@
manualDiv.classList.add('hidden'); manualDiv.classList.add('hidden');
input.value = ''; input.value = '';
} }
// Update button state after relay selection change
updateCreateBounceButtonState(bounceId);
} }
// Handle manual relay input // Handle manual relay input
@@ -1037,6 +1071,9 @@
} }
console.log('INFO', `Manual relay input for bounce ${bounceId}: ${input.value}`); console.log('INFO', `Manual relay input for bounce ${bounceId}: ${input.value}`);
} }
// Update button state after manual relay input change
updateCreateBounceButtonState(bounceId);
} }
// Get all writable relays for a thrower as comma-separated string // Get all writable relays for a thrower as comma-separated string
@@ -1129,7 +1166,7 @@
bounceSection.id = `bounce-${bounceId}`; bounceSection.id = `bounce-${bounceId}`;
bounceSection.innerHTML = ` bounceSection.innerHTML = `
<h2 id="bounce-${bounceId}-header">Bounce ${bounceId} (Kind 22222 Routing Event)</h2> <h2 id="bounce-${bounceId}-header">Add a bounce (Kind 22222 Routing Event)</h2>
<div class="input-group"> <div class="input-group">
<label for="thrower-select-${bounceId}">Thrower:</label> <label for="thrower-select-${bounceId}">Thrower:</label>
<select id="thrower-select-${bounceId}" onchange="onThrowerSelect(${bounceId})" style="width: 100%; margin-bottom: 10px;"> <select id="thrower-select-${bounceId}" onchange="onThrowerSelect(${bounceId})" style="width: 100%; margin-bottom: 10px;">
@@ -1142,7 +1179,7 @@
</div> </div>
</div> </div>
<div class="input-group"> <div class="input-group">
<label for="relay-select-${bounceId}">Target Relay:</label> <label for="relay-select-${bounceId}">Thrower throws to this relay(s):</label>
<select id="relay-select-${bounceId}" onchange="onRelaySelect(${bounceId})" style="width: 100%; margin-bottom: 10px;"> <select id="relay-select-${bounceId}" onchange="onRelaySelect(${bounceId})" style="width: 100%; margin-bottom: 10px;">
<option value="">-- Select a thrower first --</option> <option value="">-- Select a thrower first --</option>
<option value="__manual__" class="hidden">⊕ Add manually (enter relay URL)</option> <option value="__manual__" class="hidden">⊕ Add manually (enter relay URL)</option>
@@ -1173,7 +1210,7 @@
<label for="audit-tag-${bounceId}">Audit Tag (auto-generated):</label> <label for="audit-tag-${bounceId}">Audit Tag (auto-generated):</label>
<input type="text" id="audit-tag-${bounceId}" readonly style="background: #f5f5f5;"> <input type="text" id="audit-tag-${bounceId}" readonly style="background: #f5f5f5;">
</div> </div>
<button onclick="createBounce(${bounceId})" id="create-bounce-btn-${bounceId}">Create Bounce ${bounceId}</button> <button onclick="createBounce(${bounceId})" id="create-bounce-btn-${bounceId}" disabled style="color: var(--muted-color); border-color: var(--muted-color); cursor: not-allowed;">Create Bounce</button>
<div id="bounce-${bounceId}-display" class="json-display"></div> <div id="bounce-${bounceId}-display" class="json-display"></div>
<div style="text-align: right; margin-top: 5px;"> <div style="text-align: right; margin-top: 5px;">
@@ -1204,36 +1241,10 @@
// Populate thrower dropdown with discovered throwers // Populate thrower dropdown with discovered throwers
populateThrowerDropdown(bounceId); populateThrowerDropdown(bounceId);
// Update bounce labels to reflect execution order // Labels are now generic, no need to update numbering
updateBounceLabels();
} }
// Update bounce labels to reflect execution order (newest bounce is Bounce 1, oldest is Bounce N) // Bounce labels are now generic - no numbering needed
function updateBounceLabels() {
// Get all existing bounce sections
const bounceContainer = document.getElementById('bounces-container');
const bounceSections = bounceContainer.querySelectorAll('.bounce-section');
// Update labels in reverse order (newest first gets Bounce 1)
bounceSections.forEach((section, index) => {
const bounceId = section.id.replace('bounce-', '');
const executionOrder = bounceSections.length - index; // Reverse the index
// Update the header
const header = document.getElementById(`bounce-${bounceId}-header`);
if (header) {
header.textContent = `Bounce ${executionOrder} (Kind 22222 Routing Event)`;
}
// Update the create button text
const createBtn = document.getElementById(`create-bounce-btn-${bounceId}`);
if (createBtn) {
createBtn.textContent = `Create Bounce ${executionOrder}`;
}
});
console.log('INFO: Updated bounce labels for execution order');
}
// Update all timeline absolute times continuously // Update all timeline absolute times continuously
function updateAllTimelineTimes() { function updateAllTimelineTimes() {
@@ -1432,8 +1443,7 @@
// Show the Add Bounce button again now that this bounce is completed // Show the Add Bounce button again now that this bounce is completed
document.getElementById('add-bounce-btn').classList.remove('hidden'); document.getElementById('add-bounce-btn').classList.remove('hidden');
// Update bounce labels after creation to reflect execution order // Labels are generic, no update needed
updateBounceLabels();
} catch (error) { } catch (error) {
console.log('ERROR', `Failed to create bounce ${bounceId}: ${error.message}`); console.log('ERROR', `Failed to create bounce ${bounceId}: ${error.message}`);
@@ -1479,7 +1489,7 @@
// Generate the "Throw the Superball" button HTML // Generate the "Throw the Superball" button HTML
function generateThrowButtonHtml() { function generateThrowButtonHtml() {
return ` return `
<div class="timeline-step throw-button" style="background: white; border: 4px solid #4a90e2; cursor: pointer;" onclick="throwSuperball()"> <div class="timeline-step throw-button align-left" style="background: white; border: 4px solid #4a90e2; cursor: pointer; width: 60%; margin-left: 0; margin-right: auto;" onclick="throwSuperball()">
<div class="step-time"> <div class="step-time">
<div class="step-time-relative">Ready</div> <div class="step-time-relative">Ready</div>
<div class="step-time-absolute" style="font-size: 10px; color: #888;">Click to start</div> <div class="step-time-absolute" style="font-size: 10px; color: #888;">Click to start</div>
@@ -1639,27 +1649,23 @@
// Start with user sending the outermost bounce // Start with user sending the outermost bounce
let currentTime = baseTime; let currentTime = baseTime;
// Work forward through bounces (in order they were created) // Work forward through bounces (in EXECUTION order - reverse of creation order)
bounces.forEach((bounce, index) => { const reversedBounces = [...bounces].reverse();
const bounceNumber = index + 1; reversedBounces.forEach((bounce, index) => {
const isFirst = (index === 0); const bounceNumber = bounces.length - index; // Original bounce number
const isLast = (index === bounces.length - 1); const isFirst = (index === 0); // First in execution (last created)
const isLast = (index === reversedBounces.length - 1); // Last in execution (first created)
const throwerName = getThrowerName(bounce, bounceNumber); const throwerName = getThrowerName(bounce, bounceNumber);
if (isFirst) { if (isFirst) {
// User sends the outermost routing event (first bounce created) // For the first bounce, we skip the user publishing step since it's handled by the button
// Start directly with relay propagation after the button click
const routingEventSize = JSON.stringify(bounce.routingEvent).length; const routingEventSize = JSON.stringify(bounce.routingEvent).length;
const relays = getRelaysForBounce(bounceNumber);
// Get the relays from the current bounce's routing instructions (where user publishes to)
const relays = bounce.payload?.routing?.relays || [];
// Step 1: User sends to relay // Step 1: Relay propagates (immediate after button click)
flow.push({
time: currentTime,
actor: userName,
action: `Publishes routing event`,
size: routingEventSize
});
// Step 2: Relay propagates (immediate)
currentTime += 2000; // 2 seconds for relay propagation currentTime += 2000; // 2 seconds for relay propagation
flow.push({ flow.push({
time: currentTime, time: currentTime,
@@ -1679,7 +1685,10 @@
if (isLast) { if (isLast) {
// Last bounce - posts final event // Last bounce - posts final event
const finalEventSize = JSON.stringify(finalEvent).length + paddingAdjustment; const finalEventSize = JSON.stringify(finalEvent).length + paddingAdjustment;
const finalRelays = getRelaysForBounce(bounceNumber);
// Get the relays from the current bounce's routing instructions (where final event gets posted)
const finalRelays = bounce.payload?.routing?.relays || [];
const delaySeconds = getDelayForBounce(bounceNumber); const delaySeconds = getDelayForBounce(bounceNumber);
const paddingAdded = getPaddingAdjustmentForBounce(bounceNumber); const paddingAdded = getPaddingAdjustmentForBounce(bounceNumber);
@@ -1707,38 +1716,43 @@
size: Math.max(finalEventSize, 0) size: Math.max(finalEventSize, 0)
}); });
} else { } else {
// Intermediate bounce - forwards to next superball // Intermediate bounce - forwards to next superball
const nextBounce = bounces[index + 1]; const nextBounce = reversedBounces[index + 1];
const nextThrowerName = getThrowerName(nextBounce, bounceNumber + 1); const nextBounceNumber = bounces.length - (index + 1);
const nextRoutingSize = JSON.stringify(nextBounce.routingEvent).length + paddingAdjustment; const nextThrowerName = getThrowerName(nextBounce, nextBounceNumber);
const nextRelays = getRelaysForBounce(bounceNumber + 1); // Next superball's relays const nextRoutingSize = JSON.stringify(nextBounce.routingEvent).length + paddingAdjustment;
const delaySeconds = getDelayForBounce(bounceNumber);
const paddingAdded = getPaddingAdjustmentForBounce(bounceNumber); // Get the relays from the current bounce's routing instructions (where this thrower forwards to)
const currentBounce = reversedBounces[index];
const forwardingRelays = currentBounce.payload?.routing?.relays || [];
const delaySeconds = getDelayForBounce(bounceNumber);
const paddingAdded = getPaddingAdjustmentForBounce(bounceNumber);
// Create detailed forwarding action description // Create detailed forwarding action description
let actionDescription = `Grabs message, waits ${delaySeconds} seconds`; let actionDescription = `Grabs message, waits ${delaySeconds} seconds`;
if (paddingAdded > 0) { if (paddingAdded > 0) {
actionDescription += `, adds ${paddingAdded} bytes of padding`; actionDescription += `, adds ${paddingAdded} bytes of padding`;
} }
actionDescription += `, and forwards to: ${nextRelays.join(', ')}`; actionDescription += `, and forwards to: ${forwardingRelays.join(', ')}`;
// Step 3: Superball forwards to next relay // Step 3: Superball forwards to next relay
flow.push({ flow.push({
time: currentTime, time: currentTime,
actor: throwerName, actor: throwerName,
action: actionDescription, action: actionDescription,
size: Math.max(nextRoutingSize, 0) size: Math.max(nextRoutingSize, 0)
}); });
// Step 4: Relay propagates to next superball // Step 4: Relay propagates to next superball
currentTime += 2000; // 2 seconds for relay propagation currentTime += 2000; // 2 seconds for relay propagation
flow.push({ flow.push({
time: currentTime, time: currentTime,
actor: `Relay (${nextRelays.join(', ')})`, actor: `Relay (${forwardingRelays.join(', ')})`,
action: `Event available for ${nextThrowerName}`, action: `Event available for ${nextThrowerName}`,
size: Math.max(nextRoutingSize, 0) size: Math.max(nextRoutingSize, 0)
}); });
} }
}); });
return flow; return flow;
@@ -1746,17 +1760,26 @@
// Generate HTML for timeline visualization // Generate HTML for timeline visualization
function generateTimelineHtml(eventFlow) { function generateTimelineHtml(eventFlow) {
return eventFlow.map((step, index) => ` return eventFlow.map((step, index) => {
<div class="timeline-step" id="timeline-step-${index}"> // Determine alignment based on actor type
<div class="step-time"> const isRelay = step.actor.startsWith('Relay (');
<div class="step-time-relative">${formatTime(step.time)}</div> const alignmentClass = isRelay ? 'align-right' : 'align-left';
<div class="step-time-absolute" style="font-size: 10px; color: #888;">${formatAbsoluteTime(step.time)}</div> const alignmentStyle = isRelay
? 'width: 60%; margin-left: auto; margin-right: 0;'
: 'width: 60%; margin-left: 0; margin-right: auto;';
return `
<div class="timeline-step ${alignmentClass}" id="timeline-step-${index}" style="${alignmentStyle}">
<div class="step-time">
<div class="step-time-relative">${formatTime(step.time)}</div>
<div class="step-time-absolute" style="font-size: 10px; color: #888;">${formatAbsoluteTime(step.time)}</div>
</div>
<div class="step-actor">${step.actor}</div>
<div class="step-action">${step.action}</div>
<div class="step-size">${step.size} bytes</div>
</div> </div>
<div class="step-actor">${step.actor}</div> `;
<div class="step-action">${step.action}</div> }).join('');
<div class="step-size">${step.size} bytes</div>
</div>
`).join('');
} }
// Format absolute time (HH:MM:SS) // Format absolute time (HH:MM:SS)
@@ -1771,7 +1794,8 @@
// Helper functions for bounce data extraction // Helper functions for bounce data extraction
function getRelaysForBounce(bounceNumber) { function getRelaysForBounce(bounceNumber) {
const bounceIndex = bounceNumber - 1; // Convert execution order bounce number to array index (reverse lookup)
const bounceIndex = bounces.length - bounceNumber;
if (bounceIndex < 0 || bounceIndex >= bounces.length) return []; if (bounceIndex < 0 || bounceIndex >= bounces.length) return [];
// Get relays from the bounce's routing instructions // Get relays from the bounce's routing instructions
@@ -1783,7 +1807,8 @@
} }
function getDelayForBounce(bounceNumber) { function getDelayForBounce(bounceNumber) {
const bounceIndex = bounceNumber - 1; // Convert execution order bounce number to array index (reverse lookup)
const bounceIndex = bounces.length - bounceNumber;
if (bounceIndex < 0 || bounceIndex >= bounces.length) return 30; if (bounceIndex < 0 || bounceIndex >= bounces.length) return 30;
const bounce = bounces[bounceIndex]; const bounce = bounces[bounceIndex];
@@ -1794,7 +1819,8 @@
} }
function getPaddingAdjustmentForBounce(bounceNumber) { function getPaddingAdjustmentForBounce(bounceNumber) {
const bounceIndex = bounceNumber - 1; // Convert execution order bounce number to array index (reverse lookup)
const bounceIndex = bounces.length - bounceNumber;
if (bounceIndex < 0 || bounceIndex >= bounces.length) return 0; if (bounceIndex < 0 || bounceIndex >= bounces.length) return 0;
const bounce = bounces[bounceIndex]; const bounce = bounces[bounceIndex];
@@ -1858,6 +1884,11 @@
existingViz.remove(); existingViz.remove();
} }
// Refresh thrower status after reset so new dropdowns have current availability
setTimeout(() => {
discoverThrowers();
}, 500);
console.log('INFO: Builder reset successfully'); console.log('INFO: Builder reset successfully');
} }

View File

@@ -48,7 +48,7 @@
<div><strong>Events in Queue:</strong> <span id="events-queued">0</span></div> <div><strong>Events in Queue:</strong> <span id="events-queued">0</span></div>
<div><strong>Info Status:</strong> <span id="thrower-info-status">Loading...</span></div> <div><strong>Info Status:</strong> <span id="thrower-info-status">Loading...</span></div>
<div><strong>Last Updated:</strong> <span id="thrower-info-updated">Never</span></div> <div><strong>Last Updated:</strong> <span id="thrower-info-updated">Never</span></div>
<div><strong>Refresh Rate:</strong> <span id="thrower-info-refresh">60 seconds</span></div> <div><strong>Refresh Rate:</strong> <span id="thrower-info-refresh">300 seconds</span></div>
</div> </div>
</div> </div>
@@ -71,7 +71,7 @@
<div class="input-group"> <div class="input-group">
<label>Add New Relay:</label> <label>Add New Relay:</label>
<div class="add-relay-form"> <div class="add-relay-form">
<input type="url" id="new-relay-url" placeholder="wss://relay.example.com"> <input type="url" id="new-relay-url" placeholder="wss://relay.example.com (or comma-separated list)">
<select id="new-relay-type"> <select id="new-relay-type">
<option value="">Both</option> <option value="">Both</option>
<option value="read">Read</option> <option value="read">Read</option>
@@ -151,7 +151,7 @@
</div> </div>
<div class="input-group"> <div class="input-group">
<label for="edit-refresh-rate">Refresh Rate (seconds):</label> <label for="edit-refresh-rate">Refresh Rate (seconds):</label>
<input type="number" id="edit-refresh-rate" placeholder="60" value="60" min="10" max="3600"> <input type="number" id="edit-refresh-rate" placeholder="300" value="300" min="10" max="3600">
</div> </div>
<div class="input-group"> <div class="input-group">
<label for="edit-thrower-content">Additional Content (optional):</label> <label for="edit-thrower-content">Additional Content (optional):</label>
@@ -616,35 +616,95 @@
} }
// Add new relay // Add new relay (supports comma-separated list)
function addRelay() { function addRelay() {
const url = document.getElementById('new-relay-url').value.trim(); const input = document.getElementById('new-relay-url').value.trim();
const type = document.getElementById('new-relay-type').value; const type = document.getElementById('new-relay-type').value;
if (!url) { if (!input) {
showStatus('relay-status', 'Please enter a relay URL', 'error'); showStatus('relay-status', 'Please enter a relay URL', 'error');
return; return;
} }
if (!url.startsWith('wss://') && !url.startsWith('ws://')) { // Check if input contains commas (multiple URLs)
showStatus('relay-status', 'Relay URL must start with wss:// or ws://', 'error'); const urls = input.includes(',') ?
return; input.split(',').map(url => url.trim()).filter(url => url.length > 0) :
} [input];
// Check for duplicates const results = {
if (currentRelays.some(r => r.url === url)) { added: [],
showStatus('relay-status', 'Relay already exists', 'error'); failed: [],
return; duplicates: []
} };
currentRelays.push({ url, type }); // Process each URL
urls.forEach(url => {
// Validate URL format
if (!url.startsWith('wss://') && !url.startsWith('ws://')) {
results.failed.push({ url, reason: 'Must start with wss:// or ws://' });
return;
}
// Check for duplicates
if (currentRelays.some(r => r.url === url)) {
results.duplicates.push(url);
return;
}
// Add relay
currentRelays.push({ url, type, authStatus: 'unknown', lastTested: null });
results.added.push(url);
});
// Update display
displayRelayList(); displayRelayList();
// Clear form // Clear form
document.getElementById('new-relay-url').value = ''; document.getElementById('new-relay-url').value = '';
document.getElementById('new-relay-type').value = ''; document.getElementById('new-relay-type').value = '';
showStatus('relay-status', 'Relay added (remember to save)', 'info'); // Provide detailed feedback
const messages = [];
if (results.added.length > 0) {
const typeText = type ? ` (${type === 'read' ? 'Read only' : type === 'write' ? 'Write only' : 'Both'})` : ' (Both)';
if (results.added.length === 1) {
messages.push(`✅ Added: ${results.added[0]}${typeText}`);
} else {
messages.push(`✅ Added ${results.added.length} relays${typeText}:`);
results.added.forEach(url => messages.push(`${url}`));
}
}
if (results.duplicates.length > 0) {
if (results.duplicates.length === 1) {
messages.push(`⚠️ Duplicate skipped: ${results.duplicates[0]}`);
} else {
messages.push(`⚠️ ${results.duplicates.length} duplicates skipped:`);
results.duplicates.forEach(url => messages.push(`${url}`));
}
}
if (results.failed.length > 0) {
if (results.failed.length === 1) {
messages.push(`❌ Failed: ${results.failed[0].url} (${results.failed[0].reason})`);
} else {
messages.push(`${results.failed.length} failed:`);
results.failed.forEach(item => messages.push(`${item.url} (${item.reason})`));
}
}
// Show comprehensive status
const statusMessage = messages.join('\n');
const statusType = results.added.length > 0 ? 'info' :
results.failed.length > 0 ? 'error' : 'info';
// Add reminder to save if any were added
const finalMessage = results.added.length > 0 ?
statusMessage + '\n\n💾 Remember to save your relay configuration!' :
statusMessage;
showStatus('relay-status', finalMessage, statusType);
} }
// Remove relay // Remove relay
@@ -1067,7 +1127,7 @@
version: '1.0.0', version: '1.0.0',
privacyPolicy: '', privacyPolicy: '',
termsOfService: '', termsOfService: '',
refreshRate: 60, refreshRate: 300,
content: event.content || '' content: event.content || ''
}; };
@@ -1084,7 +1144,7 @@
else if (tag[0] === 'version') currentThrowerInfo.version = tag[1] || '1.0.0'; else if (tag[0] === 'version') currentThrowerInfo.version = tag[1] || '1.0.0';
else if (tag[0] === 'privacy_policy') currentThrowerInfo.privacyPolicy = tag[1] || ''; else if (tag[0] === 'privacy_policy') currentThrowerInfo.privacyPolicy = tag[1] || '';
else if (tag[0] === 'terms_of_service') currentThrowerInfo.termsOfService = tag[1] || ''; else if (tag[0] === 'terms_of_service') currentThrowerInfo.termsOfService = tag[1] || '';
else if (tag[0] === 'refresh_rate') currentThrowerInfo.refreshRate = parseInt(tag[1]) || 60; else if (tag[0] === 'refresh_rate') currentThrowerInfo.refreshRate = parseInt(tag[1]) || 300;
}); });
lastThrowerInfoPublish = event.created_at; lastThrowerInfoPublish = event.created_at;
@@ -1103,7 +1163,7 @@
version: '1.0.0', version: '1.0.0',
privacyPolicy: '', privacyPolicy: '',
termsOfService: '', termsOfService: '',
refreshRate: 60, refreshRate: 300,
content: '' content: ''
}; };
displayThrowerInfo(currentThrowerInfo); displayThrowerInfo(currentThrowerInfo);
@@ -1148,7 +1208,7 @@
document.getElementById('edit-version').value = currentThrowerInfo.version || '1.0.0'; document.getElementById('edit-version').value = currentThrowerInfo.version || '1.0.0';
document.getElementById('edit-privacy-policy').value = currentThrowerInfo.privacyPolicy || ''; document.getElementById('edit-privacy-policy').value = currentThrowerInfo.privacyPolicy || '';
document.getElementById('edit-terms-service').value = currentThrowerInfo.termsOfService || ''; document.getElementById('edit-terms-service').value = currentThrowerInfo.termsOfService || '';
document.getElementById('edit-refresh-rate').value = currentThrowerInfo.refreshRate || 60; document.getElementById('edit-refresh-rate').value = currentThrowerInfo.refreshRate || 300;
document.getElementById('edit-thrower-content').value = currentThrowerInfo.content || ''; document.getElementById('edit-thrower-content').value = currentThrowerInfo.content || '';
} }
@@ -1172,7 +1232,7 @@
const version = document.getElementById('edit-version').value.trim(); const version = document.getElementById('edit-version').value.trim();
const privacyPolicy = document.getElementById('edit-privacy-policy').value.trim(); const privacyPolicy = document.getElementById('edit-privacy-policy').value.trim();
const termsOfService = document.getElementById('edit-terms-service').value.trim(); const termsOfService = document.getElementById('edit-terms-service').value.trim();
const refreshRate = parseInt(document.getElementById('edit-refresh-rate').value) || 60; const refreshRate = parseInt(document.getElementById('edit-refresh-rate').value) || 300;
const content = document.getElementById('edit-thrower-content').value.trim(); const content = document.getElementById('edit-thrower-content').value.trim();
try { try {
@@ -2053,8 +2113,8 @@
item.status.charAt(0).toUpperCase() + item.status.slice(1); item.status.charAt(0).toUpperCase() + item.status.slice(1);
div.innerHTML = ` div.innerHTML = `
<div style="font-size: 16px; font-weight: bold; margin-bottom: 8px; color: var(--accent-color);"><strong>Status:</strong> ${statusText}</div>
<div><strong>Event:</strong> ${item.id.substring(0, 32)}...</div> <div><strong>Event:</strong> ${item.id.substring(0, 32)}...</div>
<div><strong>Status:</strong> ${statusText}</div>
<div><strong>Target Relays:</strong> ${item.routing.relays.length}</div> <div><strong>Target Relays:</strong> ${item.routing.relays.length}</div>
<div><strong>Delay:</strong> ${item.routing.delay}s</div> <div><strong>Delay:</strong> ${item.routing.delay}s</div>
${item.routing.padding ? `<div><strong>Padding:</strong> ${item.routing.padding}</div>` : ''} ${item.routing.padding ? `<div><strong>Padding:</strong> ${item.routing.padding}</div>` : ''}