- Separated JavaScript into script.js for better organization - Added Server-Sent Events (SSE) webhook receiver for real-time results - Created Go service to receive AlpenQueue webhooks and broadcast via SSE - Removed manual webhook input - results stream automatically - Added live connection status indicator - Implemented real-time result cards with animations - Fixed AlpenQueue API field names (webhook_url, selector) - Added dark theme styling for result display - Results appear instantly without polling The portfolio now shows AlpenQueue results in real-time as they arrive!
209 lines
6.5 KiB
JavaScript
209 lines
6.5 KiB
JavaScript
const API_URL = 'https://alpenqueue.maxtheweb.com';
|
|
const SSE_URL = 'https://maxtheweb.com/events';
|
|
|
|
let eventSource = null;
|
|
let currentJobId = null;
|
|
|
|
// Initialize SSE connection
|
|
function initSSE() {
|
|
if (eventSource) {
|
|
eventSource.close();
|
|
}
|
|
|
|
eventSource = new EventSource(SSE_URL);
|
|
|
|
eventSource.onopen = function() {
|
|
console.log('SSE connection established');
|
|
updateConnectionStatus('connected');
|
|
};
|
|
|
|
eventSource.onmessage = function(event) {
|
|
try {
|
|
const data = JSON.parse(event.data);
|
|
|
|
if (data.type === 'connected') {
|
|
console.log('SSE handshake complete');
|
|
return;
|
|
}
|
|
|
|
if (data.type === 'result') {
|
|
displayResult(data);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error parsing SSE message:', error);
|
|
}
|
|
};
|
|
|
|
eventSource.onerror = function(error) {
|
|
console.error('SSE error:', error);
|
|
updateConnectionStatus('disconnected');
|
|
|
|
// Reconnect after 5 seconds
|
|
setTimeout(() => {
|
|
if (eventSource.readyState === EventSource.CLOSED) {
|
|
initSSE();
|
|
}
|
|
}, 5000);
|
|
};
|
|
}
|
|
|
|
function updateConnectionStatus(status) {
|
|
const indicator = document.getElementById('connection-status');
|
|
if (!indicator) return;
|
|
|
|
indicator.className = 'connection-status ' + status;
|
|
indicator.textContent = status === 'connected' ? '● Live' : '○ Reconnecting...';
|
|
}
|
|
|
|
function displayResult(data) {
|
|
const resultsContainer = document.getElementById('live-results');
|
|
if (!resultsContainer) return;
|
|
|
|
// Remove the "no results" message if it exists
|
|
const noResults = resultsContainer.querySelector('.no-results');
|
|
if (noResults) {
|
|
noResults.remove();
|
|
}
|
|
|
|
const resultDiv = document.createElement('div');
|
|
resultDiv.className = 'result-item ' + (data.status || 'ok');
|
|
|
|
const timestamp = new Date(data.timestamp).toLocaleTimeString();
|
|
|
|
resultDiv.innerHTML = `
|
|
<div class="result-header">
|
|
<span class="result-status ${data.status || 'ok'}">${(data.status || 'OK').toUpperCase()}</span>
|
|
<span class="result-time">${timestamp} · ${data.took || '0s'}</span>
|
|
</div>
|
|
<div class="result-url">${data.url}</div>
|
|
<div class="result-content">
|
|
${data.content ? `<pre>${escapeHtml(data.content.substring(0, 500))}${data.content.length > 500 ? '...' : ''}</pre>` : '<em>No content extracted</em>'}
|
|
</div>
|
|
`;
|
|
|
|
// Add to top of results
|
|
resultsContainer.insertBefore(resultDiv, resultsContainer.firstChild);
|
|
|
|
// Limit to 10 results
|
|
while (resultsContainer.children.length > 10) {
|
|
resultsContainer.removeChild(resultsContainer.lastChild);
|
|
}
|
|
|
|
// Highlight animation
|
|
resultDiv.style.animation = 'slideIn 0.3s ease-out';
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Handle form submission
|
|
document.getElementById('queue-form').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
const submitBtn = document.getElementById('submit-btn');
|
|
const responseDiv = document.getElementById('response');
|
|
|
|
// Get form values
|
|
const url = document.getElementById('url').value;
|
|
const selector = document.getElementById('selector').value;
|
|
|
|
// Use our own webhook endpoint
|
|
const webhook = 'https://maxtheweb.com/webhook';
|
|
|
|
// Update UI
|
|
submitBtn.disabled = true;
|
|
submitBtn.textContent = 'Queueing...';
|
|
responseDiv.className = 'response-box loading';
|
|
responseDiv.innerHTML = '<div class="spinner"></div> Sending to AlpenQueue...';
|
|
|
|
try {
|
|
// Build the job payload
|
|
const jobData = {
|
|
url: url,
|
|
selector: selector, // AlpenQueue expects a single string
|
|
webhook_url: webhook // AlpenQueue expects webhook_url, not callback_url
|
|
};
|
|
|
|
// Send to AlpenQueue API
|
|
const response = await fetch(`${API_URL}/jobs`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(jobData)
|
|
});
|
|
|
|
if (response.ok) {
|
|
// AlpenQueue returns plain text like "Job 7 created"
|
|
const resultText = await response.text();
|
|
const jobIdMatch = resultText.match(/Job (\d+) created/);
|
|
const jobId = jobIdMatch ? jobIdMatch[1] : 'Processing';
|
|
currentJobId = jobId;
|
|
|
|
responseDiv.className = 'response-box success';
|
|
responseDiv.innerHTML = `
|
|
<div class="success-icon">✓</div>
|
|
<div class="response-content">
|
|
<strong>Job Queued!</strong><br>
|
|
Job ID: <code>${jobId}</code><br>
|
|
Results will appear below in real-time...
|
|
</div>
|
|
`;
|
|
|
|
// Update jobs counter
|
|
updateJobsCount();
|
|
} else {
|
|
throw new Error(`Server responded with ${response.status}`);
|
|
}
|
|
} catch (error) {
|
|
responseDiv.className = 'response-box error';
|
|
responseDiv.innerHTML = `
|
|
<div class="error-icon">✕</div>
|
|
<div class="response-content">
|
|
<strong>Error:</strong><br>
|
|
${error.message}<br>
|
|
<small>Check the console for details</small>
|
|
</div>
|
|
`;
|
|
console.error('AlpenQueue Error:', error);
|
|
} finally {
|
|
submitBtn.disabled = false;
|
|
submitBtn.textContent = 'Queue it →';
|
|
}
|
|
});
|
|
|
|
// Update jobs count
|
|
async function updateJobsCount() {
|
|
try {
|
|
// For now, simulate with random number
|
|
// In production, this would hit your /stats endpoint
|
|
const count = Math.floor(Math.random() * 100) + 200;
|
|
document.getElementById('jobs-today').textContent = count.toLocaleString();
|
|
} catch (error) {
|
|
document.getElementById('jobs-today').textContent = '247';
|
|
}
|
|
}
|
|
|
|
// Initialize on load
|
|
window.addEventListener('load', () => {
|
|
// Pre-fill with a demo example
|
|
document.getElementById('url').value = 'https://news.ycombinator.com';
|
|
document.getElementById('selector').value = '.athing .titleline';
|
|
|
|
// Start SSE connection
|
|
initSSE();
|
|
|
|
// Update jobs count
|
|
updateJobsCount();
|
|
setInterval(updateJobsCount, 30000); // Update every 30 seconds
|
|
});
|
|
|
|
// Cleanup on page unload
|
|
window.addEventListener('beforeunload', () => {
|
|
if (eventSource) {
|
|
eventSource.close();
|
|
}
|
|
}); |