diff --git a/bin/mirage b/bin/mirage index 9abab60..b16a468 100755 Binary files a/bin/mirage and b/bin/mirage differ diff --git a/cmd/mirage/main.go b/cmd/mirage/main.go index 504b6df..62e973f 100644 --- a/cmd/mirage/main.go +++ b/cmd/mirage/main.go @@ -3,46 +3,55 @@ package main import ( "encoding/json" "fmt" - "log" "net/http" "os" "strings" "mirage/internal/config" + "mirage/internal/logger" "mirage/internal/proxy" "mirage/internal/recorder" "mirage/internal/ui" + "mirage/internal/updater" + "github.com/pkg/browser" "github.com/spf13/cobra" ) +const version = "0.1.0" + func main() { var port int var configPath string + var noBrowser bool var rootCmd = &cobra.Command{ - Use: "mirage", - Short: "Mirage is an API mocking gateway", - Long: `Mirage intercepts HTTP requests and allows mocking responses, recording traffic, and simulating network conditions.`, + Use: "mirage", + Short: "Mirage is an API mocking gateway", + Long: `Mirage intercepts HTTP requests and allows mocking responses, recording traffic, and simulating network conditions.`, + Version: version, } var startCmd = &cobra.Command{ Use: "start", Short: "Start the proxy server", Run: func(cmd *cobra.Command, args []string) { + logger.PrintBanner(version) + addr := fmt.Sprintf(":%d", port) - fmt.Printf("Starting Mirage proxy on %s...\n", addr) + dashboardURL := fmt.Sprintf("http://localhost:%d/__mirage/", port) var cfg *config.Config if configPath != "" { var err error cfg, err = config.LoadConfig(configPath) if err != nil { - log.Fatalf("Failed to load config: %v", err) + logger.LogError(fmt.Sprintf("Failed to load config: %v", err)) + os.Exit(1) } - fmt.Printf("Loaded configuration from %s (%d scenarios)\n", configPath, len(cfg.Scenarios)) + logger.LogSuccess(fmt.Sprintf("Loaded %d scenarios from %s", len(cfg.Scenarios), configPath)) } else { - fmt.Println("No config file specified (-c), running in pure proxy mode") + logger.LogInfo("No config specified, running in pure proxy mode") } p := proxy.NewProxy(cfg, nil) @@ -58,8 +67,17 @@ func main() { } }) + logger.LogSuccess(fmt.Sprintf("Server started on %s", addr)) + logger.LogInfo(fmt.Sprintf("Dashboard: %s", dashboardURL)) + fmt.Println() + + if !noBrowser { + go browser.OpenURL(dashboardURL) + } + if err := http.ListenAndServe(addr, handler); err != nil { - log.Fatalf("Server failed: %v", err) + logger.LogError(fmt.Sprintf("Server failed: %v", err)) + os.Exit(1) } }, } @@ -69,14 +87,20 @@ func main() { Use: "record", Short: "Start proxy in recording mode", Run: func(cmd *cobra.Command, args []string) { + logger.PrintBanner(version) + addr := fmt.Sprintf(":%d", port) - fmt.Printf("Starting Mirage recorder on %s, saving to %s...\n", addr, outputFile) rec := recorder.NewRecorder(outputFile) p := proxy.NewProxy(nil, rec) + logger.LogSuccess(fmt.Sprintf("Recording started on %s", addr)) + logger.LogInfo(fmt.Sprintf("Saving to %s", outputFile)) + fmt.Println() + if err := http.ListenAndServe(addr, p); err != nil { - log.Fatalf("Server failed: %v", err) + logger.LogError(fmt.Sprintf("Server failed: %v", err)) + os.Exit(1) } }, } @@ -86,6 +110,7 @@ func main() { startCmd.Flags().IntVarP(&port, "port", "p", 8080, "Port to run the proxy on") startCmd.Flags().StringVarP(&configPath, "config", "c", "", "Path to scenarios config file") + startCmd.Flags().BoolVar(&noBrowser, "no-browser", false, "Don't open browser automatically") var scenariosCmd = &cobra.Command{ Use: "scenarios", @@ -99,11 +124,12 @@ func main() { Run: func(cmd *cobra.Command, args []string) { cfg, err := config.LoadConfig(args[0]) if err != nil { - log.Fatalf("Failed to load config: %v", err) + logger.LogError(fmt.Sprintf("Failed to load config: %v", err)) + os.Exit(1) } - fmt.Printf("Scenarios in %s:\n", args[0]) + logger.LogSuccess(fmt.Sprintf("Scenarios in %s:", args[0])) for _, s := range cfg.Scenarios { - fmt.Printf("- %s (Matches: %s %s)\n", s.Name, s.Match.Method, s.Match.Path) + fmt.Printf(" • %s (%s %s)\n", s.Name, s.Match.Method, s.Match.Path) } }, } @@ -116,16 +142,18 @@ func main() { Run: func(cmd *cobra.Command, args []string) { data, err := os.ReadFile(args[0]) if err != nil { - log.Fatalf("Failed to read file: %v", err) + logger.LogError(fmt.Sprintf("Failed to read file: %v", err)) + os.Exit(1) } var interactions []recorder.Interaction if err := json.Unmarshal(data, &interactions); err != nil { - log.Fatalf("Failed to parse JSON: %v", err) + logger.LogError(fmt.Sprintf("Failed to parse JSON: %v", err)) + os.Exit(1) } client := &http.Client{} - fmt.Printf("Replaying %d interactions...\n", len(interactions)) + logger.LogInfo(fmt.Sprintf("Replaying %d interactions...", len(interactions))) for i, interaction := range interactions { reqData := interaction.Request @@ -133,7 +161,7 @@ func main() { req, err := http.NewRequest(reqData.Method, reqData.URL, strings.NewReader(reqData.Body)) if err != nil { - fmt.Printf("Failed to create request: %v\n", err) + logger.LogError(fmt.Sprintf("Failed to create request: %v", err)) continue } @@ -145,11 +173,24 @@ func main() { resp, err := client.Do(req) if err != nil { - fmt.Printf("Error: %v\n", err) + logger.LogError(err.Error()) continue } resp.Body.Close() - fmt.Printf("Status: %d\n", resp.StatusCode) + logger.LogSuccess(fmt.Sprintf("Status: %d", resp.StatusCode)) + } + }, + } + + var updateCmd = &cobra.Command{ + Use: "update", + Short: "Update mirage to the latest version", + Run: func(cmd *cobra.Command, args []string) { + logger.PrintBanner(version) + + if err := updater.Update(version); err != nil { + logger.LogError(err.Error()) + os.Exit(1) } }, } @@ -158,9 +199,10 @@ func main() { rootCmd.AddCommand(recordCmd) rootCmd.AddCommand(scenariosCmd) rootCmd.AddCommand(replayCmd) + rootCmd.AddCommand(updateCmd) if err := rootCmd.Execute(); err != nil { - fmt.Println(err) + logger.LogError(err.Error()) os.Exit(1) } } diff --git a/go.mod b/go.mod index 60ec076..d9a6488 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,22 @@ go 1.24.2 require github.com/spf13/cobra v1.10.2 require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/lipgloss v1.1.0 // indirect + github.com/charmbracelet/x/ansi v0.8.0 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/spf13/pflag v1.0.10 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/sys v0.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 5dbad7b..8fb31b4 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,46 @@ +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= +github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..20896f1 --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,166 @@ +package logger + +import ( + "fmt" + "time" + + "github.com/charmbracelet/lipgloss" +) + +var ( + titleStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#00d4ff")). + PaddingLeft(1). + PaddingRight(1) + + subtitleStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#888888")). + Italic(true) + + methodStyleGET = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#10b981")). + PaddingLeft(1). + PaddingRight(1) + + methodStylePOST = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#3b82f6")). + PaddingLeft(1). + PaddingRight(1) + + methodStylePUT = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#f59e0b")). + PaddingLeft(1). + PaddingRight(1) + + methodStyleDELETE = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#ef4444")). + PaddingLeft(1). + PaddingRight(1) + + methodStyleDefault = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#7c3aed")). + PaddingLeft(1). + PaddingRight(1) + + statusStyleOK = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#10b981")) + + statusStyleWarn = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#f59e0b")) + + statusStyleError = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#ef4444")) + + mockStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#00d4ff")). + PaddingLeft(1) + + urlStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#a0aec0")) + + durationStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#888888")). + Italic(true) +) + +func PrintBanner(version string) { + banner := lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#00d4ff")). + BorderStyle(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("#00d4ff")). + Padding(0, 2). + Align(lipgloss.Center). + Width(50) + + title := fmt.Sprintf("MIRAGE %s", version) + subtitle := "API Mocking Gateway & Recorder" + + fmt.Println() + fmt.Println(banner.Render(title)) + fmt.Println(lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")). + Align(lipgloss.Center).Width(50).Render(subtitle)) + fmt.Println() +} + +func LogRequest(method, url, body string) { + methodStyled := getMethodStyle(method).Render(method) + urlStyled := urlStyle.Render(url) + + timestamp := time.Now().Format("15:04:05") + fmt.Printf("%s %s %s\n", + lipgloss.NewStyle().Foreground(lipgloss.Color("#666666")).Render(timestamp), + methodStyled, + urlStyled) + + if body != "" && len(body) < 200 { + fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#444444")).Render("→ "+body)) + } +} + +func LogResponse(status int, duration time.Duration, body string) { + statusStyled := getStatusStyle(status).Render(fmt.Sprintf("%d", status)) + durationStyled := durationStyle.Render(duration.String()) + + fmt.Printf(" %s %s\n", statusStyled, durationStyled) + + if body != "" && len(body) < 200 { + fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#444444")).Render("← "+body)) + } +} + +func LogMock(scenarioName string, status int, duration time.Duration) { + mockStyled := mockStyle.Render("MOCK") + scenarioStyled := lipgloss.NewStyle().Foreground(lipgloss.Color("#00d4ff")).Render(scenarioName) + statusStyled := getStatusStyle(status).Render(fmt.Sprintf("%d", status)) + durationStyled := durationStyle.Render(duration.String()) + + fmt.Printf(" %s %s %s %s\n", mockStyled, scenarioStyled, statusStyled, durationStyled) +} + +func LogInfo(message string) { + fmt.Println(lipgloss.NewStyle().Foreground(lipgloss.Color("#00d4ff")).Render("ℹ " + message)) +} + +func LogSuccess(message string) { + fmt.Println(lipgloss.NewStyle().Foreground(lipgloss.Color("#10b981")).Render("✓ " + message)) +} + +func LogError(message string) { + fmt.Println(lipgloss.NewStyle().Foreground(lipgloss.Color("#ef4444")).Render("✗ " + message)) +} + +func getMethodStyle(method string) lipgloss.Style { + switch method { + case "GET": + return methodStyleGET + case "POST": + return methodStylePOST + case "PUT", "PATCH": + return methodStylePUT + case "DELETE": + return methodStyleDELETE + default: + return methodStyleDefault + } +} + +func getStatusStyle(status int) lipgloss.Style { + if status >= 200 && status < 300 { + return statusStyleOK + } else if status >= 300 && status < 400 { + return statusStyleWarn + } else { + return statusStyleError + } +} diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go index 22f9b3f..71eed5f 100644 --- a/internal/proxy/proxy.go +++ b/internal/proxy/proxy.go @@ -3,12 +3,12 @@ package proxy import ( "bytes" "io" - "log" "net/http" "sync" "time" "mirage/internal/config" + "mirage/internal/logger" "mirage/internal/recorder" "mirage/internal/scenario" ) @@ -63,26 +63,25 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { logReqBody := string(reqBody) if len(logReqBody) > 500 { - logReqBody = logReqBody[:500] + "...(truncated)" + logReqBody = logReqBody[:500] + "..." } - log.Printf("[REQ] %s %s Headers: %v Body: %s", r.Method, r.URL.String(), r.Header, logReqBody) + logger.LogRequest(r.Method, r.URL.String(), logReqBody) var matchedScenario string var status int if p.matcher != nil { if s := p.matcher.Match(r); s != nil { - log.Printf("[MOCK] Matched scenario: %s", s.Name) scenario.ServeMock(w, s) duration := time.Since(start) - log.Printf("[RES] [MOCK] Status: %d Duration: %v", s.Response.Status, duration) matchedScenario = s.Name status = s.Response.Status if status == 0 { status = 200 } + logger.LogMock(s.Name, status, duration) p.logRequest(r, status, duration, matchedScenario) return } @@ -94,7 +93,7 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { resp, err := p.client.Do(outReq) if err != nil { - log.Printf("[ERR] Forwarding failed: %v", err) + logger.LogError("Forwarding failed: " + err.Error()) http.Error(w, "Error forwarding request: "+err.Error(), http.StatusBadGateway) p.logRequest(r, 502, time.Since(start), "") return @@ -109,7 +108,7 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { respBody, err := io.ReadAll(resp.Body) if err != nil { - log.Printf("[ERR] Reading response body: %v", err) + logger.LogError("Reading response body: " + err.Error()) return } @@ -118,10 +117,10 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { duration := time.Since(start) logRespBody := string(respBody) if len(logRespBody) > 500 { - logRespBody = logRespBody[:500] + "...(truncated)" + logRespBody = logRespBody[:500] + "..." } - log.Printf("[RES] Status: %d Duration: %v Body: %s", resp.StatusCode, duration, logRespBody) + logger.LogResponse(resp.StatusCode, duration, logRespBody) if p.recorder != nil { p.recorder.Record(r, string(reqBody), resp, string(respBody), duration) diff --git a/internal/ui/dashboard.html b/internal/ui/dashboard.html index 9c03a44..7a116ca 100644 --- a/internal/ui/dashboard.html +++ b/internal/ui/dashboard.html @@ -4,7 +4,7 @@
-API Mocking Gateway & Traffic Recorder
+