--- phase: 18-web-dashboard plan: 01 type: execute wave: 1 depends_on: [] files_modified: - pkg/web/server.go - pkg/web/auth.go - pkg/web/handlers.go - pkg/web/embed.go - pkg/web/static/htmx.min.js - pkg/web/static/style.css - pkg/web/templates/layout.html - pkg/web/templates/overview.html - pkg/web/server_test.go autonomous: true requirements: [WEB-01, WEB-02, WEB-10] must_haves: truths: - "chi v5 HTTP server starts on configurable port and serves embedded static assets" - "Overview page renders with summary statistics from database" - "Optional basic auth / token auth blocks unauthenticated requests when configured" artifacts: - path: "pkg/web/server.go" provides: "chi router setup, middleware stack, NewServer constructor" exports: ["Server", "NewServer", "Config"] - path: "pkg/web/auth.go" provides: "Basic auth and bearer token auth middleware" exports: ["AuthMiddleware"] - path: "pkg/web/handlers.go" provides: "Overview page handler with stats aggregation" exports: ["handleOverview"] - path: "pkg/web/embed.go" provides: "go:embed directives for static/ and templates/" exports: ["staticFS", "templateFS"] - path: "pkg/web/server_test.go" provides: "Integration tests for server, auth, overview" key_links: - from: "pkg/web/server.go" to: "pkg/storage" via: "DB dependency in Config struct" pattern: "storage\\.DB" - from: "pkg/web/handlers.go" to: "pkg/web/templates/overview.html" via: "html/template rendering" pattern: "template\\..*Execute" - from: "pkg/web/server.go" to: "pkg/web/static/" via: "go:embed + http.FileServer" pattern: "http\\.FileServer" --- Create the pkg/web package foundation: chi v5 router, go:embed static assets (htmx.min.js, Tailwind CDN reference), html/template-based layout, overview dashboard page with stats, and optional auth middleware. Purpose: Establishes the HTTP server skeleton that Plans 02 and 03 build upon. Output: Working `pkg/web` package with chi router, static serving, layout template, overview page, auth middleware. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/18-web-dashboard/18-CONTEXT.md From pkg/storage/db.go: ```go type DB struct { ... } func Open(path string) (*DB, error) func (db *DB) Close() error func (db *DB) SQL() *sql.DB ``` From pkg/storage/findings.go: ```go type Finding struct { ID, ScanID int64 ProviderName string KeyValue, KeyMasked, Confidence string SourcePath, SourceType string LineNumber int CreatedAt time.Time Verified bool VerifyStatus string VerifyHTTPCode int VerifyMetadata map[string]string } func (db *DB) ListFindings(encKey []byte) ([]Finding, error) func (db *DB) SaveFinding(f Finding, encKey []byte) (int64, error) ``` From pkg/storage/queries.go: ```go type Filters struct { Provider, Confidence, SourceType string Verified *bool Limit, Offset int } func (db *DB) ListFindingsFiltered(encKey []byte, f Filters) ([]Finding, error) func (db *DB) GetFinding(id int64, encKey []byte) (*Finding, error) func (db *DB) DeleteFinding(id int64) (int64, error) ``` From pkg/providers/registry.go: ```go type Registry struct { ... } func NewRegistry() (*Registry, error) func (r *Registry) List() []Provider func (r *Registry) Stats() RegistryStats ``` From pkg/dorks/registry.go: ```go type Registry struct { ... } func NewRegistry() (*Registry, error) func (r *Registry) List() []Dork func (r *Registry) Stats() Stats ``` From pkg/recon/engine.go: ```go type Engine struct { ... } func NewEngine() *Engine func (e *Engine) SweepAll(ctx context.Context, cfg Config) ([]Finding, error) func (e *Engine) List() []string ``` Task 1: chi v5 dependency + go:embed static assets + layout template pkg/web/embed.go, pkg/web/static/htmx.min.js, pkg/web/static/style.css, pkg/web/templates/layout.html, pkg/web/templates/overview.html 1. Run `go get github.com/go-chi/chi/v5@v5.2.5` to add chi v5 to go.mod. 2. Create `pkg/web/embed.go`: - `//go:embed static/*` into `var staticFiles embed.FS` - `//go:embed templates/*` into `var templateFiles embed.FS` - Export both via package-level vars. 3. Download htmx v2.0.4 minified JS (curl from unpkg.com/htmx.org@2.0.4/dist/htmx.min.js) and save to `pkg/web/static/htmx.min.js`. 4. Create `pkg/web/static/style.css` with minimal custom styles (body font, table styling, card class). The layout will load Tailwind v4 from CDN (`https://cdn.tailwindcss.com`) per the CONTEXT.md deferred decision. The local style.css is for overrides only. 5. Create `pkg/web/templates/layout.html` — html/template (NOT templ, per deferred decision): - DOCTYPE, html, head with Tailwind CDN link, htmx.min.js script tag (served from /static/htmx.min.js), local style.css link - Navigation bar: KeyHunter brand, links to Overview (/), Keys (/keys), Providers (/providers), Recon (/recon), Dorks (/dorks), Settings (/settings) - `{{block "content" .}}{{end}}` placeholder for page content - Use `{{define "layout"}}...{{end}}` wrapping pattern so pages extend it 6. Create `pkg/web/templates/overview.html` extending layout: - `{{template "layout" .}}` with `{{define "content"}}` block - Four stat cards in a Tailwind grid (lg:grid-cols-4, sm:grid-cols-2): Total Keys, Providers Loaded, Recon Sources, Last Scan - Recent findings table showing last 10 keys (masked): Provider, Masked Key, Source, Confidence, Date - Data struct: `OverviewData{TotalKeys int, TotalProviders int, ReconSources int, LastScan string, RecentFindings []storage.Finding}` cd /home/salva/Documents/apikey && go build ./pkg/web/... pkg/web/embed.go compiles with go:embed directives, htmx.min.js is vendored, layout.html and overview.html parse without errors, chi v5 is in go.mod Task 2: Server struct, auth middleware, overview handler, and tests pkg/web/server.go, pkg/web/auth.go, pkg/web/handlers.go, pkg/web/server_test.go - Test: GET / returns 200 with "KeyHunter" in body (overview page renders) - Test: GET /static/htmx.min.js returns 200 with JS content - Test: GET / with auth enabled but no credentials returns 401 - Test: GET / with correct basic auth returns 200 - Test: GET / with correct bearer token returns 200 - Test: Overview page shows provider count and key count from injected data 1. Create `pkg/web/server.go`: - `type Config struct { DB *storage.DB; EncKey []byte; Providers *providers.Registry; Dorks *dorks.Registry; ReconEngine *recon.Engine; Port int; AuthUser string; AuthPass string; AuthToken string }` — all fields the server needs - `type Server struct { router chi.Router; cfg Config; tmpl *template.Template }` - `func NewServer(cfg Config) (*Server, error)` — parses all templates from templateFiles embed.FS, builds chi.Router - Router setup: `chi.NewRouter()`, use `middleware.Logger`, `middleware.Recoverer`, `middleware.RealIP` - If AuthUser or AuthToken is set, apply AuthMiddleware (from auth.go) - Mount `/static/` serving from staticFiles embed.FS (use `http.StripPrefix` + `http.FileServer(http.FS(...))`) - Register routes: `GET /` -> handleOverview - `func (s *Server) ListenAndServe() error` — starts `http.Server` on `cfg.Port` - `func (s *Server) Router() chi.Router` — expose for testing 2. Create `pkg/web/auth.go`: - `func AuthMiddleware(user, pass, token string) func(http.Handler) http.Handler` - Check Authorization header: if "Bearer " matches configured token, pass through - If "Basic " matches user:pass, pass through - Otherwise return 401 with `WWW-Authenticate: Basic realm="keyhunter"` header - If all auth fields are empty strings, middleware is a no-op passthrough 3. Create `pkg/web/handlers.go`: - `type OverviewData struct { TotalKeys, TotalProviders, ReconSources int; LastScan string; RecentFindings []storage.Finding; PageTitle string }` - `func (s *Server) handleOverview(w http.ResponseWriter, r *http.Request)` - Query: count findings via `len(db.ListFindingsFiltered(encKey, Filters{Limit: 10}))` for recent, run a COUNT query on the SQL for total - Provider count from `s.cfg.Providers.Stats().Total` (or `len(s.cfg.Providers.List())`) - Recon sources from `len(s.cfg.ReconEngine.List())` - Render overview template with OverviewData 4. Create `pkg/web/server_test.go`: - Use `httptest.NewRecorder` + `httptest.NewRequest` against `s.Router()` - Test overview returns 200 with "KeyHunter" in body - Test static asset serving - Test auth middleware (401 without creds, 200 with basic auth, 200 with bearer token) - For DB-dependent tests, use in-memory SQLite (`storage.Open(":memory:")`) or skip DB and test the router/auth independently with a nil-safe overview (show zeroes when DB is nil) cd /home/salva/Documents/apikey && go test ./pkg/web/... -v -count=1 Server starts with chi router, static assets served via go:embed, overview page renders with stats, auth middleware blocks unauthenticated requests when configured, all tests pass - `go build ./pkg/web/...` compiles without errors - `go test ./pkg/web/... -v` — all tests pass - `go vet ./pkg/web/...` — no issues - chi v5.2.5 in go.mod - pkg/web/server.go exports Server, NewServer, Config - GET / returns overview HTML with stat cards - GET /static/htmx.min.js returns vendored htmx - Auth middleware returns 401 when credentials missing (when auth configured) - Auth middleware passes with valid basic auth or bearer token After completion, create `.planning/phases/18-web-dashboard/18-01-SUMMARY.md`