From 150f8058d64e3e7545f3f81962441ed6d3a3ad9d Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Fri, 10 Apr 2026 15:42:11 +0300 Subject: Release v0.1.5 --sync rsync to pi mirrors when reachable; default dirs ./inbox ./dist; ignore inbox/dist. Splash index paths (/, trailing slash). PgUp/PgDn scroll + highlight. Nav shared CSS in head for valid HTML; ARIA/viewport fixes; neon viewport. Made-with: Cursor --- .gitignore | 2 + cmd/snonux/main.go | 11 ++++- cmd/snonux/main_test.go | 22 ++++++++++ cmd/snonux/sync.go | 71 ++++++++++++++++++++++++++++++ internal/config/config.go | 3 ++ internal/generator/doc.go | 3 +- internal/generator/shared.go | 81 +++++++++++++++++++++++++++-------- internal/generator/theme_aurora.go | 3 +- internal/generator/theme_brutalist.go | 3 +- internal/generator/theme_glass.go | 3 +- internal/generator/theme_matrix.go | 3 +- internal/generator/theme_minimal.go | 3 +- internal/generator/theme_neon.go | 5 ++- internal/generator/theme_ocean.go | 3 +- internal/generator/theme_paper.go | 3 +- internal/generator/theme_retro.go | 3 +- internal/generator/theme_synthwave.go | 3 +- internal/generator/theme_terminal.go | 3 +- internal/generator/themes.go | 3 +- internal/version/version.go | 2 +- 20 files changed, 197 insertions(+), 36 deletions(-) create mode 100644 .gitignore create mode 100644 cmd/snonux/sync.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..60dcd17 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +inbox/ +dist/ diff --git a/cmd/snonux/main.go b/cmd/snonux/main.go index 1d00646..42a377f 100644 --- a/cmd/snonux/main.go +++ b/cmd/snonux/main.go @@ -4,7 +4,7 @@ // // Usage: // -// snonux --input ./inbox --output ./outdir [--base-url https://snonux.foo] +// snonux [--input ./inbox] [--output ./dist] [--base-url https://snonux.foo] package main import ( @@ -51,6 +51,12 @@ func main() { if err := run(cfg); err != nil { log.Fatalf("error: %v", err) } + + if cfg.Sync { + if err := syncOutput(cfg.OutputDir); err != nil { + log.Fatalf("error: %v", err) + } + } } // errParseFlags is returned when flag parsing fails (e.g. unknown flag). @@ -69,9 +75,10 @@ func parseFlags(args []string) (*config.Config, cliMode, error) { listThemes := fs.Bool("list-themes", false, "print all available theme names and exit") fs.StringVar(&cfg.InputDir, "input", "./inbox", "directory containing new source files to process") - fs.StringVar(&cfg.OutputDir, "output", "~/git/snonux.foo/dist", "root directory for generated static site output") + fs.StringVar(&cfg.OutputDir, "output", "./dist", "root directory for generated static site output") fs.StringVar(&cfg.BaseURL, "base-url", "https://snonux.foo", "canonical base URL used in Atom feed links") fs.StringVar(&cfg.Theme, "theme", "random", "visual theme name, or \"random\" to pick one at random") + fs.BoolVar(&cfg.Sync, "sync", false, "after a successful run, rsync -output to pi0/pi1 when both are pingable (SSH user: SNONUX_SYNC_USER or login name)") if err := fs.Parse(args); err != nil { return nil, modeRun, fmt.Errorf("%w: %w", errParseFlags, err) diff --git a/cmd/snonux/main_test.go b/cmd/snonux/main_test.go index 212efc8..7f05c8b 100644 --- a/cmd/snonux/main_test.go +++ b/cmd/snonux/main_test.go @@ -104,6 +104,28 @@ func TestParseFlags_run(t *testing.T) { } } +func TestParseFlags_sync(t *testing.T) { + t.Parallel() + + in := t.TempDir() + out := t.TempDir() + cfg, mode, err := parseFlags([]string{ + "-input", in, + "-output", out, + "-theme", "neon", + "-sync", + }) + if err != nil { + t.Fatal(err) + } + if mode != modeRun { + t.Fatalf("mode %v", mode) + } + if !cfg.Sync { + t.Fatal("expected cfg.Sync") + } +} + func TestParseFlags_randomTheme(t *testing.T) { t.Parallel() diff --git a/cmd/snonux/sync.go b/cmd/snonux/sync.go new file mode 100644 index 0000000..46993e2 --- /dev/null +++ b/cmd/snonux/sync.go @@ -0,0 +1,71 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "os/exec" + "os/user" + "path/filepath" + "time" +) + +// SNONUX_SYNC_USER overrides the SSH username for rsync (default: current login name). +const envSyncUser = "SNONUX_SYNC_USER" + +var syncTargets = []string{ + "pi0.lan.buetow.org", + "pi1.lan.buetow.org", +} + +const syncRemoteDir = "/var/www/html/snonux/" + +// syncOutput rsyncs localOutput (trailing-slash source) to each sync target over SSH +// port 22. It runs only if every target answers ICMP ping (Linux iputils: ping -c 1 -W …). +func syncOutput(localOutput string) error { + for _, host := range syncTargets { + if !hostPingable(host) { + log.Printf("sync skipped: %q not pingable (all mirror hosts must be reachable)", host) + return nil + } + } + + sshUser := os.Getenv(envSyncUser) + if sshUser == "" { + u, err := user.Current() + if err != nil { + return fmt.Errorf("sync user: %w (set %s)", err, envSyncUser) + } + sshUser = u.Username + } + + absOut, err := filepath.Abs(localOutput) + if err != nil { + return fmt.Errorf("sync output dir: %w", err) + } + src := filepath.Clean(absOut) + string(filepath.Separator) + + ssh := "ssh -p 22 -o BatchMode=yes -o ConnectTimeout=15" + for _, host := range syncTargets { + dest := fmt.Sprintf("%s@%s:%s", sshUser, host, syncRemoteDir) + log.Printf("rsync %s -> %s", src, dest) + cmd := exec.Command("rsync", "-az", "-e", ssh, src, dest) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("rsync to %s: %w", host, err) + } + } + return nil +} + +func hostPingable(host string) bool { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + // Linux iputils-ping: -c 1 one packet, -W 3 wait up to 3s for reply. + cmd := exec.CommandContext(ctx, "ping", "-c", "1", "-W", "3", host) + cmd.Stdout = nil + cmd.Stderr = nil + return cmd.Run() == nil +} diff --git a/internal/config/config.go b/internal/config/config.go index fd6e560..f52b445 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -23,4 +23,7 @@ type Config struct { // Theme selects the visual style for generated HTML pages. // Defaults to "neon". Run with --help to see all available themes. Theme string + + // Sync, when true, rsyncs OutputDir to fixed mirror hosts after a successful run. + Sync bool } diff --git a/internal/generator/doc.go b/internal/generator/doc.go index ad974ad..d7d4a53 100644 --- a/internal/generator/doc.go +++ b/internal/generator/doc.go @@ -9,7 +9,8 @@ // - themes.go — Theme registry (name → template string) and getTheme / // ListThemes for the CLI. // - theme_*.go — One file per visual theme: full-page HTML that invokes -// {{template "splashGate"}}, {{template "navhints" .}}, {{template "navmodal" .}}, +// {{template "navSharedCSSInner"}} inside -
+{{end}} + +{{define "navmodal"}} +