mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-23 14:06:18 +01:00
feat(hub): show "update available" notification in hub web UI (#1830)
* refactor, make opt-in, and deprecate /api/beszel/getkey in favor of /api/beszel/info --------- Co-authored-by: henrygd <hank@henrygd.me>
This commit is contained in:
@@ -110,21 +110,13 @@ func (p *updater) update() (updated bool, err error) {
|
||||
}
|
||||
|
||||
var latest *release
|
||||
var useMirror bool
|
||||
|
||||
// Determine the API endpoint based on UseMirror flag
|
||||
apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", p.config.Owner, p.config.Repo)
|
||||
apiURL := getApiURL(p.config.UseMirror, p.config.Owner, p.config.Repo)
|
||||
if p.config.UseMirror {
|
||||
useMirror = true
|
||||
apiURL = fmt.Sprintf("https://gh.beszel.dev/repos/%s/%s/releases/latest?api=true", p.config.Owner, p.config.Repo)
|
||||
ColorPrint(ColorYellow, "Using mirror for update.")
|
||||
}
|
||||
|
||||
latest, err = fetchLatestRelease(
|
||||
p.config.Context,
|
||||
p.config.HttpClient,
|
||||
apiURL,
|
||||
)
|
||||
latest, err = FetchLatestRelease(p.config.Context, p.config.HttpClient, apiURL)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -150,7 +142,7 @@ func (p *updater) update() (updated bool, err error) {
|
||||
|
||||
// download the release asset
|
||||
assetPath := filepath.Join(releaseDir, asset.Name)
|
||||
if err := downloadFile(p.config.Context, p.config.HttpClient, asset.DownloadUrl, assetPath, useMirror); err != nil {
|
||||
if err := downloadFile(p.config.Context, p.config.HttpClient, asset.DownloadUrl, assetPath, p.config.UseMirror); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -226,11 +218,11 @@ func (p *updater) update() (updated bool, err error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func fetchLatestRelease(
|
||||
ctx context.Context,
|
||||
client HttpClient,
|
||||
url string,
|
||||
) (*release, error) {
|
||||
func FetchLatestRelease(ctx context.Context, client HttpClient, url string) (*release, error) {
|
||||
if url == "" {
|
||||
url = getApiURL(false, "henrygd", "beszel")
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -375,3 +367,10 @@ func isGlibc() bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getApiURL(useMirror bool, owner, repo string) string {
|
||||
if useMirror {
|
||||
return fmt.Sprintf("https://gh.beszel.dev/repos/%s/%s/releases/latest?api=true", owner, repo)
|
||||
}
|
||||
return fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", owner, repo)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
@@ -14,8 +15,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/blang/semver"
|
||||
"github.com/henrygd/beszel"
|
||||
"github.com/henrygd/beszel/internal/alerts"
|
||||
"github.com/henrygd/beszel/internal/ghupdate"
|
||||
"github.com/henrygd/beszel/internal/hub/config"
|
||||
"github.com/henrygd/beszel/internal/hub/heartbeat"
|
||||
"github.com/henrygd/beszel/internal/hub/systems"
|
||||
@@ -191,6 +194,36 @@ func (h *Hub) registerMiddlewares(se *core.ServeEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
type UpdateInfo struct {
|
||||
lastCheck time.Time
|
||||
Version string `json:"v"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
func (info *UpdateInfo) getUpdate(e *core.RequestEvent) error {
|
||||
if time.Since(info.lastCheck) < 6*time.Hour {
|
||||
return e.JSON(http.StatusOK, info)
|
||||
}
|
||||
latestRelease, err := ghupdate.FetchLatestRelease(context.Background(), http.DefaultClient, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
currentVersion, err := semver.Parse(strings.TrimPrefix(beszel.Version, "v"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
latestVersion, err := semver.Parse(strings.TrimPrefix(latestRelease.Tag, "v"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info.lastCheck = time.Now()
|
||||
if latestVersion.GT(currentVersion) {
|
||||
info.Version = strings.TrimPrefix(latestRelease.Tag, "v")
|
||||
info.Url = latestRelease.Url
|
||||
}
|
||||
return e.JSON(http.StatusOK, info)
|
||||
}
|
||||
|
||||
// registerApiRoutes registers custom API routes
|
||||
func (h *Hub) registerApiRoutes(se *core.ServeEvent) error {
|
||||
// auth protected routes
|
||||
@@ -209,9 +242,13 @@ func (h *Hub) registerApiRoutes(se *core.ServeEvent) error {
|
||||
return e.JSON(http.StatusOK, map[string]bool{"firstRun": err == nil && total == 0})
|
||||
})
|
||||
// get public key and version
|
||||
apiAuth.GET("/getkey", func(e *core.RequestEvent) error {
|
||||
return e.JSON(http.StatusOK, map[string]string{"key": h.pubKey, "v": beszel.Version})
|
||||
})
|
||||
apiAuth.GET("/info", h.getInfo)
|
||||
apiAuth.GET("/getkey", h.getInfo) // deprecated - keep for compatibility w/ integrations
|
||||
// check for updates
|
||||
if optIn, _ := GetEnv("CHECK_UPDATES"); optIn == "true" {
|
||||
var updateInfo UpdateInfo
|
||||
apiAuth.GET("/update", updateInfo.getUpdate)
|
||||
}
|
||||
// send test notification
|
||||
apiAuth.POST("/test-notification", h.SendTestNotification)
|
||||
// heartbeat status and test
|
||||
@@ -240,6 +277,23 @@ func (h *Hub) registerApiRoutes(se *core.ServeEvent) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// getInfo returns data needed by authenticated users, such as the public key and current version
|
||||
func (h *Hub) getInfo(e *core.RequestEvent) error {
|
||||
type infoResponse struct {
|
||||
Key string `json:"key"`
|
||||
Version string `json:"v"`
|
||||
CheckUpdate bool `json:"cu"`
|
||||
}
|
||||
info := infoResponse{
|
||||
Key: h.pubKey,
|
||||
Version: beszel.Version,
|
||||
}
|
||||
if optIn, _ := GetEnv("CHECK_UPDATES"); optIn == "true" {
|
||||
info.CheckUpdate = true
|
||||
}
|
||||
return e.JSON(http.StatusOK, info)
|
||||
}
|
||||
|
||||
// GetUniversalToken handles the universal token API endpoint (create, read, delete)
|
||||
func (h *Hub) getUniversalToken(e *core.RequestEvent) error {
|
||||
tokenMap := universalTokenMap.GetMap()
|
||||
|
||||
@@ -606,6 +606,17 @@ func TestApiRoutesAuthentication(t *testing.T) {
|
||||
ExpectedContent: []string{"\"key\":", "\"v\":"},
|
||||
TestAppFactory: testAppFactory,
|
||||
},
|
||||
{
|
||||
Name: "GET /info - should return the same as /getkey",
|
||||
Method: http.MethodGet,
|
||||
URL: "/api/beszel/info",
|
||||
Headers: map[string]string{
|
||||
"Authorization": userToken,
|
||||
},
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{"\"key\":", "\"v\":"},
|
||||
TestAppFactory: testAppFactory,
|
||||
},
|
||||
{
|
||||
Name: "GET /first-run - no auth should succeed",
|
||||
Method: http.MethodGet,
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { GithubIcon } from "lucide-react"
|
||||
import { $newVersion } from "@/lib/stores"
|
||||
import { Separator } from "./ui/separator"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
|
||||
export function FooterRepoLink() {
|
||||
const newVersion = useStore($newVersion)
|
||||
return (
|
||||
<div className="flex gap-1.5 justify-end items-center pe-3 sm:pe-6 mt-3.5 mb-4 text-xs opacity-80">
|
||||
<a
|
||||
@@ -21,6 +25,19 @@ export function FooterRepoLink() {
|
||||
>
|
||||
Beszel {globalThis.BESZEL.HUB_VERSION}
|
||||
</a>
|
||||
{newVersion?.v && (
|
||||
<>
|
||||
<Separator orientation="vertical" className="h-2.5 bg-muted-foreground opacity-70" />
|
||||
<a
|
||||
href={newVersion.url}
|
||||
target="_blank"
|
||||
className="text-yellow-500 hover:text-yellow-400 duration-75"
|
||||
rel="noopener"
|
||||
>
|
||||
<Trans context="New version available">{newVersion.v} available</Trans>
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { atom, computed, listenKeys, map, type ReadableAtom } from "nanostores"
|
||||
import type { AlertMap, ChartTimes, SystemRecord, UserSettings } from "@/types"
|
||||
import type { AlertMap, ChartTimes, SystemRecord, UpdateInfo, UserSettings } from "@/types"
|
||||
import { pb } from "./api"
|
||||
import { Unit } from "./enums"
|
||||
|
||||
@@ -28,6 +28,9 @@ export const $alerts = map<AlertMap>({})
|
||||
/** SSH public key */
|
||||
export const $publicKey = atom("")
|
||||
|
||||
/** New version info if an update is available, otherwise undefined */
|
||||
export const $newVersion = atom<UpdateInfo | undefined>()
|
||||
|
||||
/** Chart time period */
|
||||
export const $chartTime = atom<ChartTimes>("1h")
|
||||
|
||||
|
||||
@@ -12,17 +12,19 @@ import Settings from "@/components/routes/settings/layout.tsx"
|
||||
import { ThemeProvider } from "@/components/theme-provider.tsx"
|
||||
import { Toaster } from "@/components/ui/toaster.tsx"
|
||||
import { alertManager } from "@/lib/alerts"
|
||||
import { pb, updateUserSettings } from "@/lib/api.ts"
|
||||
import { isAdmin, pb, updateUserSettings } from "@/lib/api.ts"
|
||||
import { dynamicActivate, getLocale } from "@/lib/i18n"
|
||||
import {
|
||||
$authenticated,
|
||||
$copyContent,
|
||||
$direction,
|
||||
$newVersion,
|
||||
$publicKey,
|
||||
$userSettings,
|
||||
defaultLayoutWidth,
|
||||
} from "@/lib/stores.ts"
|
||||
import * as systemsManager from "@/lib/systemsManager.ts"
|
||||
import type { BeszelInfo, UpdateInfo } from "./types"
|
||||
|
||||
const LoginPage = lazy(() => import("@/components/login/login.tsx"))
|
||||
const Home = lazy(() => import("@/components/routes/home.tsx"))
|
||||
@@ -39,9 +41,13 @@ const App = memo(() => {
|
||||
pb.authStore.onChange(() => {
|
||||
$authenticated.set(pb.authStore.isValid)
|
||||
})
|
||||
// get version / public key
|
||||
pb.send("/api/beszel/getkey", {}).then((data) => {
|
||||
// get general info for authenticated users, such as public key and version
|
||||
pb.send<BeszelInfo>("/api/beszel/info", {}).then((data) => {
|
||||
$publicKey.set(data.key)
|
||||
// check for updates if enabled
|
||||
if (data.cu && isAdmin()) {
|
||||
pb.send<UpdateInfo>("/api/beszel/update", {}).then($newVersion.set)
|
||||
}
|
||||
})
|
||||
// get user settings
|
||||
updateUserSettings()
|
||||
|
||||
13
internal/site/src/types.d.ts
vendored
13
internal/site/src/types.d.ts
vendored
@@ -525,4 +525,15 @@ export interface SystemdServiceDetails {
|
||||
WantedBy: any[];
|
||||
Wants: string[];
|
||||
WantsMountsFor: any[];
|
||||
}
|
||||
}
|
||||
|
||||
export interface BeszelInfo {
|
||||
key: string // public key
|
||||
v: string // version
|
||||
cu: boolean // check updates
|
||||
}
|
||||
|
||||
export interface UpdateInfo {
|
||||
v: string // new version
|
||||
url: string // url to new version
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user