chore(tui): polish demo + regenerate screenshots
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
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user