diff --git a/pkg/hold/admin/handlers_relays.go b/pkg/hold/admin/handlers_relays.go index f75a94d..693f92e 100644 --- a/pkg/hold/admin/handlers_relays.go +++ b/pkg/hold/admin/handlers_relays.go @@ -1,7 +1,6 @@ package admin import ( - "fmt" "log/slog" "net/http" "net/url" @@ -85,71 +84,87 @@ func (ui *AdminUI) handleRelayStatus(w http.ResponseWriter, r *http.Request) { ui.renderTemplate(w, "partials/relay_status.html", view) } -// handleRelayCrawl requests crawl from a single relay. -func (ui *AdminUI) handleRelayCrawl(w http.ResponseWriter, r *http.Request) { - if err := r.ParseForm(); err != nil { - setFlash(w, r, "error", "Invalid form data") - http.Redirect(w, r, "/admin#relays", http.StatusFound) - return - } - - relayURL := r.FormValue("url") - if relayURL == "" { - setFlash(w, r, "error", "Missing relay URL") - http.Redirect(w, r, "/admin#relays", http.StatusFound) - return - } - - if err := atproto.RequestCrawl(relayURL, ui.config.PublicURL); err != nil { - slog.Warn("Failed to request crawl from relay", "relay", relayURL, "error", err) - setFlash(w, r, "error", "Crawl request failed: "+err.Error()) - } else { - slog.Info("Crawl requested via admin panel", "relay", relayURL) - setFlash(w, r, "success", "Crawl requested from "+relayURL) - } - - http.Redirect(w, r, "/admin#relays", http.StatusFound) +// RelayCrawlResultView is the data for a single relay crawl result row. +type RelayCrawlResultView struct { + Name string + URL string + Success bool + Error string } -// handleRelayCrawlAll requests crawl from all known relays. +// handleRelayCrawl requests crawl from a single relay and returns an HTMX partial. +func (ui *AdminUI) handleRelayCrawl(w http.ResponseWriter, r *http.Request) { + relayURL := r.URL.Query().Get("url") + relayName := r.URL.Query().Get("name") + if relayURL == "" { + http.Error(w, "Missing relay URL", http.StatusBadRequest) + return + } + + err := atproto.RequestCrawl(relayURL, ui.config.PublicURL) + + view := RelayCrawlResultView{ + Name: relayName, + URL: relayURL, + Success: err == nil, + } + if err != nil { + view.Error = err.Error() + slog.Warn("Failed to request crawl from relay", "relay", relayURL, "error", err) + } else { + slog.Info("Crawl requested via admin panel", "relay", relayURL) + } + + ui.renderTemplate(w, "partials/relay_crawl_result.html", view) +} + +// handleRelayCrawlAll requests crawl from all known relays and returns HTMX partials. func (ui *AdminUI) handleRelayCrawlAll(w http.ResponseWriter, r *http.Request) { relays := atproto.KnownRelays - var ( - wg sync.WaitGroup - mu sync.Mutex - successes int - failures int - ) + type result struct { + relay atproto.KnownRelay + err error + } - for _, relay := range relays { + results := make([]result, len(relays)) + var wg sync.WaitGroup + + for i, relay := range relays { wg.Add(1) - go func(relay atproto.KnownRelay) { + go func(i int, relay atproto.KnownRelay) { defer wg.Done() - if err := atproto.RequestCrawl(relay.URL, ui.config.PublicURL); err != nil { - slog.Warn("Failed to request crawl", "relay", relay.Name, "error", err) - mu.Lock() - failures++ - mu.Unlock() - } else { - mu.Lock() - successes++ - mu.Unlock() - } - }(relay) + err := atproto.RequestCrawl(relay.URL, ui.config.PublicURL) + results[i] = result{relay: relay, err: err} + }(i, relay) } wg.Wait() session := getSessionFromContext(r.Context()) + successes := 0 + for _, res := range results { + if res.err == nil { + successes++ + } + } slog.Info("Crawl all requested via admin panel", - "successes", successes, "failures", failures, "by", session.DID) + "successes", successes, "failures", len(relays)-successes, "by", session.DID) - if failures == 0 { - setFlash(w, r, "success", fmt.Sprintf("Crawl requested from all %d relays", successes)) - } else { - setFlash(w, r, "warning", fmt.Sprintf("Crawl: %d succeeded, %d failed", successes, failures)) + var views []RelayCrawlResultView + for _, res := range results { + v := RelayCrawlResultView{ + Name: res.relay.Name, + URL: res.relay.URL, + Success: res.err == nil, + } + if res.err != nil { + v.Error = res.err.Error() + } + views = append(views, v) } - http.Redirect(w, r, "/admin#relays", http.StatusFound) + ui.renderTemplate(w, "partials/relay_crawl_results.html", struct { + Results []RelayCrawlResultView + }{Results: views}) } diff --git a/pkg/hold/admin/templates/partials/relay_crawl_result.html b/pkg/hold/admin/templates/partials/relay_crawl_result.html new file mode 100644 index 0000000..70b82b4 --- /dev/null +++ b/pkg/hold/admin/templates/partials/relay_crawl_result.html @@ -0,0 +1,48 @@ +{{define "partials/relay_crawl_result.html"}} + + + {{if .Success}} + + {{ icon "check-circle" "size-3" }} + Sent + + {{else}} + + {{ icon "alert-circle" "size-3" }} + Failed + + {{end}} + + +
+ {{.Name}}
+ {{.URL}} +
+ + + - + + + {{if .Success}} + + + Crawl requested, refreshing... + + {{else}} + {{.Error}} + {{end}} + + + + + +{{end}} diff --git a/pkg/hold/admin/templates/partials/relay_crawl_results.html b/pkg/hold/admin/templates/partials/relay_crawl_results.html new file mode 100644 index 0000000..b751a19 --- /dev/null +++ b/pkg/hold/admin/templates/partials/relay_crawl_results.html @@ -0,0 +1,50 @@ +{{define "partials/relay_crawl_results.html"}} +{{range .Results}} + + + {{if .Success}} + + {{ icon "check-circle" "size-3" }} + Sent + + {{else}} + + {{ icon "alert-circle" "size-3" }} + Failed + + {{end}} + + +
+ {{.Name}}
+ {{.URL}} +
+ + + - + + + {{if .Success}} + + + Crawl requested, refreshing... + + {{else}} + {{.Error}} + {{end}} + + + + + +{{end}} +{{end}} diff --git a/pkg/hold/admin/templates/partials/relay_status.html b/pkg/hold/admin/templates/partials/relay_status.html index 0a78539..1a590cb 100644 --- a/pkg/hold/admin/templates/partials/relay_status.html +++ b/pkg/hold/admin/templates/partials/relay_status.html @@ -48,13 +48,14 @@ {{if .Online}} -
- - -
+ {{end}} diff --git a/pkg/hold/admin/templates/partials/tab_relays.html b/pkg/hold/admin/templates/partials/tab_relays.html index 2193aeb..289e8d0 100644 --- a/pkg/hold/admin/templates/partials/tab_relays.html +++ b/pkg/hold/admin/templates/partials/tab_relays.html @@ -1,12 +1,24 @@ {{define "partials/tab_relays.html"}}

Relays

-
- -
+ +
+ +
+
+ +
+

Requesting crawl from all relays...

+

This may take a few seconds.

+
+
@@ -21,7 +33,7 @@ Actions - + {{range .Relays}} = 0: Backfill events from cursor onwards @@ -414,6 +421,41 @@ func (b *EventBroadcaster) Subscribe(conn *websocket.Conn, cursor int64, userAge return sub } +// sendAccountEvent writes an #account event directly to a WebSocket connection, +// signaling that this DID is active on this host. This is critical for relays +// that previously saw the DID deactivated on a different PDS. +func (b *EventBroadcaster) sendAccountEvent(conn *websocket.Conn) error { + header := events.EventHeader{ + Op: events.EvtKindMessage, + MsgType: "#account", + } + + wc, err := conn.NextWriter(websocket.BinaryMessage) + if err != nil { + return fmt.Errorf("failed to get websocket writer: %w", err) + } + + if err := header.MarshalCBOR(wc); err != nil { + wc.Close() + return fmt.Errorf("failed to write account event header: %w", err) + } + + acctEvt := &atproto.SyncSubscribeRepos_Account{ + Active: true, + Did: b.holdDID, + Seq: 0, // Not sequenced in the commit stream + Time: time.Now().Format(time.RFC3339), + } + + var obj lexutil.CBOR = acctEvt + if err := obj.MarshalCBOR(wc); err != nil { + wc.Close() + return fmt.Errorf("failed to write account event body: %w", err) + } + + return wc.Close() +} + // Unsubscribe removes a WebSocket subscriber func (b *EventBroadcaster) Unsubscribe(sub *Subscriber) { b.mu.Lock()