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)
|
// Launcher service parameters (port/public)
|
||||||
h.registerLauncherConfigRoutes(mux)
|
h.registerLauncherConfigRoutes(mux)
|
||||||
|
|
||||||
|
// Runtime build/version metadata
|
||||||
|
h.registerVersionRoutes(mux)
|
||||||
|
|
||||||
// WeChat QR login flow
|
// WeChat QR login flow
|
||||||
h.registerWeixinRoutes(mux)
|
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[]
|
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> {
|
async function request<T>(path: string, options?: RequestInit): Promise<T> {
|
||||||
const res = await fetch(path, options)
|
const res = await fetch(path, options)
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
|
@ -60,3 +67,7 @@ export async function setLauncherConfig(
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getSystemVersionInfo(): Promise<SystemVersionInfo> {
|
||||||
|
return request<SystemVersionInfo>("/api/system/version")
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,12 @@ import {
|
||||||
IconSparkles,
|
IconSparkles,
|
||||||
IconTools,
|
IconTools,
|
||||||
} from "@tabler/icons-react"
|
} from "@tabler/icons-react"
|
||||||
|
import { useQuery } from "@tanstack/react-query"
|
||||||
import { Link, useRouterState } from "@tanstack/react-router"
|
import { Link, useRouterState } from "@tanstack/react-router"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
import { getSystemVersionInfo } from "@/api/system"
|
||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
|
|
@ -27,6 +29,7 @@ import {
|
||||||
SidebarGroupLabel,
|
SidebarGroupLabel,
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
|
SidebarFooter,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
SidebarRail,
|
SidebarRail,
|
||||||
} from "@/components/ui/sidebar"
|
} from "@/components/ui/sidebar"
|
||||||
|
|
@ -78,6 +81,13 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||||
language: (i18n.resolvedLanguage ?? i18n.language ?? "").toLowerCase(),
|
language: (i18n.resolvedLanguage ?? i18n.language ?? "").toLowerCase(),
|
||||||
t,
|
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(() => {
|
const navGroups: NavGroup[] = React.useMemo(() => {
|
||||||
return [
|
return [
|
||||||
|
|
@ -235,6 +245,26 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
))}
|
))}
|
||||||
</SidebarContent>
|
</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 />
|
<SidebarRail />
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,12 @@
|
||||||
"labels": {
|
"labels": {
|
||||||
"loading": "Loading..."
|
"loading": "Loading..."
|
||||||
},
|
},
|
||||||
|
"footer": {
|
||||||
|
"version": "Version",
|
||||||
|
"commit": "Commit",
|
||||||
|
"build": "Build",
|
||||||
|
"version_unknown": "Unknown"
|
||||||
|
},
|
||||||
"credentials": {
|
"credentials": {
|
||||||
"description": "Manage OAuth and token-based credentials for supported providers.",
|
"description": "Manage OAuth and token-based credentials for supported providers.",
|
||||||
"loading": "Loading credentials...",
|
"loading": "Loading credentials...",
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,12 @@
|
||||||
"labels": {
|
"labels": {
|
||||||
"loading": "加载中..."
|
"loading": "加载中..."
|
||||||
},
|
},
|
||||||
|
"footer": {
|
||||||
|
"version": "版本",
|
||||||
|
"commit": "提交",
|
||||||
|
"build": "构建",
|
||||||
|
"version_unknown": "未知"
|
||||||
|
},
|
||||||
"credentials": {
|
"credentials": {
|
||||||
"description": "管理已支持服务商的 OAuth 与 Token 凭据。",
|
"description": "管理已支持服务商的 OAuth 与 Token 凭据。",
|
||||||
"loading": "正在加载凭据...",
|
"loading": "正在加载凭据...",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue