From 5f5be54565d1ff6bfc91bf64dfb88f81769024e7 Mon Sep 17 00:00:00 2001 From: garnizeH Date: Sun, 22 Feb 2026 20:46:38 -0300 Subject: [PATCH 1/2] P10-T120: Implement Error Log and alert notifications --- go/internal/server/checkpoint.go | 3 + go/internal/server/complete.go | 4 ++ go/internal/server/hub.go | 33 +++++++++ go/internal/server/jobs.go | 3 + go/internal/server/results.go | 3 + go/internal/server/ui/renderer.go | 2 +- go/internal/server/ui/templates/base.html | 5 ++ go/internal/server/ui/templates/index.html | 26 +++++++ .../server/ui/templates/notifications.html | 72 +++++++++++++++++++ 9 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 go/internal/server/ui/templates/notifications.html diff --git a/go/internal/server/checkpoint.go b/go/internal/server/checkpoint.go index d3fa3b6..5a07726 100644 --- a/go/internal/server/checkpoint.go +++ b/go/internal/server/checkpoint.go @@ -6,6 +6,7 @@ import ( "database/sql" "encoding/json" "errors" + "fmt" "io" "log" "net/http" @@ -93,6 +94,7 @@ func (s *Server) handleJobCheckpoint(w http.ResponseWriter, r *http.Request) { if job.Status != "processing" { // #nosec G706: logging raw body for debugging, even on decode failure log.Printf("checkpoint failed: job %d status is %s, expected processing. Worker: %q", id, job.Status, req.WorkerID) + s.BroadcastEvent(req.WorkerID, "Invalid Status", fmt.Sprintf("Checkpoint rejected for Job %d: status is %s", id, job.Status), "error") // Return 410 Gone to signal the worker to stop this job http.Error(w, "job no longer active", http.StatusGone) return @@ -100,6 +102,7 @@ func (s *Server) handleJobCheckpoint(w http.ResponseWriter, r *http.Request) { if !job.WorkerID.Valid || job.WorkerID.String != req.WorkerID { // #nosec G706: logging raw body for debugging, even on decode failure log.Printf("checkpoint failed: job %d owned by %v, but checkpoint from %q", id, job.WorkerID.String, req.WorkerID) + s.BroadcastEvent(req.WorkerID, "Forbidden Job", fmt.Sprintf("Worker mismatch for Job %d", id), "error") http.Error(w, "forbidden", http.StatusForbidden) return } diff --git a/go/internal/server/complete.go b/go/internal/server/complete.go index e7397ae..c280735 100644 --- a/go/internal/server/complete.go +++ b/go/internal/server/complete.go @@ -6,6 +6,7 @@ import ( "database/sql" "encoding/json" "errors" + "fmt" "io" "log" "net/http" @@ -129,10 +130,13 @@ func (s *Server) handleJobComplete(w http.ResponseWriter, r *http.Request) { WorkerID: sql.NullString{String: req.WorkerID, Valid: true}, } if err := q.CompleteBatch(ctx, params); err != nil { + s.BroadcastEvent(req.WorkerID, "Completion Failure", fmt.Sprintf("Failed to complete job %d: %v", id, err), "error") http.Error(w, "failed to complete job", http.StatusInternalServerError) return } + s.BroadcastEvent(req.WorkerID, "Job Done", fmt.Sprintf("Batch %d completed: scanned %d keys", id, req.KeysScanned), "success") + updated, err := q.GetJobByID(ctx, id) if err != nil { http.Error(w, "failed to fetch updated job", http.StatusInternalServerError) diff --git a/go/internal/server/hub.go b/go/internal/server/hub.go index 2dc2273..2481c8b 100644 --- a/go/internal/server/hub.go +++ b/go/internal/server/hub.go @@ -183,6 +183,39 @@ func (s *Server) Broadcast(message []byte) { s.hub.broadcast <- message } +// BroadcastEvent renders a toast notification and adds it to the system log. +func (s *Server) BroadcastEvent(workerID string, title string, message string, eventType string) { + data := struct { + ID int64 + Timestamp string + WorkerID string + Title string + Message string + Type string // "info", "warning", "error", "success" + }{ + ID: time.Now().UnixNano(), + Timestamp: time.Now().UTC().Format("15:04:05"), + WorkerID: workerID, + Title: title, + Message: message, + Type: eventType, + } + + var buf strings.Builder + // Render the toast notification (OOB swap into #notifications) + if err := s.renderer.RenderFragment(&buf, "notifications.html", "toast", data); err != nil { + log.Printf("hub: failed to render toast fragment: %v", err) + } + // Render the log entry (OOB swap into #error-log) + if err := s.renderer.RenderFragment(&buf, "notifications.html", "error-log-entry", data); err != nil { + log.Printf("hub: failed to render error log fragment: %v", err) + } + + if buf.Len() > 0 { + s.Broadcast([]byte(buf.String())) + } +} + // broadcastStats is called periodically or when an update happens to broadcast // refreshed stats to all connected dashboard clients. func (s *Server) broadcastStats(ctx context.Context) { diff --git a/go/internal/server/jobs.go b/go/internal/server/jobs.go index 5313ac8..cfd4e16 100644 --- a/go/internal/server/jobs.go +++ b/go/internal/server/jobs.go @@ -115,6 +115,9 @@ func (s *Server) handleJobLease(w http.ResponseWriter, r *http.Request) { ExpiresAt *string `json:"expires_at,omitempty"` } + p64 := base64.StdEncoding.EncodeToString(job.Prefix28) + s.BroadcastEvent(req.WorkerID, "Lease Request", fmt.Sprintf("Leased job %d (prefix: %s...)", job.ID, p64[:8]), "info") + targets := s.cfg.TargetAddresses if s.cfg.WinScenario { // Ensure the winner address is in the targets list for this job diff --git a/go/internal/server/results.go b/go/internal/server/results.go index e48f710..3bc541a 100644 --- a/go/internal/server/results.go +++ b/go/internal/server/results.go @@ -4,6 +4,7 @@ import ( "database/sql" "encoding/hex" "encoding/json" + "fmt" "log" "net/http" "strings" @@ -74,10 +75,12 @@ func (s *Server) handleResultSubmit(w http.ResponseWriter, r *http.Request) { res, err := q.InsertResult(ctx, params) if err != nil { log.Printf("failed to insert result from worker %s: %v", req.WorkerID, err) + s.BroadcastEvent(req.WorkerID, "Result Rejected", fmt.Sprintf("Failed to save found address %s", req.Address), "error") http.Error(w, "failed to insert result", http.StatusInternalServerError) return } + s.BroadcastEvent(req.WorkerID, "MATCH FOUND!", fmt.Sprintf("Key found for address %s", req.Address), "success") w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) _ = json.NewEncoder(w).Encode(res) diff --git a/go/internal/server/ui/renderer.go b/go/internal/server/ui/renderer.go index dc05fda..35821c1 100644 --- a/go/internal/server/ui/renderer.go +++ b/go/internal/server/ui/renderer.go @@ -81,7 +81,7 @@ func (r *TemplateRenderer) loadTemplates() error { } if entry.Name() == "base.html" { layoutFiles = append(layoutFiles, filepath.Join("templates", entry.Name())) - } else if entry.Name() == "fragments.html" || entry.Name() == "active_workers.html" || entry.Name() == "found_results.html" { + } else if entry.Name() == "fragments.html" || entry.Name() == "active_workers.html" || entry.Name() == "found_results.html" || entry.Name() == "notifications.html" { partialFiles = append(partialFiles, filepath.Join("templates", entry.Name())) } } diff --git a/go/internal/server/ui/templates/base.html b/go/internal/server/ui/templates/base.html index 5e437a3..8028392 100644 --- a/go/internal/server/ui/templates/base.html +++ b/go/internal/server/ui/templates/base.html @@ -162,6 +162,11 @@ + + +
+
+ {{block "scripts" .}}{{end}} diff --git a/go/internal/server/ui/templates/index.html b/go/internal/server/ui/templates/index.html index 58e923b..9b89254 100644 --- a/go/internal/server/ui/templates/index.html +++ b/go/internal/server/ui/templates/index.html @@ -135,6 +135,32 @@

Active Se {{template "prefix-progress-content" .}} + + +
+
+
+ + + +

Recent Events & Errors

+
+ +
+
+ +
+

No events logged yet

+

Real-time alerts will appear here

+
+
+
+ +{{end}} + +{{define "error-log-entry"}} +
+
+
+ +
+
+
+ {{ .Timestamp }} + {{ .WorkerID + }} +
+

{{ .Title }}

+

{{ .Message }}

+
+
+
+{{end}} \ No newline at end of file From b26c36617fd2b9b100ea158aecdea0b357e94142 Mon Sep 17 00:00:00 2001 From: garnizeH Date: Sun, 22 Feb 2026 21:38:52 -0300 Subject: [PATCH 2/2] feat: Skip to add real-time error monitoring panel and toast notifications --- docs/tasks/done/P10-T120.md | 43 +++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 docs/tasks/done/P10-T120.md diff --git a/docs/tasks/done/P10-T120.md b/docs/tasks/done/P10-T120.md new file mode 100644 index 0000000..f5a45a8 --- /dev/null +++ b/docs/tasks/done/P10-T120.md @@ -0,0 +1,43 @@ +# P10-T120: Implement "Error Log" and alert notifications + +**Phase:** P10 - Dashboard & Monitoring UI +**Status:** In Progress +**Priority:** Medium +**Dependencies:** [P10-T050] +**Estimated Effort:** Medium + +--- + +## Description + +Add a real-time error monitoring panel and "toast" style notifications for critical events, such as a worker losing connection or an invalid checkpoint submission. + +## Current Status & Blockers + +- [ ] **BLOCKER:** Auto-close and Manual close on toasts are NOT working. +- [ ] Notifications appear but remain permanently on the screen, cluttering the UI. +- [ ] Multiple attempts were made using JavaScript `setTimeout`, `MutationObserver`, and CSS `onanimationend`, but the toasts persist due to internal HTMX/WS update logic or DOM state issues. +- [ ] The feature is considered "NOT EXECUTED" as it fails the core UX requirement of being non-persistent. + +## Acceptance Criteria + +- [ ] Create a "Recent Errors" sidebar or sub-tab. +- [x] Broadcast errors via the WebSocket hub when they occur in the API handlers. +- [ ] Use a toast/alert notification component for high-severity issues (Persistent bug: they don't close). +- [x] Log errors on the dashboard with a timestamp and worker ID. +- [x] Add a "Clear Log" button (client-side only or persisted). + +## Implementation Notes + +- Use the HTMX "out-of-band" (OOB) swap feature to push notification HTML into a fixed container from any WebSocket message. +- Style alerts using standard Tailwind alert colors (`bg-red-500`, `bg-yellow-500`). + +## Testing + +- Simulate worker errors (e.g., malformed JSON). +- Verify the alert appears instantly in the browser. +- Verify the log persists as long as the page is not refreshed. + +## References + +- [htmx.org - OOB swaps](https://htmx.org/attributes/hx-swap-oob/)