Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions cmd/agent-gate/init_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand Down
6 changes: 6 additions & 0 deletions internal/dashboard/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <link rel="icon">.
// 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))
Expand Down
27 changes: 27 additions & 0 deletions internal/dashboard/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,33 @@ func TestServerRendersFullPageOnGetRoot(t *testing.T) {
assert.Contains(t, bodyStr, "<html")
assert.Contains(t, bodyStr, "agent-gate")
assert.Contains(t, bodyStr, `src="/static/htmx.min.js"`)
assert.Contains(t, bodyStr, `href="/static/favicon.svg"`)
}

func TestServerServesFavicon(t *testing.T) {
srv := httptest.NewServer(NewServer(freshOpts(t)))
defer srv.Close()

resp, err := http.Get(srv.URL + "/static/favicon.svg")
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
body, _ := io.ReadAll(resp.Body)
assert.Contains(t, string(body), "<svg")
}

func TestServerRedirectsFaviconIco(t *testing.T) {
srv := httptest.NewServer(NewServer(freshOpts(t)))
defer srv.Close()

client := &http.Client{CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}}
resp, err := client.Get(srv.URL + "/favicon.ico")
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusFound, resp.StatusCode)
assert.Equal(t, "/static/favicon.svg", resp.Header.Get("Location"))
}

func TestServerRefusesNonLoopbackBind(t *testing.T) {
Expand Down
14 changes: 14 additions & 0 deletions internal/dashboard/static/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions internal/dashboard/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>agent-gate</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Plus+Jakarta+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap">
Expand Down
44 changes: 44 additions & 0 deletions internal/initwizard/banner.go
Original file line number Diff line number Diff line change
@@ -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)
}
40 changes: 40 additions & 0 deletions internal/initwizard/banner_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
}
Loading