diff --git a/cmd/agent-gate/init_cmd.go b/cmd/agent-gate/init_cmd.go index 579dab0..9ab164a 100644 --- a/cmd/agent-gate/init_cmd.go +++ b/cmd/agent-gate/init_cmd.go @@ -40,6 +40,14 @@ func initCmd() *cobra.Command { caDir := filepath.Join(configDir, "ca") interactive := !nonInteractive && isInteractive() + + // Banner: show for any non-quiet run. Suppressed for + // --print-config (stdout is structured) and --quiet. Goes + // to stderr so it never contaminates a piped --print-config. + if !quiet && !printConfig { + v, _, _ := buildInfo() + initwizard.PrintBanner(cmd.ErrOrStderr(), v) + } var prompter initwizard.Prompter if interactive { prompter = initwizard.HuhPrompter{} diff --git a/internal/dashboard/server.go b/internal/dashboard/server.go index 8e7ee06..f65b718 100644 --- a/internal/dashboard/server.go +++ b/internal/dashboard/server.go @@ -40,6 +40,12 @@ func NewServer(opts Options) http.Handler { staticFS, _ := fs.Sub(assets, "static") mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS)))) + // Some browsers still request /favicon.ico even with . + // Send them to the SVG instead of letting it 404. + mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "/static/favicon.svg", http.StatusFound) + }) + // Routes. mux.HandleFunc("/", handleSessionsList(opts, r)) mux.HandleFunc("/sessions/", handleSessionDetail(opts, r)) diff --git a/internal/dashboard/server_test.go b/internal/dashboard/server_test.go index 8f5a5ec..7e57b0d 100644 --- a/internal/dashboard/server_test.go +++ b/internal/dashboard/server_test.go @@ -60,6 +60,33 @@ func TestServerRendersFullPageOnGetRoot(t *testing.T) { assert.Contains(t, bodyStr, " + + + + + + + + + + + diff --git a/internal/dashboard/templates/base.html b/internal/dashboard/templates/base.html index 7ff003b..9cde565 100644 --- a/internal/dashboard/templates/base.html +++ b/internal/dashboard/templates/base.html @@ -4,6 +4,7 @@ agent-gate + diff --git a/internal/initwizard/banner.go b/internal/initwizard/banner.go new file mode 100644 index 0000000..4cedd5c --- /dev/null +++ b/internal/initwizard/banner.go @@ -0,0 +1,44 @@ +package initwizard + +import ( + "fmt" + "io" + + "github.com/charmbracelet/lipgloss" +) + +// bannerArt is the wordmark printed at the top of `agent-gate init`. +// Block characters render at any reasonable terminal width (~42 cols) +// and survive copy/paste better than figlet output. Indentation is +// added by PrintBanner so lipgloss styling doesn't strip leading +// whitespace from line 1. +var bannerArt = []string{ + "▄▀█ █▀▀ █▀▀ █▄ █ ▀█▀ █▀▀ ▄▀█ ▀█▀ █▀▀", + "█▀█ █▄█ ██▄ █ ▀█ █ █▄█ █▀█ █ ██▄", +} + +const ( + bannerIndent = " " + bannerTagline = "◆ local traffic audit gate" +) + +// PrintBanner writes the agent-gate wordmark + tagline to w. Caller decides +// whether to suppress it (e.g. --quiet, --print-config). +// +// version is shown after the tagline; pass "" to omit. Color is applied via +// lipgloss; lipgloss auto-degrades on TERM=dumb / non-TTY writers. +func PrintBanner(w io.Writer, version string) { + accent := lipgloss.NewStyle().Foreground(lipgloss.Color("#0E7C5A")).Bold(true) + tagline := lipgloss.NewStyle().Foreground(lipgloss.Color("#0E7C5A")).Faint(true) + + fmt.Fprintln(w) + for _, line := range bannerArt { + fmt.Fprintln(w, bannerIndent+accent.Render(line)) + } + line := bannerTagline + if version != "" { + line = fmt.Sprintf("%s · %s", bannerTagline, version) + } + fmt.Fprintln(w, bannerIndent+tagline.Render(line)) + fmt.Fprintln(w) +} diff --git a/internal/initwizard/banner_test.go b/internal/initwizard/banner_test.go new file mode 100644 index 0000000..4f093a4 --- /dev/null +++ b/internal/initwizard/banner_test.go @@ -0,0 +1,40 @@ +package initwizard + +import ( + "bytes" + "strings" + "testing" +) + +func TestPrintBannerContainsWordmarkAndTagline(t *testing.T) { + var buf bytes.Buffer + PrintBanner(&buf, "") + got := buf.String() + + for _, line := range bannerArt { + if !strings.Contains(got, line) { + t.Errorf("banner missing wordmark line %q\nfull output:\n%s", line, got) + } + } + if !strings.Contains(got, bannerTagline) { + t.Errorf("banner missing tagline %q\nfull output:\n%s", bannerTagline, got) + } +} + +func TestPrintBannerIncludesVersionWhenProvided(t *testing.T) { + var buf bytes.Buffer + PrintBanner(&buf, "v0.3.1") + got := buf.String() + + if !strings.Contains(got, "v0.3.1") { + t.Errorf("banner did not include version; output:\n%s", got) + } +} + +func TestPrintBannerOmitsVersionWhenEmpty(t *testing.T) { + var buf bytes.Buffer + PrintBanner(&buf, "") + if strings.Contains(buf.String(), " · ") { + t.Errorf("banner should not include separator when version is empty; output:\n%s", buf.String()) + } +}