From 98338ebdc6160b177406cb7160a260bf01afc220 Mon Sep 17 00:00:00 2001 From: David Binney Date: Mon, 10 Aug 2026 23:35:19 +1000 Subject: [PATCH 1/5] feat: dark mode for Swagger UI by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gin-swagger has no theme switch — inject a dark CSS stylesheet into index.html. Opt out with ?theme=light. --- README.md | 3 + routes/routes.go | 90 ++++++++++-- routes/swagger-dark.css | 293 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 374 insertions(+), 12 deletions(-) create mode 100644 routes/swagger-dark.css diff --git a/README.md b/README.md index 4721d43..542b6eb 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,9 @@ is `200`, or check `podman logs cu-api | grep '"token"'`. **Host / port:** UI uses the host you opened. Override with `?host=127.0.0.1:18080&scheme=http` if needed. +**Theme:** dark by default (Swagger UI has no real dark mode built-in; we inject CSS). +Stock bright UI: `/api-docs/index.html?theme=light`. + --- ## Quick start diff --git a/routes/routes.go b/routes/routes.go index 93f984f..25b09a6 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -1,13 +1,16 @@ package routes import ( - "github.com/donkeyx/cluster-utils-api/docs" - "github.com/donkeyx/cluster-utils-api/handlers" - "github.com/donkeyx/cluster-utils-api/middleware" + "bytes" + _ "embed" "net/http" "strings" "sync" + "github.com/donkeyx/cluster-utils-api/docs" + "github.com/donkeyx/cluster-utils-api/handlers" + "github.com/donkeyx/cluster-utils-api/middleware" + "github.com/gin-gonic/gin" "go.uber.org/zap" @@ -18,6 +21,11 @@ import ( // swaggerInfoMu guards docs.SwaggerInfo Host/Schemes when serving the UI for different origins. var swaggerInfoMu sync.Mutex +// darkCSS is injected into Swagger UI (gin-swagger has no first-class dark mode). +// +//go:embed swagger-dark.css +var darkCSS []byte + func SetupRouter(logger *zap.Logger, st string, r *gin.Engine) { // Middleware (otel / metrics / log / recover) is registered in main before this. @@ -26,8 +34,8 @@ func SetupRouter(logger *zap.Logger, st string, r *gin.Engine) { c.Redirect(http.StatusFound, "/api-docs/index.html") }) - // Swagger UI: persist Authorize token in the browser; host/scheme follow where you opened - // the page (so port-forward / docker / cluster DNS all work). Optional overrides: + // Swagger UI: persist Authorize token; host/scheme follow where you opened the page. + // Dark theme by default; ?theme=light for stock Swagger look. // /api-docs/index.html?host=my-svc:8080&scheme=http r.GET("/api-docs/*any", swaggerHandler()) @@ -36,9 +44,6 @@ func SetupRouter(logger *zap.Logger, st string, r *gin.Engine) { r.GET("/metrics", handlers.PrometheusMetricsHandler()) // Kube-style probes (plus older aliases) - // live = liveness → restart on fail - // ready = readiness → leave Service endpoints on fail - // startup = cold start latch → kube only until first success r.GET("/livez", handlers.LiveHandler) r.GET("/healthz", handlers.HealthzHandler) r.GET("/health", handlers.HealthHandler) @@ -63,22 +68,26 @@ func SetupRouter(logger *zap.Logger, st string, r *gin.Engine) { authGroup.GET("/env", handlers.EnvHandler) authGroup.GET("/control/probes", handlers.GetProbesHandler) authGroup.PUT("/control/probes", handlers.PutProbesHandler) - // open /proxy would be SSRF (scan cluster, hit metadata, etc.) - // Separate handlers so Swagger GET has no body (browsers reject GET+body). authGroup.GET("/proxy", handlers.ProxyGetHandler) authGroup.POST("/proxy", handlers.ProxyPostHandler) } func swaggerHandler() gin.HandlerFunc { - // Empty host in the generated spec would also work; we set Host from the request - // so the Swagger top bar shows a real target and Try it out hits the right place. handler := ginSwagger.WrapHandler( swaggerFiles.Handler, ginSwagger.PersistAuthorization(true), ginSwagger.DefaultModelsExpandDepth(-1), + ginSwagger.DocExpansion("list"), ) return func(c *gin.Context) { + // Serve our dark stylesheet (relative to /api-docs/) + any := c.Param("any") + if any == "/swagger-dark.css" || any == "swagger-dark.css" || strings.HasSuffix(any, "swagger-dark.css") { + c.Data(http.StatusOK, "text/css; charset=utf-8", darkCSS) + return + } + host := strings.TrimSpace(c.Query("host")) if host == "" { host = c.Request.Host @@ -93,12 +102,69 @@ func swaggerHandler() gin.HandlerFunc { } } + light := strings.EqualFold(c.Query("theme"), "light") + // Serialize updates to the global SwaggerInfo used when doc.json is generated. swaggerInfoMu.Lock() docs.SwaggerInfo.Host = host docs.SwaggerInfo.Schemes = []string{scheme} docs.SwaggerInfo.BasePath = "/" + + // Capture HTML for index so we can inject dark CSS (stock swagger is bright white). + if !light && isSwaggerIndex(any) { + buf := &responseCapture{ResponseWriter: c.Writer, body: &bytes.Buffer{}, status: http.StatusOK} + c.Writer = buf + handler(c) + html := buf.body.String() + inject := `` + + `` + if strings.Contains(html, "") { + html = strings.Replace(html, "", inject+"", 1) + } else { + html = inject + html + } + c.Writer = buf.ResponseWriter + // Drop content-length from capture; write fresh body + for k, vv := range buf.Header() { + if strings.EqualFold(k, "Content-Length") { + continue + } + for _, v := range vv { + c.Writer.Header().Add(k, v) + } + } + c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8") + c.Writer.WriteHeader(buf.status) + _, _ = c.Writer.Write([]byte(html)) + swaggerInfoMu.Unlock() + return + } + handler(c) swaggerInfoMu.Unlock() } } + +func isSwaggerIndex(any string) bool { + any = strings.TrimPrefix(any, "/") + return any == "" || any == "index.html" || strings.HasSuffix(any, "/index.html") +} + +// responseCapture buffers the handler response so we can rewrite HTML. +type responseCapture struct { + gin.ResponseWriter + body *bytes.Buffer + status int +} + +func (w *responseCapture) Write(b []byte) (int, error) { + return w.body.Write(b) +} + +func (w *responseCapture) WriteString(s string) (int, error) { + return w.body.WriteString(s) +} + +func (w *responseCapture) WriteHeader(statusCode int) { + w.status = statusCode +} diff --git a/routes/swagger-dark.css b/routes/swagger-dark.css new file mode 100644 index 0000000..017c4d2 --- /dev/null +++ b/routes/swagger-dark.css @@ -0,0 +1,293 @@ +/* + * Dark theme for Swagger UI (injected by this app). + * Swagger UI has no first-class dark mode in gin-swagger; these overrides fix the glare. + */ + +:root { + color-scheme: dark; +} + +html { + background: #0f1115 !important; +} + +body { + background: #0f1115 !important; + color: #e6e8ee !important; + margin: 0; +} + +/* Top bar */ +.swagger-ui .topbar { + background: #161a22 !important; + border-bottom: 1px solid #2a3140; + padding: 10px 0; +} +.swagger-ui .topbar .download-url-wrapper .select-label { + color: #c5cad6 !important; +} +.swagger-ui .topbar .download-url-wrapper input[type="text"] { + background: #0f1115 !important; + border: 1px solid #3a4254 !important; + color: #e6e8ee !important; +} +.swagger-ui .topbar a { + color: #9ecbff !important; +} + +/* Info / description */ +.swagger-ui .info .title { + color: #f2f4f8 !important; +} +.swagger-ui .info .title small.version-stamp { + background: #2d6cdf !important; +} +.swagger-ui .info p, +.swagger-ui .info li, +.swagger-ui .info table, +.swagger-ui .info .base-url, +.swagger-ui .info a { + color: #c5cad6 !important; +} +.swagger-ui .info a { + color: #8ab4ff !important; +} + +/* Scheme container / authorize */ +.swagger-ui .scheme-container { + background: #161a22 !important; + box-shadow: none !important; + border-bottom: 1px solid #2a3140; +} +.swagger-ui .btn.authorize { + background: #1a2332 !important; + border-color: #4c8bf5 !important; + color: #8ab4ff !important; +} +.swagger-ui .btn.authorize svg { + fill: #8ab4ff !important; +} +.swagger-ui .authorization__btn.locked { + opacity: 1; +} + +/* Opblocks */ +.swagger-ui .opblock-tag { + color: #e6e8ee !important; + border-bottom: 1px solid #2a3140 !important; +} +.swagger-ui .opblock-tag small { + color: #9aa3b5 !important; +} +.swagger-ui .opblock { + background: #161a22 !important; + border: 1px solid #2a3140 !important; + box-shadow: none !important; +} +.swagger-ui .opblock .opblock-summary-description, +.swagger-ui .opblock .opblock-summary-path, +.swagger-ui .opblock .opblock-summary-path__deprecated, +.swagger-ui .opblock .opblock-summary-operation-id { + color: #d5dae6 !important; +} +.swagger-ui .opblock .opblock-summary-method { + color: #fff !important; +} +.swagger-ui .opblock.opblock-get { + background: rgba(45, 108, 223, 0.12) !important; + border-color: rgba(45, 108, 223, 0.45) !important; +} +.swagger-ui .opblock.opblock-post { + background: rgba(34, 160, 107, 0.12) !important; + border-color: rgba(34, 160, 107, 0.45) !important; +} +.swagger-ui .opblock.opblock-put { + background: rgba(214, 140, 40, 0.12) !important; + border-color: rgba(214, 140, 40, 0.45) !important; +} +.swagger-ui .opblock.opblock-delete { + background: rgba(200, 62, 62, 0.12) !important; + border-color: rgba(200, 62, 62, 0.45) !important; +} +.swagger-ui .opblock.opblock-patch { + background: rgba(90, 160, 180, 0.12) !important; + border-color: rgba(90, 160, 180, 0.45) !important; +} + +.swagger-ui .opblock-body, +.swagger-ui .opblock-section-header { + background: #12161e !important; + color: #e6e8ee !important; + box-shadow: none !important; + border-color: #2a3140 !important; +} +.swagger-ui .opblock-description-wrapper p, +.swagger-ui .opblock-external-docs-wrapper p, +.swagger-ui .opblock-title_normal p, +.swagger-ui .opblock-section-header h4, +.swagger-ui .opblock-section-header label, +.swagger-ui .parameter__name, +.swagger-ui .parameter__type, +.swagger-ui .parameter__deprecated, +.swagger-ui .parameter__in, +.swagger-ui table thead tr td, +.swagger-ui table thead tr th, +.swagger-ui .response-col_status, +.swagger-ui .response-col_description, +.swagger-ui .response-col_links, +.swagger-ui .tab li, +.swagger-ui .opblock-title, +.swagger-ui label, +.swagger-ui .response-control-media-type__accept-message { + color: #c5cad6 !important; +} + +/* Models */ +.swagger-ui section.models { + border: 1px solid #2a3140 !important; + background: #161a22 !important; +} +.swagger-ui section.models.is-open h4, +.swagger-ui section.models h4 { + color: #e6e8ee !important; + border-bottom: 1px solid #2a3140 !important; + background: #161a22 !important; +} +.swagger-ui .model-box, +.swagger-ui .model, +.swagger-ui .model-title, +.swagger-ui .prop-type, +.swagger-ui .prop-format { + background: transparent !important; + color: #c5cad6 !important; +} +.swagger-ui .model-toggle:after { + background: #9aa3b5 !important; +} + +/* Inputs */ +.swagger-ui input[type="text"], +.swagger-ui input[type="password"], +.swagger-ui input[type="search"], +.swagger-ui input[type="email"], +.swagger-ui input[type="file"], +.swagger-ui textarea, +.swagger-ui select { + background: #0f1115 !important; + color: #e6e8ee !important; + border: 1px solid #3a4254 !important; + outline: none !important; + box-shadow: none !important; +} +.swagger-ui input::placeholder, +.swagger-ui textarea::placeholder { + color: #6b7385 !important; +} + +/* Buttons */ +.swagger-ui .btn { + background: #1a2332 !important; + color: #e6e8ee !important; + border: 1px solid #3a4254 !important; + box-shadow: none !important; +} +.swagger-ui .btn.execute { + background: #2d6cdf !important; + border-color: #2d6cdf !important; + color: #fff !important; +} +.swagger-ui .btn.cancel { + background: transparent !important; + border-color: #6b7385 !important; + color: #c5cad6 !important; +} +.swagger-ui .try-out__btn { + border-color: #4c8bf5 !important; + color: #8ab4ff !important; +} + +/* Response / code blocks */ +.swagger-ui .highlight-code, +.swagger-ui .microlight, +.swagger-ui .responses-inner h4, +.swagger-ui .responses-inner h5, +.swagger-ui .response-col_description__inner div.markdown, +.swagger-ui .markdown code, +.swagger-ui .renderedMarkdown code { + color: #d7dce8 !important; +} +.swagger-ui .highlight-code { + background: #0b0d11 !important; +} +.swagger-ui .responses-wrapper, +.swagger-ui .responses-inner { + background: transparent !important; +} +.swagger-ui .response-col_description__inner div.markdown, +.swagger-ui .response-col_description__inner div.renderedMarkdown, +.swagger-ui .markdown p, +.swagger-ui .renderedMarkdown p { + color: #c5cad6 !important; +} +.swagger-ui table tbody tr td { + color: #c5cad6 !important; + border-color: #2a3140 !important; +} +.swagger-ui .response-control-media-type__title { + color: #9aa3b5 !important; +} + +/* Curl box */ +.swagger-ui .curl-command, +.swagger-ui .curl { + background: #0b0d11 !important; + color: #d7dce8 !important; + border: 1px solid #2a3140 !important; +} + +/* Dialogs (Authorize modal) */ +.swagger-ui .dialog-ux .modal-ux { + background: #161a22 !important; + border: 1px solid #2a3140 !important; + color: #e6e8ee !important; +} +.swagger-ui .dialog-ux .modal-ux-header { + border-bottom: 1px solid #2a3140 !important; +} +.swagger-ui .dialog-ux .modal-ux-header h3, +.swagger-ui .dialog-ux .modal-ux-content h4, +.swagger-ui .dialog-ux .modal-ux-content p, +.swagger-ui .dialog-ux .modal-ux-content label { + color: #e6e8ee !important; +} +.swagger-ui .auth-container { + border-color: #2a3140 !important; +} +.swagger-ui .auth-btn-wrapper .btn-done { + background: #2d6cdf !important; + border-color: #2d6cdf !important; + color: #fff !important; +} + +/* Filter / misc */ +.swagger-ui .filter .operation-filter-input { + background: #0f1115 !important; + border: 1px solid #3a4254 !important; + color: #e6e8ee !important; +} +.swagger-ui .loading-container .loading::after { + border-color: #2d6cdf transparent transparent !important; +} + +/* Scrollbars (webkit) */ +.swagger-ui ::-webkit-scrollbar { + width: 10px; + height: 10px; +} +.swagger-ui ::-webkit-scrollbar-thumb { + background: #3a4254; + border-radius: 6px; +} +.swagger-ui ::-webkit-scrollbar-track { + background: #0f1115; +} From b7ee9ce74d618a2de6a23a8050e05e81850a4155 Mon Sep 17 00:00:00 2001 From: David Binney Date: Mon, 10 Aug 2026 23:37:17 +1000 Subject: [PATCH 2/5] feat: slicker swagger dark skin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restyle stock Swagger UI with a custom dark skin (Outfit + JetBrains Mono, gradient panels, method accents, soft glow). Still CSS-only — no first-class theme in gin-swagger. --- routes/swagger-dark.css | 434 ++++++++++++++++++++++++++++------------ 1 file changed, 309 insertions(+), 125 deletions(-) diff --git a/routes/swagger-dark.css b/routes/swagger-dark.css index 017c4d2..38cf05a 100644 --- a/routes/swagger-dark.css +++ b/routes/swagger-dark.css @@ -1,157 +1,304 @@ /* - * Dark theme for Swagger UI (injected by this app). - * Swagger UI has no first-class dark mode in gin-swagger; these overrides fix the glare. + * Cluster Utils API — Swagger UI skin + * Stock Swagger has no themes; this is a full restyle for dark + a bit of swagger. */ +@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Outfit:wght@400;500;600;700&display=swap"); + :root { color-scheme: dark; + --cu-bg: #07090f; + --cu-bg-elevated: #0e121b; + --cu-bg-panel: #121826; + --cu-bg-soft: #182033; + --cu-border: #243049; + --cu-border-strong: #33415f; + --cu-text: #eef1f8; + --cu-text-dim: #9aa6c0; + --cu-text-mute: #6d7a96; + --cu-accent: #6ea8ff; + --cu-accent-2: #a78bfa; + --cu-accent-hot: #22d3a6; + --cu-danger: #f87171; + --cu-warn: #fbbf24; + --cu-get: #38bdf8; + --cu-post: #34d399; + --cu-put: #fbbf24; + --cu-delete: #f87171; + --cu-patch: #c084fc; + --cu-glow: 0 0 0 1px rgba(110, 168, 255, 0.15), 0 12px 40px rgba(0, 0, 0, 0.45); + --cu-radius: 12px; + --cu-font: "Outfit", "Segoe UI", system-ui, sans-serif; + --cu-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace; } html { - background: #0f1115 !important; + background: var(--cu-bg) !important; } body { - background: #0f1115 !important; - color: #e6e8ee !important; margin: 0; + background: + radial-gradient(1200px 600px at 10% -10%, rgba(110, 168, 255, 0.12), transparent 55%), + radial-gradient(900px 500px at 100% 0%, rgba(167, 139, 250, 0.10), transparent 50%), + radial-gradient(800px 400px at 50% 100%, rgba(34, 211, 166, 0.06), transparent 50%), + var(--cu-bg) !important; + color: var(--cu-text) !important; + font-family: var(--cu-font) !important; + min-height: 100vh; } -/* Top bar */ +/* Hide stock top bar noise a bit — keep brand space clean */ .swagger-ui .topbar { - background: #161a22 !important; - border-bottom: 1px solid #2a3140; - padding: 10px 0; + background: rgba(14, 18, 27, 0.85) !important; + backdrop-filter: blur(12px); + border-bottom: 1px solid var(--cu-border); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.35); + padding: 12px 0; } .swagger-ui .topbar .download-url-wrapper .select-label { - color: #c5cad6 !important; + color: var(--cu-text-dim) !important; + font-family: var(--cu-font) !important; } .swagger-ui .topbar .download-url-wrapper input[type="text"] { - background: #0f1115 !important; - border: 1px solid #3a4254 !important; - color: #e6e8ee !important; + background: var(--cu-bg) !important; + border: 1px solid var(--cu-border-strong) !important; + color: var(--cu-text) !important; + border-radius: 8px !important; + font-family: var(--cu-mono) !important; + font-size: 13px !important; +} +.swagger-ui .topbar-wrapper img, +.swagger-ui .topbar .link span { + display: none !important; /* drop default swagger logo wordmark clutter */ +} +.swagger-ui .topbar .link:after { + content: "🐴 Cluster Utils API"; + color: var(--cu-text) !important; + font-family: var(--cu-font) !important; + font-weight: 700; + font-size: 1.05rem; + letter-spacing: 0.02em; } .swagger-ui .topbar a { - color: #9ecbff !important; + color: var(--cu-accent) !important; +} + +/* Wrapper breathing room */ +.swagger-ui .wrapper { + max-width: 1200px; + padding: 0 20px; } -/* Info / description */ +/* Info block */ +.swagger-ui .info { + margin: 28px 0 20px !important; + padding: 22px 24px !important; + background: linear-gradient(145deg, rgba(24, 32, 51, 0.9), rgba(14, 18, 27, 0.95)) !important; + border: 1px solid var(--cu-border) !important; + border-radius: var(--cu-radius) !important; + box-shadow: var(--cu-glow); +} .swagger-ui .info .title { - color: #f2f4f8 !important; + color: var(--cu-text) !important; + font-family: var(--cu-font) !important; + font-weight: 700 !important; + font-size: 2rem !important; + letter-spacing: -0.02em; } .swagger-ui .info .title small.version-stamp { - background: #2d6cdf !important; + background: linear-gradient(135deg, var(--cu-accent), var(--cu-accent-2)) !important; + border-radius: 999px !important; + padding: 4px 10px !important; + font-family: var(--cu-mono) !important; + font-weight: 600 !important; +} +.swagger-ui .info .title small { + top: -4px !important; } .swagger-ui .info p, .swagger-ui .info li, .swagger-ui .info table, -.swagger-ui .info .base-url, -.swagger-ui .info a { - color: #c5cad6 !important; +.swagger-ui .info .base-url { + color: var(--cu-text-dim) !important; + font-size: 15px !important; + line-height: 1.55 !important; } .swagger-ui .info a { - color: #8ab4ff !important; + color: var(--cu-accent) !important; + text-decoration: none !important; +} +.swagger-ui .info a:hover { + color: #b3d4ff !important; + text-decoration: underline !important; } -/* Scheme container / authorize */ +/* Authorize strip */ .swagger-ui .scheme-container { - background: #161a22 !important; + background: rgba(18, 24, 38, 0.9) !important; box-shadow: none !important; - border-bottom: 1px solid #2a3140; + border: 1px solid var(--cu-border); + border-radius: var(--cu-radius); + margin: 0 0 20px !important; + padding: 14px 18px !important; + backdrop-filter: blur(8px); } .swagger-ui .btn.authorize { - background: #1a2332 !important; - border-color: #4c8bf5 !important; - color: #8ab4ff !important; + background: linear-gradient(135deg, rgba(110, 168, 255, 0.15), rgba(167, 139, 250, 0.15)) !important; + border: 1px solid var(--cu-accent) !important; + color: var(--cu-accent) !important; + border-radius: 10px !important; + font-family: var(--cu-font) !important; + font-weight: 600 !important; + letter-spacing: 0.02em; + transition: transform 0.12s ease, box-shadow 0.12s ease; +} +.swagger-ui .btn.authorize:hover { + transform: translateY(-1px); + box-shadow: 0 6px 20px rgba(110, 168, 255, 0.25); } .swagger-ui .btn.authorize svg { - fill: #8ab4ff !important; + fill: var(--cu-accent) !important; } .swagger-ui .authorization__btn.locked { opacity: 1; } -/* Opblocks */ +/* Tags */ .swagger-ui .opblock-tag { - color: #e6e8ee !important; - border-bottom: 1px solid #2a3140 !important; + color: var(--cu-text) !important; + border-bottom: 1px solid var(--cu-border) !important; + font-family: var(--cu-font) !important; + font-weight: 600 !important; + font-size: 1.15rem !important; + margin: 18px 0 8px !important; + padding: 10px 4px !important; } .swagger-ui .opblock-tag small { - color: #9aa3b5 !important; + color: var(--cu-text-mute) !important; + font-family: var(--cu-mono) !important; + font-size: 12px !important; } + +/* Operation blocks */ .swagger-ui .opblock { - background: #161a22 !important; - border: 1px solid #2a3140 !important; - box-shadow: none !important; + border-radius: 12px !important; + margin: 0 0 12px !important; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25) !important; + border: 1px solid var(--cu-border) !important; + overflow: hidden; + background: var(--cu-bg-panel) !important; } -.swagger-ui .opblock .opblock-summary-description, -.swagger-ui .opblock .opblock-summary-path, -.swagger-ui .opblock .opblock-summary-path__deprecated, -.swagger-ui .opblock .opblock-summary-operation-id { - color: #d5dae6 !important; +.swagger-ui .opblock .opblock-summary { + border: none !important; + padding: 10px 12px !important; } .swagger-ui .opblock .opblock-summary-method { - color: #fff !important; + border-radius: 8px !important; + font-family: var(--cu-mono) !important; + font-weight: 700 !important; + min-width: 72px !important; + text-align: center; + letter-spacing: 0.04em; } +.swagger-ui .opblock .opblock-summary-path, +.swagger-ui .opblock .opblock-summary-path__deprecated { + color: var(--cu-text) !important; + font-family: var(--cu-mono) !important; + font-size: 14px !important; + font-weight: 600 !important; +} +.swagger-ui .opblock .opblock-summary-description { + color: var(--cu-text-dim) !important; + font-family: var(--cu-font) !important; +} + .swagger-ui .opblock.opblock-get { - background: rgba(45, 108, 223, 0.12) !important; - border-color: rgba(45, 108, 223, 0.45) !important; + background: linear-gradient(90deg, rgba(56, 189, 248, 0.10), var(--cu-bg-panel) 40%) !important; + border-color: rgba(56, 189, 248, 0.35) !important; +} +.swagger-ui .opblock.opblock-get .opblock-summary-method { + background: var(--cu-get) !important; } .swagger-ui .opblock.opblock-post { - background: rgba(34, 160, 107, 0.12) !important; - border-color: rgba(34, 160, 107, 0.45) !important; + background: linear-gradient(90deg, rgba(52, 211, 153, 0.10), var(--cu-bg-panel) 40%) !important; + border-color: rgba(52, 211, 153, 0.35) !important; +} +.swagger-ui .opblock.opblock-post .opblock-summary-method { + background: var(--cu-post) !important; + color: #052e1c !important; } .swagger-ui .opblock.opblock-put { - background: rgba(214, 140, 40, 0.12) !important; - border-color: rgba(214, 140, 40, 0.45) !important; + background: linear-gradient(90deg, rgba(251, 191, 36, 0.10), var(--cu-bg-panel) 40%) !important; + border-color: rgba(251, 191, 36, 0.35) !important; +} +.swagger-ui .opblock.opblock-put .opblock-summary-method { + background: var(--cu-put) !important; + color: #422006 !important; } .swagger-ui .opblock.opblock-delete { - background: rgba(200, 62, 62, 0.12) !important; - border-color: rgba(200, 62, 62, 0.45) !important; + background: linear-gradient(90deg, rgba(248, 113, 113, 0.10), var(--cu-bg-panel) 40%) !important; + border-color: rgba(248, 113, 113, 0.35) !important; +} +.swagger-ui .opblock.opblock-delete .opblock-summary-method { + background: var(--cu-delete) !important; } .swagger-ui .opblock.opblock-patch { - background: rgba(90, 160, 180, 0.12) !important; - border-color: rgba(90, 160, 180, 0.45) !important; + background: linear-gradient(90deg, rgba(192, 132, 252, 0.10), var(--cu-bg-panel) 40%) !important; + border-color: rgba(192, 132, 252, 0.35) !important; +} +.swagger-ui .opblock.opblock-patch .opblock-summary-method { + background: var(--cu-patch) !important; + color: #2e1065 !important; } .swagger-ui .opblock-body, .swagger-ui .opblock-section-header { - background: #12161e !important; - color: #e6e8ee !important; + background: var(--cu-bg-elevated) !important; + color: var(--cu-text) !important; box-shadow: none !important; - border-color: #2a3140 !important; + border-color: var(--cu-border) !important; } -.swagger-ui .opblock-description-wrapper p, -.swagger-ui .opblock-external-docs-wrapper p, -.swagger-ui .opblock-title_normal p, .swagger-ui .opblock-section-header h4, .swagger-ui .opblock-section-header label, +.swagger-ui .opblock-description-wrapper p, +.swagger-ui .opblock-title_normal p, .swagger-ui .parameter__name, .swagger-ui .parameter__type, -.swagger-ui .parameter__deprecated, .swagger-ui .parameter__in, +.swagger-ui .parameter__deprecated, .swagger-ui table thead tr td, .swagger-ui table thead tr th, .swagger-ui .response-col_status, .swagger-ui .response-col_description, -.swagger-ui .response-col_links, -.swagger-ui .tab li, +.swagger-ui .tab li button, .swagger-ui .opblock-title, .swagger-ui label, .swagger-ui .response-control-media-type__accept-message { - color: #c5cad6 !important; + color: var(--cu-text-dim) !important; + font-family: var(--cu-font) !important; +} +.swagger-ui .parameter__name { + font-family: var(--cu-mono) !important; + color: var(--cu-text) !important; + font-size: 13px !important; +} +.swagger-ui .parameter__name.required:after { + color: var(--cu-danger) !important; } /* Models */ .swagger-ui section.models { - border: 1px solid #2a3140 !important; - background: #161a22 !important; + border: 1px solid var(--cu-border) !important; + background: var(--cu-bg-panel) !important; + border-radius: var(--cu-radius) !important; + box-shadow: var(--cu-glow); } -.swagger-ui section.models.is-open h4, .swagger-ui section.models h4 { - color: #e6e8ee !important; - border-bottom: 1px solid #2a3140 !important; - background: #161a22 !important; + color: var(--cu-text) !important; + border-bottom: 1px solid var(--cu-border) !important; + background: transparent !important; + font-family: var(--cu-font) !important; } .swagger-ui .model-box, .swagger-ui .model, @@ -159,10 +306,11 @@ body { .swagger-ui .prop-type, .swagger-ui .prop-format { background: transparent !important; - color: #c5cad6 !important; + color: var(--cu-text-dim) !important; + font-family: var(--cu-mono) !important; } -.swagger-ui .model-toggle:after { - background: #9aa3b5 !important; +.swagger-ui .prop-type { + color: var(--cu-accent-hot) !important; } /* Inputs */ @@ -173,121 +321,157 @@ body { .swagger-ui input[type="file"], .swagger-ui textarea, .swagger-ui select { - background: #0f1115 !important; - color: #e6e8ee !important; - border: 1px solid #3a4254 !important; + background: var(--cu-bg) !important; + color: var(--cu-text) !important; + border: 1px solid var(--cu-border-strong) !important; + border-radius: 8px !important; outline: none !important; box-shadow: none !important; + font-family: var(--cu-mono) !important; + font-size: 13px !important; +} +.swagger-ui input:focus, +.swagger-ui textarea:focus, +.swagger-ui select:focus { + border-color: var(--cu-accent) !important; + box-shadow: 0 0 0 3px rgba(110, 168, 255, 0.18) !important; } .swagger-ui input::placeholder, .swagger-ui textarea::placeholder { - color: #6b7385 !important; + color: var(--cu-text-mute) !important; } /* Buttons */ .swagger-ui .btn { - background: #1a2332 !important; - color: #e6e8ee !important; - border: 1px solid #3a4254 !important; + background: var(--cu-bg-soft) !important; + color: var(--cu-text) !important; + border: 1px solid var(--cu-border-strong) !important; + border-radius: 10px !important; box-shadow: none !important; + font-family: var(--cu-font) !important; + font-weight: 600 !important; } .swagger-ui .btn.execute { - background: #2d6cdf !important; - border-color: #2d6cdf !important; + background: linear-gradient(135deg, #3b82f6, #6366f1) !important; + border: none !important; color: #fff !important; + box-shadow: 0 8px 24px rgba(59, 130, 246, 0.35) !important; +} +.swagger-ui .btn.execute:hover { + filter: brightness(1.08); } .swagger-ui .btn.cancel { background: transparent !important; - border-color: #6b7385 !important; - color: #c5cad6 !important; + border-color: var(--cu-text-mute) !important; + color: var(--cu-text-dim) !important; } .swagger-ui .try-out__btn { - border-color: #4c8bf5 !important; - color: #8ab4ff !important; + border-color: var(--cu-accent) !important; + color: var(--cu-accent) !important; + background: transparent !important; } -/* Response / code blocks */ +/* Code / responses */ .swagger-ui .highlight-code, -.swagger-ui .microlight, +.swagger-ui .microlight { + background: #05070c !important; + color: #dbe4ff !important; + border: 1px solid var(--cu-border) !important; + border-radius: 10px !important; + font-family: var(--cu-mono) !important; + font-size: 12.5px !important; + line-height: 1.5 !important; +} .swagger-ui .responses-inner h4, .swagger-ui .responses-inner h5, .swagger-ui .response-col_description__inner div.markdown, +.swagger-ui .markdown p, +.swagger-ui .renderedMarkdown p, .swagger-ui .markdown code, .swagger-ui .renderedMarkdown code { - color: #d7dce8 !important; -} -.swagger-ui .highlight-code { - background: #0b0d11 !important; -} -.swagger-ui .responses-wrapper, -.swagger-ui .responses-inner { - background: transparent !important; -} -.swagger-ui .response-col_description__inner div.markdown, -.swagger-ui .response-col_description__inner div.renderedMarkdown, -.swagger-ui .markdown p, -.swagger-ui .renderedMarkdown p { - color: #c5cad6 !important; + color: var(--cu-text-dim) !important; } .swagger-ui table tbody tr td { - color: #c5cad6 !important; - border-color: #2a3140 !important; + color: var(--cu-text-dim) !important; + border-color: var(--cu-border) !important; } .swagger-ui .response-control-media-type__title { - color: #9aa3b5 !important; + color: var(--cu-text-mute) !important; } -/* Curl box */ +/* Curl */ .swagger-ui .curl-command, .swagger-ui .curl { - background: #0b0d11 !important; - color: #d7dce8 !important; - border: 1px solid #2a3140 !important; + background: #05070c !important; + color: #dbe4ff !important; + border: 1px solid var(--cu-border) !important; + border-radius: 10px !important; + font-family: var(--cu-mono) !important; } -/* Dialogs (Authorize modal) */ +/* Copy buttons etc */ +.swagger-ui .copy-to-clipboard { + background: var(--cu-bg-soft) !important; +} + +/* Authorize modal */ .swagger-ui .dialog-ux .modal-ux { - background: #161a22 !important; - border: 1px solid #2a3140 !important; - color: #e6e8ee !important; + background: var(--cu-bg-panel) !important; + border: 1px solid var(--cu-border) !important; + border-radius: 16px !important; + color: var(--cu-text) !important; + box-shadow: 0 24px 80px rgba(0, 0, 0, 0.65) !important; } .swagger-ui .dialog-ux .modal-ux-header { - border-bottom: 1px solid #2a3140 !important; + border-bottom: 1px solid var(--cu-border) !important; } .swagger-ui .dialog-ux .modal-ux-header h3, .swagger-ui .dialog-ux .modal-ux-content h4, .swagger-ui .dialog-ux .modal-ux-content p, .swagger-ui .dialog-ux .modal-ux-content label { - color: #e6e8ee !important; + color: var(--cu-text) !important; + font-family: var(--cu-font) !important; } .swagger-ui .auth-container { - border-color: #2a3140 !important; + border-color: var(--cu-border) !important; + border-radius: 10px !important; } -.swagger-ui .auth-btn-wrapper .btn-done { - background: #2d6cdf !important; - border-color: #2d6cdf !important; +.swagger-ui .auth-btn-wrapper .btn-done, +.swagger-ui .btn.modal-btn.auth.authorize { + background: linear-gradient(135deg, #3b82f6, #6366f1) !important; + border: none !important; color: #fff !important; } -/* Filter / misc */ +/* Filter */ .swagger-ui .filter .operation-filter-input { - background: #0f1115 !important; - border: 1px solid #3a4254 !important; - color: #e6e8ee !important; + background: var(--cu-bg) !important; + border: 1px solid var(--cu-border-strong) !important; + color: var(--cu-text) !important; + border-radius: 10px !important; + font-family: var(--cu-mono) !important; } + +/* Loading spinner */ .swagger-ui .loading-container .loading::after { - border-color: #2d6cdf transparent transparent !important; + border-color: var(--cu-accent) transparent transparent !important; } -/* Scrollbars (webkit) */ +/* Scrollbars */ .swagger-ui ::-webkit-scrollbar { width: 10px; height: 10px; } .swagger-ui ::-webkit-scrollbar-thumb { - background: #3a4254; - border-radius: 6px; + background: linear-gradient(180deg, #3a4a6a, #2a354d); + border-radius: 8px; } .swagger-ui ::-webkit-scrollbar-track { - background: #0f1115; + background: var(--cu-bg); +} + +/* Markdown lists in description */ +.swagger-ui .markdown li, +.swagger-ui .renderedMarkdown li { + color: var(--cu-text-dim) !important; } From 3f4f4d4777168751aaf58d0a3d42795ca62b713c Mon Sep 17 00:00:00 2001 From: David Binney Date: Mon, 10 Aug 2026 23:41:41 +1000 Subject: [PATCH 3/5] feat: donkeyx / terminal flair on swagger skin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shell-prompt topbar, phosphor code panels, scan grid, session banner, and corner tag — still CSS-only on top of stock Swagger UI. --- routes/swagger-dark.css | 376 +++++++++++++++++++++++++++++++++------- 1 file changed, 313 insertions(+), 63 deletions(-) diff --git a/routes/swagger-dark.css b/routes/swagger-dark.css index 38cf05a..fda8d6b 100644 --- a/routes/swagger-dark.css +++ b/routes/swagger-dark.css @@ -1,9 +1,10 @@ /* * Cluster Utils API — Swagger UI skin - * Stock Swagger has no themes; this is a full restyle for dark + a bit of swagger. + * Stock Swagger has no themes; this is a full restyle with a little + * donkeyx / terminal flair (CSS-only). */ -@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Outfit:wght@400;500;600;700&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Outfit:wght@400;500;600;700&display=swap"); :root { color-scheme: dark; @@ -18,7 +19,7 @@ --cu-text-mute: #6d7a96; --cu-accent: #6ea8ff; --cu-accent-2: #a78bfa; - --cu-accent-hot: #22d3a6; + --cu-accent-hot: #22d3a6; /* terminal phosphor */ --cu-danger: #f87171; --cu-warn: #fbbf24; --cu-get: #38bdf8; @@ -27,61 +28,179 @@ --cu-delete: #f87171; --cu-patch: #c084fc; --cu-glow: 0 0 0 1px rgba(110, 168, 255, 0.15), 0 12px 40px rgba(0, 0, 0, 0.45); + --cu-glow-hot: 0 0 0 1px rgba(34, 211, 166, 0.2), 0 0 28px rgba(34, 211, 166, 0.08); --cu-radius: 12px; --cu-font: "Outfit", "Segoe UI", system-ui, sans-serif; --cu-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace; } +@keyframes cu-blink { + 0%, 49% { opacity: 1; } + 50%, 100% { opacity: 0; } +} + +@keyframes cu-scan { + 0% { transform: translateY(-100%); } + 100% { transform: translateY(100vh); } +} + +@keyframes cu-glow-pulse { + 0%, 100% { box-shadow: 0 0 0 1px rgba(34, 211, 166, 0.15), 0 8px 32px rgba(0, 0, 0, 0.35); } + 50% { box-shadow: 0 0 0 1px rgba(34, 211, 166, 0.35), 0 8px 36px rgba(34, 211, 166, 0.12); } +} + +::selection { + background: rgba(34, 211, 166, 0.28); + color: #f0fff8; +} + html { background: var(--cu-bg) !important; } body { margin: 0; - background: + background-color: var(--cu-bg) !important; + background-image: + /* faint circuit grid */ + linear-gradient(rgba(110, 168, 255, 0.035) 1px, transparent 1px), + linear-gradient(90deg, rgba(110, 168, 255, 0.035) 1px, transparent 1px), + /* color washes */ radial-gradient(1200px 600px at 10% -10%, rgba(110, 168, 255, 0.12), transparent 55%), radial-gradient(900px 500px at 100% 0%, rgba(167, 139, 250, 0.10), transparent 50%), - radial-gradient(800px 400px at 50% 100%, rgba(34, 211, 166, 0.06), transparent 50%), - var(--cu-bg) !important; + radial-gradient(800px 400px at 50% 100%, rgba(34, 211, 166, 0.07), transparent 50%) !important; + background-size: + 48px 48px, + 48px 48px, + auto, + auto, + auto !important; color: var(--cu-text) !important; font-family: var(--cu-font) !important; min-height: 100vh; } -/* Hide stock top bar noise a bit — keep brand space clean */ +/* CRT scanlines + soft vignette — pointer-events none so UI stays usable */ +body::before { + content: ""; + pointer-events: none; + position: fixed; + inset: 0; + z-index: 9998; + background: + repeating-linear-gradient( + 0deg, + transparent, + transparent 2px, + rgba(0, 0, 0, 0.045) 2px, + rgba(0, 0, 0, 0.045) 4px + ), + radial-gradient(ellipse at center, transparent 55%, rgba(0, 0, 0, 0.35) 100%); + opacity: 0.7; +} + +/* donkeyx corner tag */ +body::after { + content: "// donkeyx · 0xCU · poke with care"; + pointer-events: none; + position: fixed; + bottom: 14px; + right: 18px; + z-index: 9999; + font-family: var(--cu-mono); + font-size: 11px; + font-weight: 600; + letter-spacing: 0.06em; + color: var(--cu-accent-hot); + opacity: 0.55; + text-shadow: 0 0 12px rgba(34, 211, 166, 0.35); +} + +/* ── Top bar ─────────────────────────────────────────────── */ .swagger-ui .topbar { - background: rgba(14, 18, 27, 0.85) !important; - backdrop-filter: blur(12px); + background: rgba(10, 14, 22, 0.92) !important; + backdrop-filter: blur(14px); border-bottom: 1px solid var(--cu-border); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.35); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); padding: 12px 0; + position: relative; + animation: cu-glow-pulse 6s ease-in-out infinite; +} +/* thin phosphor scan line under topbar */ +.swagger-ui .topbar::after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: -1px; + height: 1px; + background: linear-gradient( + 90deg, + transparent 0%, + var(--cu-accent-hot) 20%, + var(--cu-accent) 50%, + var(--cu-accent-2) 80%, + transparent 100% + ); + opacity: 0.7; } .swagger-ui .topbar .download-url-wrapper .select-label { color: var(--cu-text-dim) !important; - font-family: var(--cu-font) !important; + font-family: var(--cu-mono) !important; + font-size: 12px !important; + text-transform: lowercase; + letter-spacing: 0.04em; +} +.swagger-ui .topbar .download-url-wrapper .select-label span:before { + content: "$ "; + color: var(--cu-accent-hot); } .swagger-ui .topbar .download-url-wrapper input[type="text"] { background: var(--cu-bg) !important; border: 1px solid var(--cu-border-strong) !important; - color: var(--cu-text) !important; + color: var(--cu-accent-hot) !important; border-radius: 8px !important; font-family: var(--cu-mono) !important; font-size: 13px !important; + box-shadow: inset 0 0 0 1px rgba(34, 211, 166, 0.06) !important; } .swagger-ui .topbar-wrapper img, .swagger-ui .topbar .link span { - display: none !important; /* drop default swagger logo wordmark clutter */ + display: none !important; } +/* Brand: donkeyx shell prompt */ .swagger-ui .topbar .link:after { - content: "🐴 Cluster Utils API"; + content: "🐴 donkeyx@cluster-utils:~$"; color: var(--cu-text) !important; - font-family: var(--cu-font) !important; + font-family: var(--cu-mono) !important; font-weight: 700; - font-size: 1.05rem; - letter-spacing: 0.02em; + font-size: 0.95rem; + letter-spacing: 0.01em; + text-shadow: 0 0 18px rgba(34, 211, 166, 0.25); } .swagger-ui .topbar a { - color: var(--cu-accent) !important; + color: var(--cu-accent-hot) !important; +} +/* tiny ops badge on the right of topbar wrapper */ +.swagger-ui .topbar .topbar-wrapper::after { + content: "TRACE · PING · PROXY"; + margin-left: auto; + font-family: var(--cu-mono); + font-size: 10px; + font-weight: 700; + letter-spacing: 0.14em; + color: var(--cu-text-mute); + border: 1px solid var(--cu-border); + border-radius: 999px; + padding: 4px 10px; + background: rgba(34, 211, 166, 0.06); + color: var(--cu-accent-hot); + opacity: 0.85; +} +.swagger-ui .topbar .topbar-wrapper { + display: flex !important; + align-items: center; + gap: 16px; } /* Wrapper breathing room */ @@ -90,14 +209,33 @@ body { padding: 0 20px; } -/* Info block */ +/* ── Info / hero ─────────────────────────────────────────── */ .swagger-ui .info { margin: 28px 0 20px !important; - padding: 22px 24px !important; - background: linear-gradient(145deg, rgba(24, 32, 51, 0.9), rgba(14, 18, 27, 0.95)) !important; + padding: 22px 24px 22px 28px !important; + background: + linear-gradient(145deg, rgba(24, 32, 51, 0.95), rgba(10, 14, 22, 0.98)) !important; border: 1px solid var(--cu-border) !important; + border-left: 3px solid var(--cu-accent-hot) !important; border-radius: var(--cu-radius) !important; - box-shadow: var(--cu-glow); + box-shadow: var(--cu-glow), var(--cu-glow-hot); + position: relative; + overflow: hidden; +} +/* terminal session banner */ +.swagger-ui .info::before { + content: "$> session opened · github.com/donkeyx · /a/* needs Bearer AUTH_TOKEN"; + display: block; + font-family: var(--cu-mono); + font-size: 11.5px; + font-weight: 600; + color: var(--cu-accent-hot); + letter-spacing: 0.02em; + margin-bottom: 14px; + padding-bottom: 10px; + border-bottom: 1px dashed rgba(34, 211, 166, 0.25); + text-shadow: 0 0 10px rgba(34, 211, 166, 0.2); + opacity: 0.9; } .swagger-ui .info .title { color: var(--cu-text) !important; @@ -105,17 +243,31 @@ body { font-weight: 700 !important; font-size: 2rem !important; letter-spacing: -0.02em; + text-shadow: 0 0 40px rgba(110, 168, 255, 0.2); } +/* blinking terminal caret after title */ .swagger-ui .info .title small.version-stamp { - background: linear-gradient(135deg, var(--cu-accent), var(--cu-accent-2)) !important; + background: linear-gradient(135deg, var(--cu-accent-hot), var(--cu-accent)) !important; border-radius: 999px !important; padding: 4px 10px !important; font-family: var(--cu-mono) !important; - font-weight: 600 !important; + font-weight: 700 !important; + color: #04120e !important; + box-shadow: 0 0 16px rgba(34, 211, 166, 0.35); } .swagger-ui .info .title small { top: -4px !important; } +.swagger-ui .info .title::after { + content: "▌"; + color: var(--cu-accent-hot); + font-weight: 400; + margin-left: 4px; + animation: cu-blink 1.05s step-end infinite; + text-shadow: 0 0 8px var(--cu-accent-hot); + font-size: 0.85em; + vertical-align: baseline; +} .swagger-ui .info p, .swagger-ui .info li, .swagger-ui .info table, @@ -124,47 +276,67 @@ body { font-size: 15px !important; line-height: 1.55 !important; } +.swagger-ui .info .base-url { + font-family: var(--cu-mono) !important; + color: var(--cu-accent-hot) !important; + font-size: 13px !important; +} .swagger-ui .info a { color: var(--cu-accent) !important; text-decoration: none !important; + border-bottom: 1px dashed rgba(110, 168, 255, 0.35); } .swagger-ui .info a:hover { color: #b3d4ff !important; - text-decoration: underline !important; + border-bottom-color: var(--cu-accent); } -/* Authorize strip */ +/* ── Authorize strip ─────────────────────────────────────── */ .swagger-ui .scheme-container { - background: rgba(18, 24, 38, 0.9) !important; + background: rgba(18, 24, 38, 0.92) !important; box-shadow: none !important; border: 1px solid var(--cu-border); border-radius: var(--cu-radius); margin: 0 0 20px !important; padding: 14px 18px !important; backdrop-filter: blur(8px); + position: relative; +} +.swagger-ui .scheme-container::before { + content: "[ auth ]"; + font-family: var(--cu-mono); + font-size: 10px; + font-weight: 700; + letter-spacing: 0.12em; + color: var(--cu-text-mute); + margin-right: 10px; + text-transform: uppercase; } .swagger-ui .btn.authorize { - background: linear-gradient(135deg, rgba(110, 168, 255, 0.15), rgba(167, 139, 250, 0.15)) !important; - border: 1px solid var(--cu-accent) !important; - color: var(--cu-accent) !important; + background: linear-gradient(135deg, rgba(34, 211, 166, 0.12), rgba(110, 168, 255, 0.12)) !important; + border: 1px solid var(--cu-accent-hot) !important; + color: var(--cu-accent-hot) !important; border-radius: 10px !important; - font-family: var(--cu-font) !important; - font-weight: 600 !important; - letter-spacing: 0.02em; + font-family: var(--cu-mono) !important; + font-weight: 700 !important; + letter-spacing: 0.06em; + text-transform: uppercase; + font-size: 12px !important; transition: transform 0.12s ease, box-shadow 0.12s ease; + box-shadow: 0 0 18px rgba(34, 211, 166, 0.12); } .swagger-ui .btn.authorize:hover { transform: translateY(-1px); - box-shadow: 0 6px 20px rgba(110, 168, 255, 0.25); + box-shadow: 0 6px 22px rgba(34, 211, 166, 0.3); } .swagger-ui .btn.authorize svg { - fill: var(--cu-accent) !important; + fill: var(--cu-accent-hot) !important; } .swagger-ui .authorization__btn.locked { opacity: 1; } -/* Tags */ +/* ── Tags ────────────────────────────────────────────────── */ .swagger-ui .opblock-tag { color: var(--cu-text) !important; border-bottom: 1px solid var(--cu-border) !important; @@ -179,8 +351,14 @@ body { font-family: var(--cu-mono) !important; font-size: 12px !important; } +.swagger-ui .opblock-tag-section h3.opblock-tag span:first-child::before { + content: "> "; + color: var(--cu-accent-hot); + font-family: var(--cu-mono); + font-weight: 700; +} -/* Operation blocks */ +/* ── Operation blocks ────────────────────────────────────── */ .swagger-ui .opblock { border-radius: 12px !important; margin: 0 0 12px !important; @@ -188,6 +366,10 @@ body { border: 1px solid var(--cu-border) !important; overflow: hidden; background: var(--cu-bg-panel) !important; + transition: border-color 0.15s ease, box-shadow 0.15s ease; +} +.swagger-ui .opblock:hover { + box-shadow: 0 10px 28px rgba(0, 0, 0, 0.35) !important; } .swagger-ui .opblock .opblock-summary { border: none !important; @@ -199,7 +381,8 @@ body { font-weight: 700 !important; min-width: 72px !important; text-align: center; - letter-spacing: 0.04em; + letter-spacing: 0.06em; + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); } .swagger-ui .opblock .opblock-summary-path, .swagger-ui .opblock .opblock-summary-path__deprecated { @@ -208,6 +391,11 @@ body { font-size: 14px !important; font-weight: 600 !important; } +.swagger-ui .opblock .opblock-summary-path::before { + content: "› "; + color: var(--cu-accent-hot); + opacity: 0.7; +} .swagger-ui .opblock .opblock-summary-description { color: var(--cu-text-dim) !important; font-family: var(--cu-font) !important; @@ -219,6 +407,7 @@ body { } .swagger-ui .opblock.opblock-get .opblock-summary-method { background: var(--cu-get) !important; + color: #041018 !important; } .swagger-ui .opblock.opblock-post { background: linear-gradient(90deg, rgba(52, 211, 153, 0.10), var(--cu-bg-panel) 40%) !important; @@ -287,7 +476,7 @@ body { color: var(--cu-danger) !important; } -/* Models */ +/* ── Models ──────────────────────────────────────────────── */ .swagger-ui section.models { border: 1px solid var(--cu-border) !important; background: var(--cu-bg-panel) !important; @@ -298,7 +487,14 @@ body { color: var(--cu-text) !important; border-bottom: 1px solid var(--cu-border) !important; background: transparent !important; - font-family: var(--cu-font) !important; + font-family: var(--cu-mono) !important; + letter-spacing: 0.04em; +} +.swagger-ui section.models h4 span::before { + content: "struct "; + color: var(--cu-accent-hot); + font-size: 0.85em; + opacity: 0.8; } .swagger-ui .model-box, .swagger-ui .model, @@ -313,7 +509,7 @@ body { color: var(--cu-accent-hot) !important; } -/* Inputs */ +/* ── Inputs ──────────────────────────────────────────────── */ .swagger-ui input[type="text"], .swagger-ui input[type="password"], .swagger-ui input[type="search"], @@ -333,15 +529,15 @@ body { .swagger-ui input:focus, .swagger-ui textarea:focus, .swagger-ui select:focus { - border-color: var(--cu-accent) !important; - box-shadow: 0 0 0 3px rgba(110, 168, 255, 0.18) !important; + border-color: var(--cu-accent-hot) !important; + box-shadow: 0 0 0 3px rgba(34, 211, 166, 0.16) !important; } .swagger-ui input::placeholder, .swagger-ui textarea::placeholder { color: var(--cu-text-mute) !important; } -/* Buttons */ +/* ── Buttons ─────────────────────────────────────────────── */ .swagger-ui .btn { background: var(--cu-bg-soft) !important; color: var(--cu-text) !important; @@ -351,14 +547,21 @@ body { font-family: var(--cu-font) !important; font-weight: 600 !important; } +/* Execute = "run the payload" */ .swagger-ui .btn.execute { - background: linear-gradient(135deg, #3b82f6, #6366f1) !important; + background: linear-gradient(135deg, #059669, #0d9488 55%, #2563eb) !important; border: none !important; color: #fff !important; - box-shadow: 0 8px 24px rgba(59, 130, 246, 0.35) !important; + box-shadow: 0 8px 24px rgba(16, 185, 129, 0.35) !important; + font-family: var(--cu-mono) !important; + font-weight: 700 !important; + letter-spacing: 0.08em; + text-transform: uppercase; + font-size: 12px !important; } .swagger-ui .btn.execute:hover { - filter: brightness(1.08); + filter: brightness(1.1); + box-shadow: 0 10px 28px rgba(34, 211, 166, 0.45) !important; } .swagger-ui .btn.cancel { background: transparent !important; @@ -366,21 +569,26 @@ body { color: var(--cu-text-dim) !important; } .swagger-ui .try-out__btn { - border-color: var(--cu-accent) !important; - color: var(--cu-accent) !important; - background: transparent !important; + border-color: var(--cu-accent-hot) !important; + color: var(--cu-accent-hot) !important; + background: rgba(34, 211, 166, 0.06) !important; + font-family: var(--cu-mono) !important; + font-size: 12px !important; + letter-spacing: 0.04em; } -/* Code / responses */ +/* ── Code / responses (terminal phosphor) ────────────────── */ .swagger-ui .highlight-code, .swagger-ui .microlight { - background: #05070c !important; - color: #dbe4ff !important; - border: 1px solid var(--cu-border) !important; + background: #030508 !important; + color: #9ef0c8 !important; + border: 1px solid rgba(34, 211, 166, 0.2) !important; border-radius: 10px !important; font-family: var(--cu-mono) !important; font-size: 12.5px !important; line-height: 1.5 !important; + box-shadow: inset 0 0 40px rgba(34, 211, 166, 0.04), 0 0 0 1px rgba(0, 0, 0, 0.4); + text-shadow: 0 0 8px rgba(34, 211, 166, 0.15); } .swagger-ui .responses-inner h4, .swagger-ui .responses-inner h5, @@ -391,6 +599,14 @@ body { .swagger-ui .renderedMarkdown code { color: var(--cu-text-dim) !important; } +.swagger-ui .markdown code, +.swagger-ui .renderedMarkdown code { + font-family: var(--cu-mono) !important; + background: rgba(34, 211, 166, 0.08) !important; + color: var(--cu-accent-hot) !important; + border-radius: 4px; + padding: 1px 5px; +} .swagger-ui table tbody tr td { color: var(--cu-text-dim) !important; border-color: var(--cu-border) !important; @@ -398,15 +614,21 @@ body { .swagger-ui .response-control-media-type__title { color: var(--cu-text-mute) !important; } +/* status codes get a bit of matrix love */ +.swagger-ui .response-col_status { + font-family: var(--cu-mono) !important; + font-weight: 700 !important; +} /* Curl */ .swagger-ui .curl-command, .swagger-ui .curl { - background: #05070c !important; - color: #dbe4ff !important; - border: 1px solid var(--cu-border) !important; + background: #030508 !important; + color: #9ef0c8 !important; + border: 1px solid rgba(34, 211, 166, 0.22) !important; border-radius: 10px !important; font-family: var(--cu-mono) !important; + box-shadow: inset 0 0 30px rgba(34, 211, 166, 0.05); } /* Copy buttons etc */ @@ -414,17 +636,25 @@ body { background: var(--cu-bg-soft) !important; } -/* Authorize modal */ +/* ── Authorize modal ─────────────────────────────────────── */ .swagger-ui .dialog-ux .modal-ux { background: var(--cu-bg-panel) !important; border: 1px solid var(--cu-border) !important; + border-top: 2px solid var(--cu-accent-hot) !important; border-radius: 16px !important; color: var(--cu-text) !important; - box-shadow: 0 24px 80px rgba(0, 0, 0, 0.65) !important; + box-shadow: 0 24px 80px rgba(0, 0, 0, 0.65), 0 0 40px rgba(34, 211, 166, 0.08) !important; } .swagger-ui .dialog-ux .modal-ux-header { border-bottom: 1px solid var(--cu-border) !important; } +.swagger-ui .dialog-ux .modal-ux-header h3 { + font-family: var(--cu-mono) !important; + letter-spacing: 0.04em; +} +.swagger-ui .dialog-ux .modal-ux-header h3::before { + content: "🔑 "; +} .swagger-ui .dialog-ux .modal-ux-header h3, .swagger-ui .dialog-ux .modal-ux-content h4, .swagger-ui .dialog-ux .modal-ux-content p, @@ -435,26 +665,35 @@ body { .swagger-ui .auth-container { border-color: var(--cu-border) !important; border-radius: 10px !important; + background: rgba(3, 5, 8, 0.5) !important; } .swagger-ui .auth-btn-wrapper .btn-done, .swagger-ui .btn.modal-btn.auth.authorize { - background: linear-gradient(135deg, #3b82f6, #6366f1) !important; + background: linear-gradient(135deg, #059669, #0d9488) !important; border: none !important; color: #fff !important; + font-family: var(--cu-mono) !important; + letter-spacing: 0.06em; + text-transform: uppercase; + font-size: 12px !important; + box-shadow: 0 6px 20px rgba(16, 185, 129, 0.35) !important; } /* Filter */ .swagger-ui .filter .operation-filter-input { background: var(--cu-bg) !important; border: 1px solid var(--cu-border-strong) !important; - color: var(--cu-text) !important; + color: var(--cu-accent-hot) !important; border-radius: 10px !important; font-family: var(--cu-mono) !important; } +.swagger-ui .filter .operation-filter-input::placeholder { + color: var(--cu-text-mute) !important; +} /* Loading spinner */ .swagger-ui .loading-container .loading::after { - border-color: var(--cu-accent) transparent transparent !important; + border-color: var(--cu-accent-hot) transparent transparent !important; } /* Scrollbars */ @@ -463,7 +702,7 @@ body { height: 10px; } .swagger-ui ::-webkit-scrollbar-thumb { - background: linear-gradient(180deg, #3a4a6a, #2a354d); + background: linear-gradient(180deg, #1a9b74, #2a354d); border-radius: 8px; } .swagger-ui ::-webkit-scrollbar-track { @@ -475,3 +714,14 @@ body { .swagger-ui .renderedMarkdown li { color: var(--cu-text-dim) !important; } + +/* Prefer reduced motion: kill blink/scan for a11y */ +@media (prefers-reduced-motion: reduce) { + .swagger-ui .info .title::after, + .swagger-ui .topbar { + animation: none !important; + } + body::before { + opacity: 0.35; + } +} From 35c75821ffd93e47d9f1d9e67c8ff2bdfd2855bb Mon Sep 17 00:00:00 2001 From: David Binney Date: Mon, 10 Aug 2026 23:46:07 +1000 Subject: [PATCH 4/5] feat: SWAGGER_HOST / SWAGGER_SCHEME env for Try-it-out URL Startup defaults for the public host Swagger calls, so you don't need ?host=&scheme= on every open. Query params still win per request; otherwise env; otherwise request Host / X-Forwarded-Proto. --- README.md | 15 +++++++++++++-- k8s-cluster-util-apis.yml | 5 +++++ main.go | 8 ++++++++ routes/routes.go | 15 ++++++++++++--- 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 542b6eb..67ffa0a 100644 --- a/README.md +++ b/README.md @@ -118,8 +118,19 @@ If still 401: hard-refresh swagger (stale token), confirm `curl -sS -o /dev/null -w '%{http_code}\n' -H 'Authorization: Bearer dev' http://127.0.0.1:18080/a/env` is `200`, or check `podman logs cu-api | grep '"token"'`. -**Host / port:** UI uses the host you opened. Override with -`?host=127.0.0.1:18080&scheme=http` if needed. +**Host / scheme (Try-it-out target):** by default the UI uses the host you opened. +If you’re behind an ingress / port-map and that is wrong, set once at startup: + +| Env | Example | Purpose | +|-----|---------|---------| +| `SWAGGER_HOST` | `api.example.com` or `cluster-utils-api.ns.svc:8080` | Host:port Swagger “Try it out” calls | +| `SWAGGER_SCHEME` | `https` or `http` | Scheme for those calls | + +Per-request override still works and wins over env: +`/api-docs/index.html?host=127.0.0.1:18080&scheme=http` + +Priority: **query → env → request Host / `X-Forwarded-Proto`**. +(`PORT` is only the process listen port; it does not set the public URL.) **Theme:** dark by default (Swagger UI has no real dark mode built-in; we inject CSS). Stock bright UI: `/api-docs/index.html?theme=light`. diff --git a/k8s-cluster-util-apis.yml b/k8s-cluster-util-apis.yml index 0de1b4b..62695a6 100644 --- a/k8s-cluster-util-apis.yml +++ b/k8s-cluster-util-apis.yml @@ -88,6 +88,11 @@ spec: # value: "10" # - name: AUTH_TOKEN # value: "fixed-token-for-tests" + # Swagger Try-it-out public URL (optional; default = request Host) + # - name: SWAGGER_HOST + # value: cluster-utils-api.default.svc.cluster.local:8080 + # - name: SWAGGER_SCHEME + # value: http # --- OTEL traces (OTLP *push* to Alloy → Tempo; not scraped) --- # - name: OTEL_SERVICE_NAME # value: cluster-utils-api diff --git a/main.go b/main.go index aa08650..26ccb55 100644 --- a/main.go +++ b/main.go @@ -98,6 +98,14 @@ func main() { zap.String("env", getCurlCommand(port, securityToken)), zap.String("probes", fmt.Sprintf("curl -sS -H 'Authorization: Bearer %s' http://localhost:%d/a/control/probes | jq", securityToken, port)), ) + // Optional fixed Try-it-out target (Swagger host/scheme). Empty = use request Host. + if h, s := os.Getenv("SWAGGER_HOST"), os.Getenv("SWAGGER_SCHEME"); h != "" || s != "" { + logger.Info("swagger public URL override (Try-it-out)", + zap.String("SWAGGER_HOST", h), + zap.String("SWAGGER_SCHEME", s), + zap.String("note", "query ?host=&scheme= still wins per request"), + ) + } r.Run(fmt.Sprintf(":%d", port)) } diff --git a/routes/routes.go b/routes/routes.go index 25b09a6..cd1fa6c 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -4,6 +4,7 @@ import ( "bytes" _ "embed" "net/http" + "os" "strings" "sync" @@ -34,9 +35,9 @@ func SetupRouter(logger *zap.Logger, st string, r *gin.Engine) { c.Redirect(http.StatusFound, "/api-docs/index.html") }) - // Swagger UI: persist Authorize token; host/scheme follow where you opened the page. + // Swagger UI: persist Authorize token; host/scheme for Try-it-out. + // Priority: ?host=&scheme= query > SWAGGER_HOST / SWAGGER_SCHEME env > request Host / X-Forwarded-Proto. // Dark theme by default; ?theme=light for stock Swagger look. - // /api-docs/index.html?host=my-svc:8080&scheme=http r.GET("/api-docs/*any", swaggerHandler()) r.GET("/help", handlers.HelpHandler) @@ -88,14 +89,22 @@ func swaggerHandler() gin.HandlerFunc { return } + // Resolve Try-it-out server URL (not the listen bind — that's PORT). + // 1) query 2) SWAGGER_* env 3) request host := strings.TrimSpace(c.Query("host")) + if host == "" { + host = strings.TrimSpace(os.Getenv("SWAGGER_HOST")) + } if host == "" { host = c.Request.Host } scheme := strings.ToLower(strings.TrimSpace(c.Query("scheme"))) if scheme != "http" && scheme != "https" { - if c.Request.TLS != nil || c.GetHeader("X-Forwarded-Proto") == "https" { + scheme = strings.ToLower(strings.TrimSpace(os.Getenv("SWAGGER_SCHEME"))) + } + if scheme != "http" && scheme != "https" { + if c.Request.TLS != nil || strings.EqualFold(c.GetHeader("X-Forwarded-Proto"), "https") { scheme = "https" } else { scheme = "http" From 1fee9bd0dbc0bda02a6a15961ba53af8bd14717a Mon Sep 17 00:00:00 2001 From: David Binney Date: Mon, 10 Aug 2026 23:59:37 +1000 Subject: [PATCH 5/5] feat: live Try-it-out target in swagger + v2.5.2 docs Show current scheme://host in the Swagger info panel, with source (query/env/request) and ?host=&scheme= tip. Bump to 2.5.2, link the API repo next to the pair toolkit, refresh README/k8s notes. --- README.md | 42 ++++++++++++++++++++++--------- RELEASE-v2.5.2.md | 35 ++++++++++++++++++++++++++ docs/docs.go | 4 +-- docs/swagger.json | 4 +-- docs/swagger.yaml | 5 ++-- k8s-cluster-util-apis.yml | 4 +-- main.go | 5 ++-- routes/routes.go | 52 ++++++++++++++++++++++++++++++--------- routes/swagger-dark.css | 22 +++++++++++++++-- 9 files changed, 139 insertions(+), 34 deletions(-) create mode 100644 RELEASE-v2.5.2.md diff --git a/README.md b/README.md index 67ffa0a..4d2a53a 100644 --- a/README.md +++ b/README.md @@ -118,22 +118,42 @@ If still 401: hard-refresh swagger (stale token), confirm `curl -sS -o /dev/null -w '%{http_code}\n' -H 'Authorization: Bearer dev' http://127.0.0.1:18080/a/env` is `200`, or check `podman logs cu-api | grep '"token"'`. -**Host / scheme (Try-it-out target):** by default the UI uses the host you opened. -If you’re behind an ingress / port-map and that is wrong, set once at startup: +#### Change the Try-it-out API host (query params — most common) + +By default Swagger calls **whatever host you opened the docs on**. +If that is wrong (port-map, ingress, in-cluster Service, tunnel), **pass the host on the docs URL** — you will do this a lot: + +```text +# point Try-it-out at a different host:port +/api-docs/index.html?host=127.0.0.1:18080&scheme=http + +# in-cluster Service from a port-forwarded docs UI +/api-docs/index.html?host=cluster-utils-api.default.svc.cluster.local:8080&scheme=http + +# public ingress +/api-docs/index.html?host=api.example.com&scheme=https +``` + +| Query | Example | What it does | +|-------|---------|----------------| +| `host` | `my-svc.ns.svc:8080` | Host:port **Execute** / Try-it-out uses | +| `scheme` | `http` or `https` | Scheme for those calls | +| `theme` | `light` | Stock bright Swagger (dark is default) | + +Bookmark the full URL once; every open keeps that target. + +#### Or set it once at process start (env) | Env | Example | Purpose | |-----|---------|---------| -| `SWAGGER_HOST` | `api.example.com` or `cluster-utils-api.ns.svc:8080` | Host:port Swagger “Try it out” calls | -| `SWAGGER_SCHEME` | `https` or `http` | Scheme for those calls | - -Per-request override still works and wins over env: -`/api-docs/index.html?host=127.0.0.1:18080&scheme=http` +| `SWAGGER_HOST` | `api.example.com` or `cluster-utils-api.ns.svc:8080` | Default host:port for Try-it-out | +| `SWAGGER_SCHEME` | `https` or `http` | Default scheme | -Priority: **query → env → request Host / `X-Forwarded-Proto`**. -(`PORT` is only the process listen port; it does not set the public URL.) +**Priority:** query `?host=&scheme=` → env `SWAGGER_*` → request Host / `X-Forwarded-Proto`. +(`PORT` is only the process listen port; it does **not** set the public URL.) -**Theme:** dark by default (Swagger UI has no real dark mode built-in; we inject CSS). -Stock bright UI: `/api-docs/index.html?theme=light`. +**Theme:** dark by default (custom CSS skin — stock Swagger has no real dark mode). +Light: `?theme=light`. --- diff --git a/RELEASE-v2.5.2.md b/RELEASE-v2.5.2.md new file mode 100644 index 0000000..48a2209 --- /dev/null +++ b/RELEASE-v2.5.2.md @@ -0,0 +1,35 @@ +## cluster-utils-api v2.5.2 — Swagger dark skin + Try-it-out host + +Polish release: usable dark Swagger UI and clear control over which host “Try it out” hits. + +### Swagger UI skin +- Custom **dark theme by default** (stock Swagger has no first-class dark mode — we inject CSS) +- Donkeyx / terminal flair: shell-prompt topbar, phosphor code, scan grid +- `?theme=light` restores stock bright UI + +### Try-it-out host / scheme +Swagger defaults to the host you opened. When that is wrong (port-map, ingress, in-cluster Service): + +**Query params (most common — bookmark the URL):** +```text +/api-docs/index.html?host=my-svc.ns.svc:8080&scheme=http +/api-docs/index.html?host=api.example.com&scheme=https +``` + +**Or once at startup:** +| Env | Purpose | +|-----|---------| +| `SWAGGER_HOST` | Default host:port for Try-it-out | +| `SWAGGER_SCHEME` | `http` or `https` | + +**Priority:** query → env → request Host / `X-Forwarded-Proto` + +The Swagger **info panel shows the live target** (`scheme://host/`), where it came from, and how to override it. + +### Docs +- Links to **this repo** (docs/source) and **cluster-utils** (pair toolkit) +- README + k8s sample updated for host override and image `2.5.2` + +### Images +- `ghcr.io/donkeyx/cluster-utils-api:2.5.2` / `:latest` +- `donkeyx/cluster-utils-api:2.5.2` / `:latest` diff --git a/docs/docs.go b/docs/docs.go index e2c6a18..b5f1a18 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -847,12 +847,12 @@ const docTemplate = `{ // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ - Version: "2.5.0", + Version: "2.5.2", Host: "", BasePath: "/", Schemes: []string{}, Title: "Cluster Utils API", - Description: "HTTP sidekick for cluster-utils — drop into a namespace and poke probes, ingress headers, env, and east-west hops.\n\n• Authorize (top-right lock): Bearer <token> e.g. local podman → Bearer dev\n• Probes: /startupz /livez /readyz · Control: GET|PUT /a/control/probes\n• Proxy: GET /a/proxy?url=https://httpbin.org/get (or POST JSON body)\n• Pair: https://github.com/donkeyx/cluster-utils", + Description: "HTTP sidekick for cluster-utils — drop into a namespace and poke probes, ingress headers, env, and east-west hops.\n\n• Authorize (top-right lock): Bearer <token> e.g. local podman → Bearer dev\n• Probes: /startupz /livez /readyz · Control: GET|PUT /a/control/probes\n• Proxy: GET /a/proxy?url=https://httpbin.org/get (or POST JSON body)\n• Docs / source: https://github.com/donkeyx/cluster-utils-api\n• Pair (shell toolkit): https://github.com/donkeyx/cluster-utils", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, LeftDelim: "{{", diff --git a/docs/swagger.json b/docs/swagger.json index d7209c9..25658b9 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1,10 +1,10 @@ { "swagger": "2.0", "info": { - "description": "HTTP sidekick for cluster-utils — drop into a namespace and poke probes, ingress headers, env, and east-west hops.\n\n• Authorize (top-right lock): Bearer \u0026lt;token\u0026gt; e.g. local podman → Bearer dev\n• Probes: /startupz /livez /readyz · Control: GET|PUT /a/control/probes\n• Proxy: GET /a/proxy?url=https://httpbin.org/get (or POST JSON body)\n• Pair: https://github.com/donkeyx/cluster-utils", + "description": "HTTP sidekick for cluster-utils — drop into a namespace and poke probes, ingress headers, env, and east-west hops.\n\n• Authorize (top-right lock): Bearer \u0026lt;token\u0026gt; e.g. local podman → Bearer dev\n• Probes: /startupz /livez /readyz · Control: GET|PUT /a/control/probes\n• Proxy: GET /a/proxy?url=https://httpbin.org/get (or POST JSON body)\n• Docs / source: https://github.com/donkeyx/cluster-utils-api\n• Pair (shell toolkit): https://github.com/donkeyx/cluster-utils", "title": "Cluster Utils API", "contact": {}, - "version": "2.5.0" + "version": "2.5.2" }, "basePath": "/", "paths": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 1f6c9ce..8e9218e 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -135,9 +135,10 @@ info: • Authorize (top-right lock): Bearer <token> e.g. local podman → Bearer dev • Probes: /startupz /livez /readyz · Control: GET|PUT /a/control/probes • Proxy: GET /a/proxy?url=https://httpbin.org/get (or POST JSON body) - • Pair: https://github.com/donkeyx/cluster-utils + • Docs / source: https://github.com/donkeyx/cluster-utils-api + • Pair (shell toolkit): https://github.com/donkeyx/cluster-utils title: Cluster Utils API - version: 2.5.0 + version: 2.5.2 paths: /a/control/probes: get: diff --git a/k8s-cluster-util-apis.yml b/k8s-cluster-util-apis.yml index 62695a6..ccdbf7c 100644 --- a/k8s-cluster-util-apis.yml +++ b/k8s-cluster-util-apis.yml @@ -21,8 +21,8 @@ spec: - name: cluster-utils-api # GHCR primary (Hub still works as mirror). Version tag → default pull IfNotPresent. # :latest still forces Always in kube even without imagePullPolicy. - image: ghcr.io/donkeyx/cluster-utils-api:2.5.0 - # Alternative: docker.io/donkeyx/cluster-utils-api:2.5.0 + image: ghcr.io/donkeyx/cluster-utils-api:2.5.2 + # Alternative: docker.io/donkeyx/cluster-utils-api:2.5.2 ports: - name: http containerPort: 8080 diff --git a/main.go b/main.go index 26ccb55..67c43ae 100644 --- a/main.go +++ b/main.go @@ -1,11 +1,12 @@ // @title Cluster Utils API -// @version 2.5.0 +// @version 2.5.2 // @description HTTP sidekick for cluster-utils — drop into a namespace and poke probes, ingress headers, env, and east-west hops. // @description // @description • Authorize (top-right lock): Bearer <token> e.g. local podman → Bearer dev // @description • Probes: /startupz /livez /readyz · Control: GET|PUT /a/control/probes // @description • Proxy: GET /a/proxy?url=https://httpbin.org/get (or POST JSON body) -// @description • Pair: https://github.com/donkeyx/cluster-utils +// @description • Docs / source: https://github.com/donkeyx/cluster-utils-api +// @description • Pair (shell toolkit): https://github.com/donkeyx/cluster-utils // @BasePath / // @securityDefinitions.apikey BearerAuth // @in header diff --git a/routes/routes.go b/routes/routes.go index cd1fa6c..a8a5e02 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -3,6 +3,7 @@ package routes import ( "bytes" _ "embed" + "fmt" "net/http" "os" "strings" @@ -19,9 +20,15 @@ import ( ginSwagger "github.com/swaggo/gin-swagger" ) -// swaggerInfoMu guards docs.SwaggerInfo Host/Schemes when serving the UI for different origins. +// swaggerInfoMu guards docs.SwaggerInfo Host/Schemes/Description when serving the UI. var swaggerInfoMu sync.Mutex +// swaggerBaseDescription is the static @description from swag (no per-request target line). +var ( + swaggerBaseDescription string + swaggerBaseDescriptionOnce sync.Once +) + // darkCSS is injected into Swagger UI (gin-swagger has no first-class dark mode). // //go:embed swagger-dark.css @@ -91,23 +98,33 @@ func swaggerHandler() gin.HandlerFunc { // Resolve Try-it-out server URL (not the listen bind — that's PORT). // 1) query 2) SWAGGER_* env 3) request + hostSource := "request Host" host := strings.TrimSpace(c.Query("host")) - if host == "" { + if host != "" { + hostSource = "URL ?host=" + } else { host = strings.TrimSpace(os.Getenv("SWAGGER_HOST")) - } - if host == "" { - host = c.Request.Host + if host != "" { + hostSource = "SWAGGER_HOST env" + } else { + host = c.Request.Host + } } + schemeSource := "request / X-Forwarded-Proto" scheme := strings.ToLower(strings.TrimSpace(c.Query("scheme"))) - if scheme != "http" && scheme != "https" { + if scheme == "http" || scheme == "https" { + schemeSource = "URL ?scheme=" + } else { scheme = strings.ToLower(strings.TrimSpace(os.Getenv("SWAGGER_SCHEME"))) - } - if scheme != "http" && scheme != "https" { - if c.Request.TLS != nil || strings.EqualFold(c.GetHeader("X-Forwarded-Proto"), "https") { - scheme = "https" + if scheme == "http" || scheme == "https" { + schemeSource = "SWAGGER_SCHEME env" } else { - scheme = "http" + if c.Request.TLS != nil || strings.EqualFold(c.GetHeader("X-Forwarded-Proto"), "https") { + scheme = "https" + } else { + scheme = "http" + } } } @@ -115,9 +132,22 @@ func swaggerHandler() gin.HandlerFunc { // Serialize updates to the global SwaggerInfo used when doc.json is generated. swaggerInfoMu.Lock() + swaggerBaseDescriptionOnce.Do(func() { + swaggerBaseDescription = docs.SwaggerInfo.Description + }) docs.SwaggerInfo.Host = host docs.SwaggerInfo.Schemes = []string{scheme} docs.SwaggerInfo.BasePath = "/" + // Live target + how to change it (shows in the Swagger info panel). + docs.SwaggerInfo.Description = swaggerBaseDescription + fmt.Sprintf( + "\n\n---\n\n**Try-it-out target (now):** `%s://%s/` \n"+ + "_host from %s · scheme from %s_ \n\n"+ + "Change for this tab: append query params, e.g. \n"+ + "`/api-docs/index.html?host=my-svc.ns.svc:8080&scheme=http` \n"+ + "Or set env `SWAGGER_HOST` / `SWAGGER_SCHEME` once at startup. \n"+ + "Priority: **query → env → request Host**.", + scheme, host, hostSource, schemeSource, + ) // Capture HTML for index so we can inject dark CSS (stock swagger is bright white). if !light && isSwaggerIndex(any) { diff --git a/routes/swagger-dark.css b/routes/swagger-dark.css index fda8d6b..d62b723 100644 --- a/routes/swagger-dark.css +++ b/routes/swagger-dark.css @@ -222,9 +222,9 @@ body::after { position: relative; overflow: hidden; } -/* terminal session banner */ +/* terminal session banner — live host is in the description body (server-injected) */ .swagger-ui .info::before { - content: "$> session opened · github.com/donkeyx · /a/* needs Bearer AUTH_TOKEN"; + content: "$> session opened · Try-it-out host is below · override with ?host=&scheme= · /a/* needs Bearer AUTH_TOKEN"; display: block; font-family: var(--cu-mono); font-size: 11.5px; @@ -237,6 +237,24 @@ body::after { text-shadow: 0 0 10px rgba(34, 211, 166, 0.2); opacity: 0.9; } +/* highlight the live target block (markdown
+ strong in description) */ +.swagger-ui .info .description .renderedMarkdown hr, +.swagger-ui .info .description .markdown hr { + border: none; + border-top: 1px dashed rgba(34, 211, 166, 0.35); + margin: 14px 0; +} +.swagger-ui .info .description .renderedMarkdown strong, +.swagger-ui .info .description .markdown strong { + color: var(--cu-accent-hot) !important; + font-family: var(--cu-mono); + font-weight: 700; +} +.swagger-ui .info .description .renderedMarkdown code, +.swagger-ui .info .description .markdown code { + color: var(--cu-accent-hot) !important; + background: rgba(34, 211, 166, 0.1) !important; +} .swagger-ui .info .title { color: var(--cu-text) !important; font-family: var(--cu-font) !important;