From 13e3aec2d58ba11e7ff9b35e941669b8d1303883 Mon Sep 17 00:00:00 2001 From: hobokenchicken Date: Mon, 27 Jul 2026 09:51:15 -0400 Subject: [PATCH] feat(web): add automated build version tracking and update notification banner --- Makefile | 2 +- cmd/server/main.go | 13 ++++-- web/src/components/Layout.tsx | 2 + web/src/components/VersionNotifier.tsx | 58 ++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 web/src/components/VersionNotifier.tsx diff --git a/Makefile b/Makefile index 64576f6..ce4a5f4 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ build: build-web build-server # Build the Go server build-server: - CGO_ENABLED=0 go build -o dumpster-server ./cmd/server + CGO_ENABLED=0 go build -ldflags "-X main.GitSHA=$(GIT_SHA)" -o dumpster-server ./cmd/server # Build the TUI client build-tui: diff --git a/cmd/server/main.go b/cmd/server/main.go index 733b092..1705eff 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -47,9 +47,8 @@ import ( // @host localhost:8080 // @BasePath /api/v1 // @schemes http https -// @securityDefinitions.apikey SessionAuth -// @in cookie -// @name dumpster_session +var GitSHA = "dev" + func main() { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) @@ -153,6 +152,14 @@ func main() { // API routes r.Route("/api/v1", func(r chi.Router) { + r.Get("/version", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + json.NewEncoder(w).Encode(map[string]string{ + "version": GitSHA, + }) + }) + // Auth (public: register, login, logout) with strict rate limiting r.Route("/auth", func(r chi.Router) { r.Use(middleware.RateLimit(5, 10)) // 5 req/s, burst 10 diff --git a/web/src/components/Layout.tsx b/web/src/components/Layout.tsx index e9e7929..c32c73c 100644 --- a/web/src/components/Layout.tsx +++ b/web/src/components/Layout.tsx @@ -15,6 +15,7 @@ import { MemberList } from './MemberList.tsx'; import { VoicePanel } from './VoicePanel.tsx'; import { ServerSettingsModal } from './ServerSettingsModal.tsx'; import { ThemeToggle } from './ThemeToggle.tsx'; +import { VersionNotifier } from './VersionNotifier.tsx'; const STATUS_CYCLE: UserStatus[] = ['online', 'idle', 'dnd', 'offline']; function statusColor(status: UserStatus): string { @@ -99,6 +100,7 @@ export function Layout() { return (
+ {/* Outer terminal frame with safe area */}
diff --git a/web/src/components/VersionNotifier.tsx b/web/src/components/VersionNotifier.tsx new file mode 100644 index 0000000..f551eec --- /dev/null +++ b/web/src/components/VersionNotifier.tsx @@ -0,0 +1,58 @@ +import { useEffect, useState } from "react"; +import { api } from "../lib/api.ts"; +import { useWebSocketStore } from "../stores/ws.ts"; + +export function VersionNotifier() { + const [serverVersion, setServerVersion] = useState(null); + const [updateAvailable, setUpdateAvailable] = useState(false); + const connected = useWebSocketStore((s) => s.connected); + + const checkVersion = async () => { + try { + const data = await api.get<{ version: string }>("/version"); + if (data && data.version && data.version !== "dev") { + setServerVersion((prev) => { + if (!prev) { + return data.version; + } + if (prev !== data.version) { + setUpdateAvailable(true); + } + return data.version; + }); + } + } catch { + // ignore check errors + } + }; + + useEffect(() => { + checkVersion(); + const interval = setInterval(checkVersion, 2 * 60 * 1000); // Check every 2 mins + return () => clearInterval(interval); + }, []); + + useEffect(() => { + if (connected) { + checkVersion(); + } + }, [connected]); + + const handleReload = () => { + window.location.reload(); + }; + + if (!updateAvailable) return null; + + return ( +
+ 🚀 A new update is available! ({serverVersion}) + +
+ ); +}