feat(web): display backend version info in sidebar
This commit is contained in:
parent
76cd7f8ad5
commit
18ec2631aa
7 changed files with 185 additions and 0 deletions
|
|
@ -76,6 +76,9 @@ func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
|
|||
// Launcher service parameters (port/public)
|
||||
h.registerLauncherConfigRoutes(mux)
|
||||
|
||||
// Runtime build/version metadata
|
||||
h.registerVersionRoutes(mux)
|
||||
|
||||
// WeChat QR login flow
|
||||
h.registerWeixinRoutes(mux)
|
||||
|
||||
|
|
|
|||
31
web/backend/api/version.go
Normal file
31
web/backend/api/version.go
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
)
|
||||
|
||||
type systemVersionResponse struct {
|
||||
Version string `json:"version"`
|
||||
GitCommit string `json:"git_commit,omitempty"`
|
||||
BuildTime string `json:"build_time,omitempty"`
|
||||
GoVersion string `json:"go_version"`
|
||||
}
|
||||
|
||||
func (h *Handler) registerVersionRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("GET /api/system/version", h.handleGetVersion)
|
||||
}
|
||||
|
||||
func (h *Handler) handleGetVersion(w http.ResponseWriter, _ *http.Request) {
|
||||
buildTime, goVer := config.FormatBuildInfo()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(systemVersionResponse{
|
||||
Version: config.GetVersion(),
|
||||
GitCommit: config.GitCommit,
|
||||
BuildTime: buildTime,
|
||||
GoVersion: goVer,
|
||||
})
|
||||
}
|
||||
98
web/backend/api/version_test.go
Normal file
98
web/backend/api/version_test.go
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
)
|
||||
|
||||
func TestGetSystemVersion(t *testing.T) {
|
||||
originalVersion := config.Version
|
||||
originalGitCommit := config.GitCommit
|
||||
originalBuildTime := config.BuildTime
|
||||
originalGoVersion := config.GoVersion
|
||||
t.Cleanup(func() {
|
||||
config.Version = originalVersion
|
||||
config.GitCommit = originalGitCommit
|
||||
config.BuildTime = originalBuildTime
|
||||
config.GoVersion = originalGoVersion
|
||||
})
|
||||
|
||||
config.Version = "v1.2.3"
|
||||
config.GitCommit = "deadbeef"
|
||||
config.BuildTime = "2026-03-27T12:34:56Z"
|
||||
config.GoVersion = "go1.24.1"
|
||||
|
||||
h := NewHandler("")
|
||||
mux := http.NewServeMux()
|
||||
h.RegisterRoutes(mux)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/system/version", nil)
|
||||
mux.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String())
|
||||
}
|
||||
|
||||
var got systemVersionResponse
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
|
||||
t.Fatalf("unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
if got.Version != config.Version {
|
||||
t.Fatalf("version = %q, want %q", got.Version, config.Version)
|
||||
}
|
||||
if got.GitCommit != config.GitCommit {
|
||||
t.Fatalf("git_commit = %q, want %q", got.GitCommit, config.GitCommit)
|
||||
}
|
||||
if got.BuildTime != config.BuildTime {
|
||||
t.Fatalf("build_time = %q, want %q", got.BuildTime, config.BuildTime)
|
||||
}
|
||||
if got.GoVersion != config.GoVersion {
|
||||
t.Fatalf("go_version = %q, want %q", got.GoVersion, config.GoVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSystemVersionUsesRuntimeGoVersionFallback(t *testing.T) {
|
||||
originalVersion := config.Version
|
||||
originalGitCommit := config.GitCommit
|
||||
originalBuildTime := config.BuildTime
|
||||
originalGoVersion := config.GoVersion
|
||||
t.Cleanup(func() {
|
||||
config.Version = originalVersion
|
||||
config.GitCommit = originalGitCommit
|
||||
config.BuildTime = originalBuildTime
|
||||
config.GoVersion = originalGoVersion
|
||||
})
|
||||
|
||||
config.Version = "dev"
|
||||
config.GitCommit = ""
|
||||
config.BuildTime = ""
|
||||
config.GoVersion = ""
|
||||
|
||||
h := NewHandler("")
|
||||
mux := http.NewServeMux()
|
||||
h.RegisterRoutes(mux)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/system/version", nil)
|
||||
mux.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String())
|
||||
}
|
||||
|
||||
var got systemVersionResponse
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
|
||||
t.Fatalf("unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
if got.GoVersion != runtime.Version() {
|
||||
t.Fatalf("go_version = %q, want runtime version %q", got.GoVersion, runtime.Version())
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,13 @@ export interface LauncherConfig {
|
|||
allowed_cidrs: string[]
|
||||
}
|
||||
|
||||
export interface SystemVersionInfo {
|
||||
version: string
|
||||
git_commit?: string
|
||||
build_time?: string
|
||||
go_version: string
|
||||
}
|
||||
|
||||
async function request<T>(path: string, options?: RequestInit): Promise<T> {
|
||||
const res = await fetch(path, options)
|
||||
if (!res.ok) {
|
||||
|
|
@ -60,3 +67,7 @@ export async function setLauncherConfig(
|
|||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export async function getSystemVersionInfo(): Promise<SystemVersionInfo> {
|
||||
return request<SystemVersionInfo>("/api/system/version")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,10 +10,12 @@ import {
|
|||
IconSparkles,
|
||||
IconTools,
|
||||
} from "@tabler/icons-react"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { Link, useRouterState } from "@tanstack/react-router"
|
||||
import * as React from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { getSystemVersionInfo } from "@/api/system"
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
|
|
@ -27,6 +29,7 @@ import {
|
|||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarFooter,
|
||||
SidebarMenuItem,
|
||||
SidebarRail,
|
||||
} from "@/components/ui/sidebar"
|
||||
|
|
@ -78,6 +81,13 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||
language: (i18n.resolvedLanguage ?? i18n.language ?? "").toLowerCase(),
|
||||
t,
|
||||
})
|
||||
const { data: versionInfo } = useQuery({
|
||||
queryKey: ["system", "version"],
|
||||
queryFn: getSystemVersionInfo,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
})
|
||||
|
||||
const versionText = versionInfo?.version ?? t("footer.version_unknown")
|
||||
|
||||
const navGroups: NavGroup[] = React.useMemo(() => {
|
||||
return [
|
||||
|
|
@ -235,6 +245,26 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||
</Collapsible>
|
||||
))}
|
||||
</SidebarContent>
|
||||
<SidebarFooter className="border-t-border/30 group-data-[collapsible=icon]:hidden border-t px-3 py-2">
|
||||
<div className="text-muted-foreground flex flex-col gap-0.5 text-[11px] leading-4">
|
||||
<div className="truncate" title={versionText}>
|
||||
<span className="text-foreground/80">{t("footer.version")}:</span>{" "}
|
||||
{versionText}
|
||||
</div>
|
||||
{versionInfo?.git_commit && (
|
||||
<div className="truncate" title={versionInfo.git_commit}>
|
||||
<span className="text-foreground/80">{t("footer.commit")}:</span>{" "}
|
||||
{versionInfo.git_commit}
|
||||
</div>
|
||||
)}
|
||||
{versionInfo?.build_time && (
|
||||
<div className="truncate" title={versionInfo.build_time}>
|
||||
<span className="text-foreground/80">{t("footer.build")}:</span>{" "}
|
||||
{versionInfo.build_time}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SidebarFooter>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -79,6 +79,12 @@
|
|||
"labels": {
|
||||
"loading": "Loading..."
|
||||
},
|
||||
"footer": {
|
||||
"version": "Version",
|
||||
"commit": "Commit",
|
||||
"build": "Build",
|
||||
"version_unknown": "Unknown"
|
||||
},
|
||||
"credentials": {
|
||||
"description": "Manage OAuth and token-based credentials for supported providers.",
|
||||
"loading": "Loading credentials...",
|
||||
|
|
|
|||
|
|
@ -79,6 +79,12 @@
|
|||
"labels": {
|
||||
"loading": "加载中..."
|
||||
},
|
||||
"footer": {
|
||||
"version": "版本",
|
||||
"commit": "提交",
|
||||
"build": "构建",
|
||||
"version_unknown": "未知"
|
||||
},
|
||||
"credentials": {
|
||||
"description": "管理已支持服务商的 OAuth 与 Token 凭据。",
|
||||
"loading": "正在加载凭据...",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue