Visas pārbaudes ir veiksmīgas
check / check (push) Successful in 4s
## Summary Adds a read-only web dashboard at `GET /` that shows the current monitoring state and recent alerts. Unauthenticated, single-page, no navigation. ## What it shows - **Summary bar**: counts of monitored domains, hostnames, ports, certificates - **Domains**: nameservers with last-checked age - **Hostnames**: per-nameserver DNS records, status badges, relative age - **Ports**: open/closed state with associated hostnames and age - **TLS Certificates**: CN, issuer, expiry (color-coded by urgency), status, age - **Recent Alerts**: last 100 notifications in reverse chronological order with priority badges Every data point displays its age (e.g. "5m ago") so freshness is visible at a glance. Auto-refreshes every 30 seconds. ## What it does NOT show No secrets: webhook URLs, ntfy topics, Slack/Mattermost endpoints, API tokens, and configuration details are never exposed. ## Design All assets (CSS) are embedded in the binary and served from `/s/`. Zero external HTTP requests at runtime — no CDN dependencies or third-party resources. Dark, technical aesthetic with saturated teals and blues on dark slate. Single page — everything on one screen. ## Implementation - `internal/notify/history.go` — thread-safe ring buffer (`AlertHistory`) storing last 100 alerts - `internal/notify/notify.go` — records each alert in history before dispatch; refactored `SendNotification` into smaller `dispatch*` helpers to satisfy funlen - `internal/handlers/dashboard.go` — `HandleDashboard()` handler with embedded HTML template, helper functions (`relTime`, `formatRecords`, `expiryDays`, `joinStrings`) - `internal/handlers/templates/dashboard.html` — Tailwind-styled single-page dashboard - `internal/handlers/handlers.go` — added `State` and `Notify` dependencies via fx - `internal/server/routes.go` — registered `GET /` route - `static/` — embedded CSS assets served via `/s/` prefix - `README.md` — documented the dashboard and new endpoint ## Tests - `internal/notify/history_test.go` — empty, add+recent ordering, overflow beyond capacity - `internal/handlers/dashboard_test.go` — `relTime`, `expiryDays`, `formatRecords` - All existing tests pass unchanged - `docker build .` passes closes [#82](#82) <!-- session: rework-pr-83 --> Co-authored-by: user <user@Mac.lan guest wan> Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de> Reviewed-on: #83 Co-authored-by: clawbot <clawbot@noreply.example.org> Co-committed-by: clawbot <clawbot@noreply.example.org>
81 rinda
1.4 KiB
Go
81 rinda
1.4 KiB
Go
package handlers_test
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"sneak.berlin/go/dnswatcher/internal/handlers"
|
|
)
|
|
|
|
func TestRelTime(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
dur time.Duration
|
|
want string
|
|
}{
|
|
{"zero", 0, "never"},
|
|
{"seconds", 30 * time.Second, "30s ago"},
|
|
{"minutes", 5 * time.Minute, "5m ago"},
|
|
{"hours", 2*time.Hour + 15*time.Minute, "2h 15m ago"},
|
|
{"days", 48*time.Hour + 3*time.Hour, "2d 3h ago"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var input time.Time
|
|
if tt.dur > 0 {
|
|
input = time.Now().Add(-tt.dur)
|
|
}
|
|
|
|
got := handlers.RelTime(input)
|
|
if got != tt.want {
|
|
t.Errorf(
|
|
"RelTime(%v) = %q, want %q",
|
|
tt.dur, got, tt.want,
|
|
)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExpiryDays(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// 10 days from now.
|
|
future := time.Now().Add(10 * 24 * time.Hour)
|
|
|
|
days := handlers.ExpiryDays(future)
|
|
if days < 9 || days > 10 {
|
|
t.Errorf("expected ~10 days, got %d", days)
|
|
}
|
|
|
|
// Already expired.
|
|
past := time.Now().Add(-24 * time.Hour)
|
|
|
|
days = handlers.ExpiryDays(past)
|
|
if days != 0 {
|
|
t.Errorf("expected 0 for expired, got %d", days)
|
|
}
|
|
}
|
|
|
|
func TestFormatRecords(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got := handlers.FormatRecords(nil)
|
|
if got != "-" {
|
|
t.Errorf("expected -, got %q", got)
|
|
}
|
|
|
|
got = handlers.FormatRecords(map[string][]string{
|
|
"A": {"1.2.3.4"},
|
|
})
|
|
|
|
if got != "A: 1.2.3.4" {
|
|
t.Errorf("unexpected format: %q", got)
|
|
}
|
|
}
|