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())
+ }
+}