03cbe283df
Rework the VHS demo so the README screenshots actually entice a download. Demo data / tooling: - seed.yaml: real, reachable service URLs (detail now shows nextcloud.com, not example.com); Auth Portal -> non-resolving home.arpa host so it reads as a believable, reliably-DOWN monitor - backfill: transient outages for Nextcloud/Jellyfin/Immich aligned with their state changes (uptime % now matches); log timestamps derived from now so the Logs view reads chronologically; real SSL warning; three probe nodes across regions; seeded alert send health - demo.tape: shorter warm-up, added Nodes + theme captures, ordered so every shot stays inside the 60s node-freshness window (consistent probe count) - vhs/crop: new tool to trim the empty terminal border around each screenshot - setup.sh: build backfill up front for deterministic timing; UPTOP_DEMO=1 Supporting code: - persist alert send health (new alert_health table, load on startup, best-effort save on send) so health/last-sent survive restarts - latency Min/Avg/Max ignore failed checks (no more "Min 0ms") - correct "probe"/"probes" pluralization - stable status dot instead of an animated spinner under UPTOP_DEMO
124 lines
2.8 KiB
Go
124 lines
2.8 KiB
Go
// Command crop trims the uniform background border around each VHS screenshot so the
|
|
// content fills the frame instead of floating in a large empty terminal. Sparse views
|
|
// (alerts, detail, nodes) would otherwise sit in a sea of dead space.
|
|
//
|
|
// Usage: crop [dir] (dir defaults to vhs/screenshots)
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"image/png"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
// pad is the margin (px) left around the detected content. tol is the per-channel
|
|
// colour distance (summed) above which a pixel counts as content rather than background.
|
|
const (
|
|
pad = 24
|
|
tol = 28
|
|
)
|
|
|
|
func main() {
|
|
dir := "vhs/screenshots"
|
|
if len(os.Args) > 1 {
|
|
dir = os.Args[1]
|
|
}
|
|
paths, err := filepath.Glob(filepath.Join(dir, "*.png"))
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "glob: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if len(paths) == 0 {
|
|
fmt.Fprintf(os.Stderr, "no PNGs in %s\n", dir)
|
|
os.Exit(1)
|
|
}
|
|
for _, p := range paths {
|
|
w, h, err := cropFile(p)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "crop %s: %v\n", p, err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("cropped %s -> %dx%d\n", filepath.Base(p), w, h)
|
|
}
|
|
}
|
|
|
|
func cropFile(path string) (int, int, error) {
|
|
f, err := os.Open(path) //nolint:gosec // dev tool: paths come from a trusted local glob
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
src, err := png.Decode(f)
|
|
_ = f.Close()
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
|
|
b := src.Bounds()
|
|
// Background colour sampled from a corner — always inside VHS's blank padding.
|
|
bgR, bgG, bgB := rgb(src.At(b.Min.X+2, b.Min.Y+2))
|
|
|
|
minX, minY := b.Max.X, b.Max.Y
|
|
maxX, maxY := b.Min.X, b.Min.Y
|
|
found := false
|
|
for y := b.Min.Y; y < b.Max.Y; y++ {
|
|
for x := b.Min.X; x < b.Max.X; x++ {
|
|
r, g, bl := rgb(src.At(x, y))
|
|
if abs(r-bgR)+abs(g-bgG)+abs(bl-bgB) > tol {
|
|
found = true
|
|
minX, minY = min(minX, x), min(minY, y)
|
|
maxX, maxY = max(maxX, x), max(maxY, y)
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
return b.Dx(), b.Dy(), nil // blank frame — leave untouched
|
|
}
|
|
|
|
minX = clamp(minX-pad, b.Min.X, b.Max.X)
|
|
minY = clamp(minY-pad, b.Min.Y, b.Max.Y)
|
|
maxX = clamp(maxX+pad+1, b.Min.X, b.Max.X)
|
|
maxY = clamp(maxY+pad+1, b.Min.Y, b.Max.Y)
|
|
|
|
dst := image.NewRGBA(image.Rect(0, 0, maxX-minX, maxY-minY))
|
|
for y := minY; y < maxY; y++ {
|
|
for x := minX; x < maxX; x++ {
|
|
dst.Set(x-minX, y-minY, src.At(x, y))
|
|
}
|
|
}
|
|
|
|
out, err := os.Create(path) //nolint:gosec // dev tool: paths come from a trusted local glob
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
defer out.Close() //nolint:errcheck // best-effort close on write path
|
|
if err := png.Encode(out, dst); err != nil {
|
|
return 0, 0, err
|
|
}
|
|
return dst.Bounds().Dx(), dst.Bounds().Dy(), nil
|
|
}
|
|
|
|
func rgb(c color.Color) (int, int, int) {
|
|
r, g, b, _ := c.RGBA()
|
|
return int(r >> 8), int(g >> 8), int(b >> 8)
|
|
}
|
|
|
|
func abs(x int) int {
|
|
if x < 0 {
|
|
return -x
|
|
}
|
|
return x
|
|
}
|
|
|
|
func clamp(v, lo, hi int) int {
|
|
if v < lo {
|
|
return lo
|
|
}
|
|
if v > hi {
|
|
return hi
|
|
}
|
|
return v
|
|
}
|