Portfolio/webhook-sse/main.go
Max 186baa1239 Transform portfolio into live playground with real-time results
- 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!
2025-11-16 17:43:17 +00:00

182 lines
4.1 KiB
Go

package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"sync"
"time"
)
// WebhookPayload matches AlpenQueue's webhook format
type WebhookPayload struct {
Status string `json:"status"`
Took string `json:"took"`
URL string `json:"url"`
Content string `json:"content"`
}
// SSEBroker manages SSE connections and broadcasts
type SSEBroker struct {
clients map[chan string]bool
newClients chan chan string
deadClients chan chan string
messages chan string
mu sync.RWMutex
}
func NewSSEBroker() *SSEBroker {
broker := &SSEBroker{
clients: make(map[chan string]bool),
newClients: make(chan chan string),
deadClients: make(chan chan string),
messages: make(chan string, 100),
}
go broker.run()
return broker
}
func (b *SSEBroker) run() {
for {
select {
case client := <-b.newClients:
b.mu.Lock()
b.clients[client] = true
b.mu.Unlock()
log.Printf("New SSE client connected. Total: %d", len(b.clients))
case client := <-b.deadClients:
b.mu.Lock()
delete(b.clients, client)
close(client)
b.mu.Unlock()
log.Printf("SSE client disconnected. Total: %d", len(b.clients))
case msg := <-b.messages:
b.mu.RLock()
for client := range b.clients {
select {
case client <- msg:
default:
// Client is slow/blocked, skip
}
}
b.mu.RUnlock()
}
}
}
func (b *SSEBroker) Broadcast(message string) {
b.messages <- message
}
func (b *SSEBroker) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Set SSE headers
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Access-Control-Allow-Origin", "https://maxtheweb.com")
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "Streaming not supported", http.StatusInternalServerError)
return
}
messageChan := make(chan string)
b.newClients <- messageChan
// Send initial connection message
fmt.Fprintf(w, "data: {\"type\":\"connected\"}\n\n")
flusher.Flush()
// Keep-alive ticker
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case msg := <-messageChan:
fmt.Fprintf(w, "data: %s\n\n", msg)
flusher.Flush()
case <-ticker.C:
fmt.Fprintf(w, ": keep-alive\n\n")
flusher.Flush()
case <-r.Context().Done():
b.deadClients <- messageChan
return
}
}
}
var broker *SSEBroker
func webhookHandler(w http.ResponseWriter, r *http.Request) {
// CORS headers
w.Header().Set("Access-Control-Allow-Origin", "https://alpenqueue.maxtheweb.com")
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusNoContent)
return
}
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("Error reading webhook body: %v", err)
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
defer r.Body.Close()
var payload WebhookPayload
if err := json.Unmarshal(body, &payload); err != nil {
log.Printf("Error parsing webhook JSON: %v", err)
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
log.Printf("Received webhook: status=%s, url=%s, took=%s", payload.Status, payload.URL, payload.Took)
// Create SSE message
sseMessage := map[string]interface{}{
"type": "result",
"status": payload.Status,
"took": payload.Took,
"url": payload.URL,
"content": payload.Content,
"timestamp": time.Now().Format(time.RFC3339),
}
jsonMsg, _ := json.Marshal(sseMessage)
broker.Broadcast(string(jsonMsg))
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
func main() {
broker = NewSSEBroker()
http.HandleFunc("/webhook", webhookHandler)
http.Handle("/events", broker)
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "OK")
})
log.Println("Webhook SSE server starting on :8081")
if err := http.ListenAndServe(":8081", nil); err != nil {
log.Fatal(err)
}
}