FEATURE (version): Reload frontend if faced version mismatch with backend

This commit is contained in:
Rostislav Dugin
2026-03-11 12:28:07 +03:00
parent 446b96c6c0
commit 05115047c3
9 changed files with 107 additions and 0 deletions

View File

@@ -29,6 +29,7 @@ import (
"databasus-backend/internal/features/restores/restoring"
"databasus-backend/internal/features/storages"
system_healthcheck "databasus-backend/internal/features/system/healthcheck"
system_version "databasus-backend/internal/features/system/version"
task_cancellation "databasus-backend/internal/features/tasks/cancellation"
users_controllers "databasus-backend/internal/features/users/controllers"
users_middleware "databasus-backend/internal/features/users/middleware"
@@ -210,6 +211,7 @@ func setUpRoutes(r *gin.Engine) {
userController := users_controllers.GetUserController()
userController.RegisterRoutes(v1)
system_healthcheck.GetHealthcheckController().RegisterRoutes(v1)
system_version.GetVersionController().RegisterRoutes(v1)
backups_controllers.GetBackupController().RegisterPublicRoutes(v1)
backups_controllers.GetPostgresWalBackupController().RegisterRoutes(v1)
databases.GetDatabaseController().RegisterPublicRoutes(v1)

View File

@@ -0,0 +1,30 @@
package system_version
import (
"net/http"
"os"
"github.com/gin-gonic/gin"
)
type VersionController struct{}
func (c *VersionController) RegisterRoutes(router *gin.RouterGroup) {
router.GET("/system/version", c.GetVersion)
}
// GetVersion
// @Summary Get application version
// @Description Returns the current application version
// @Tags system/version
// @Produce json
// @Success 200 {object} VersionResponse
// @Router /system/version [get]
func (c *VersionController) GetVersion(ctx *gin.Context) {
version := os.Getenv("APP_VERSION")
if version == "" {
version = "dev"
}
ctx.JSON(http.StatusOK, VersionResponse{Version: version})
}

View File

@@ -0,0 +1,7 @@
package system_version
var versionController = &VersionController{}
func GetVersionController() *VersionController {
return versionController
}

View File

@@ -0,0 +1,5 @@
package system_version
type VersionResponse struct {
Version string `json:"version"`
}

View File

@@ -3,6 +3,8 @@ import { useEffect, useState } from 'react';
import { BrowserRouter, Route } from 'react-router';
import { Routes } from 'react-router';
import { useVersionCheck } from './shared/hooks/useVersionCheck';
import { userApi } from './entity/users';
import { AuthPageComponent } from './pages/AuthPageComponent';
import { OAuthCallbackPage } from './pages/OAuthCallbackPage';
@@ -14,6 +16,8 @@ function AppContent() {
const [isAuthorized, setIsAuthorized] = useState(false);
const { resolvedTheme } = useTheme();
useVersionCheck();
useEffect(() => {
const isAuthorized = userApi.isAuthorized();
setIsAuthorized(isAuthorized);

View File

@@ -0,0 +1,14 @@
import { getApplicationServer } from '../../../constants';
import type { VersionResponse } from '../model/VersionResponse';
export const systemApi = {
async getVersion(): Promise<VersionResponse> {
const response = await fetch(`${getApplicationServer()}/api/v1/system/version`);
if (!response.ok) {
throw new Error(`Failed to fetch version: ${response.status}`);
}
return response.json();
},
};

View File

@@ -0,0 +1,2 @@
export { systemApi } from './api/systemApi';
export { type VersionResponse } from './model/VersionResponse';

View File

@@ -0,0 +1,3 @@
export type VersionResponse = {
version: string;
};

View File

@@ -0,0 +1,40 @@
import { useEffect } from 'react';
import { APP_VERSION } from '../../constants';
import { systemApi } from '../../entity/system';
const VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1000;
const RELOAD_COOLDOWN_MS = 10 * 1000;
const LAST_RELOAD_KEY = 'lastVersionReload';
export function useVersionCheck() {
useEffect(() => {
if (APP_VERSION === 'dev') {
return;
}
const checkVersion = async () => {
try {
const { version } = await systemApi.getVersion();
if (version && version !== APP_VERSION) {
const lastReload = Number(localStorage.getItem(LAST_RELOAD_KEY) || '0');
if (Date.now() - lastReload < RELOAD_COOLDOWN_MS) {
return;
}
localStorage.setItem(LAST_RELOAD_KEY, String(Date.now()));
window.location.reload();
}
} catch {
// Silently ignore errors — network issues shouldn't break the app
}
};
checkVersion();
const interval = setInterval(checkVersion, VERSION_CHECK_INTERVAL_MS);
return () => clearInterval(interval);
}, []);
}