feat(web): add automated build version tracking and update notification banner
This commit is contained in:
@@ -8,7 +8,7 @@ build: build-web build-server
|
|||||||
|
|
||||||
# Build the Go server
|
# Build the Go server
|
||||||
build-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 the TUI client
|
||||||
build-tui:
|
build-tui:
|
||||||
|
|||||||
+10
-3
@@ -47,9 +47,8 @@ import (
|
|||||||
// @host localhost:8080
|
// @host localhost:8080
|
||||||
// @BasePath /api/v1
|
// @BasePath /api/v1
|
||||||
// @schemes http https
|
// @schemes http https
|
||||||
// @securityDefinitions.apikey SessionAuth
|
var GitSHA = "dev"
|
||||||
// @in cookie
|
|
||||||
// @name dumpster_session
|
|
||||||
func main() {
|
func main() {
|
||||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||||
|
|
||||||
@@ -153,6 +152,14 @@ func main() {
|
|||||||
|
|
||||||
// API routes
|
// API routes
|
||||||
r.Route("/api/v1", func(r chi.Router) {
|
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
|
// Auth (public: register, login, logout) with strict rate limiting
|
||||||
r.Route("/auth", func(r chi.Router) {
|
r.Route("/auth", func(r chi.Router) {
|
||||||
r.Use(middleware.RateLimit(5, 10)) // 5 req/s, burst 10
|
r.Use(middleware.RateLimit(5, 10)) // 5 req/s, burst 10
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { MemberList } from './MemberList.tsx';
|
|||||||
import { VoicePanel } from './VoicePanel.tsx';
|
import { VoicePanel } from './VoicePanel.tsx';
|
||||||
import { ServerSettingsModal } from './ServerSettingsModal.tsx';
|
import { ServerSettingsModal } from './ServerSettingsModal.tsx';
|
||||||
import { ThemeToggle } from './ThemeToggle.tsx';
|
import { ThemeToggle } from './ThemeToggle.tsx';
|
||||||
|
import { VersionNotifier } from './VersionNotifier.tsx';
|
||||||
|
|
||||||
const STATUS_CYCLE: UserStatus[] = ['online', 'idle', 'dnd', 'offline'];
|
const STATUS_CYCLE: UserStatus[] = ['online', 'idle', 'dnd', 'offline'];
|
||||||
function statusColor(status: UserStatus): string {
|
function statusColor(status: UserStatus): string {
|
||||||
@@ -99,6 +100,7 @@ export function Layout() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full bg-gb-bg text-gb-fg font-mono flex flex-col">
|
<div className="h-full w-full bg-gb-bg text-gb-fg font-mono flex flex-col">
|
||||||
|
<VersionNotifier />
|
||||||
{/* Outer terminal frame with safe area */}
|
{/* Outer terminal frame with safe area */}
|
||||||
<div className="flex-1 terminal-border bg-gb-bg-h flex flex-col min-h-0"
|
<div className="flex-1 terminal-border bg-gb-bg-h flex flex-col min-h-0"
|
||||||
style={{ padding: 'var(--safe-top, 0.25rem) var(--safe-right, 0) var(--safe-bottom, 0) var(--safe-left, 0)' }}>
|
style={{ padding: 'var(--safe-top, 0.25rem) var(--safe-right, 0) var(--safe-bottom, 0) var(--safe-left, 0)' }}>
|
||||||
|
|||||||
@@ -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<string | null>(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 (
|
||||||
|
<div className="bg-gb-accent text-gb-bg-h px-4 py-2 text-xs font-semibold flex items-center justify-between shadow-lg z-50 animate-pulse">
|
||||||
|
<span>🚀 A new update is available! ({serverVersion})</span>
|
||||||
|
<button
|
||||||
|
onClick={handleReload}
|
||||||
|
className="bg-gb-bg-h text-gb-accent px-3 py-1 rounded hover:opacity-90 font-bold transition-all"
|
||||||
|
>
|
||||||
|
Update Now
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user