Implement version check on load, if the footer has data-update-check=true,it fetches /api/version and sets the version badge. No request is made when update check is disabled.

This commit is contained in:
2026-02-02 20:46:55 +01:00
parent f00c1c1b22
commit b3e32fd5c1
11 changed files with 157 additions and 11 deletions

View File

@@ -390,6 +390,9 @@
"auth.logout": "Abmelden", "auth.logout": "Abmelden",
"auth.user_info": "Benutzerinformationen", "auth.user_info": "Benutzerinformationen",
"auth.session_expired": "Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.", "auth.session_expired": "Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.",
"auth.login_required": "Authentifizierung erforderlich" "auth.login_required": "Authentifizierung erforderlich",
"footer.version": "Fail2ban-UI v{version}",
"footer.latest": "Aktuell",
"footer.update_available": "Update verfügbar: v{version}"
} }

View File

@@ -390,6 +390,9 @@
"auth.logout": "Abmäudä", "auth.logout": "Abmäudä",
"auth.user_info": "Benutzerinformationä", "auth.user_info": "Benutzerinformationä",
"auth.session_expired": "Ihri Sitzig isch abglaufä. Bitte mäudä di erneut a.", "auth.session_expired": "Ihri Sitzig isch abglaufä. Bitte mäudä di erneut a.",
"auth.login_required": "Authentifizierig erforderlich" "auth.login_required": "Authentifizierig erforderlich",
"footer.version": "Fail2ban-UI v{version}",
"footer.latest": "Aktuell",
"footer.update_available": "Update verfüegbar: v{version}"
} }

View File

@@ -390,6 +390,9 @@
"auth.logout": "Logout", "auth.logout": "Logout",
"auth.user_info": "User Information", "auth.user_info": "User Information",
"auth.session_expired": "Your session has expired. Please log in again.", "auth.session_expired": "Your session has expired. Please log in again.",
"auth.login_required": "Authentication required" "auth.login_required": "Authentication required",
"footer.version": "Fail2ban-UI v{version}",
"footer.latest": "Latest",
"footer.update_available": "Update available: v{version}"
} }

View File

@@ -390,5 +390,8 @@
"auth.logout": "Cerrar sesión", "auth.logout": "Cerrar sesión",
"auth.user_info": "Información del usuario", "auth.user_info": "Información del usuario",
"auth.session_expired": "Su sesión ha expirado. Por favor, inicie sesión nuevamente.", "auth.session_expired": "Su sesión ha expirado. Por favor, inicie sesión nuevamente.",
"auth.login_required": "Autenticación requerida" "auth.login_required": "Autenticación requerida",
"footer.version": "Fail2ban-UI v{version}",
"footer.latest": "Actual",
"footer.update_available": "Actualización disponible: v{version}"
} }

View File

@@ -390,5 +390,8 @@
"auth.logout": "Déconnexion", "auth.logout": "Déconnexion",
"auth.user_info": "Informations utilisateur", "auth.user_info": "Informations utilisateur",
"auth.session_expired": "Votre session a expiré. Veuillez vous reconnecter.", "auth.session_expired": "Votre session a expiré. Veuillez vous reconnecter.",
"auth.login_required": "Authentification requise" "auth.login_required": "Authentification requise",
"footer.version": "Fail2ban-UI v{version}",
"footer.latest": "À jour",
"footer.update_available": "Mise à jour disponible : v{version}"
} }

View File

@@ -390,5 +390,8 @@
"auth.logout": "Esci", "auth.logout": "Esci",
"auth.user_info": "Informazioni utente", "auth.user_info": "Informazioni utente",
"auth.session_expired": "La tua sessione è scaduta. Si prega di accedere nuovamente.", "auth.session_expired": "La tua sessione è scaduta. Si prega di accedere nuovamente.",
"auth.login_required": "Autenticazione richiesta" "auth.login_required": "Autenticazione richiesta",
"footer.version": "Fail2ban-UI v{version}",
"footer.latest": "Aggiornato",
"footer.update_available": "Aggiornamento disponibile: v{version}"
} }

View File

@@ -0,0 +1,20 @@
// Fail2ban UI - A Swiss made, management interface for Fail2ban.
//
// Copyright (C) 2025 Swissmakers GmbH (https://swissmakers.ch)
//
// Licensed under the GNU General Public License, Version 3 (GPL-3.0)
// You may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.gnu.org/licenses/gpl-3.0.en.html
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package version
// Version is the current Fail2ban-UI version (in best case it matches the latest release tag on GitHub).
const Version = "1.3.6"

View File

@@ -50,6 +50,7 @@ import (
"github.com/swissmakers/fail2ban-ui/internal/fail2ban" "github.com/swissmakers/fail2ban-ui/internal/fail2ban"
"github.com/swissmakers/fail2ban-ui/internal/integrations" "github.com/swissmakers/fail2ban-ui/internal/integrations"
"github.com/swissmakers/fail2ban-ui/internal/storage" "github.com/swissmakers/fail2ban-ui/internal/storage"
"github.com/swissmakers/fail2ban-ui/internal/version"
) )
// wsHub is the global WebSocket hub instance // wsHub is the global WebSocket hub instance
@@ -1141,15 +1142,97 @@ func renderIndexPage(c *gin.Context) {
} }
} }
// Update check: default true; set UPDATE_CHECK=false to disable external GitHub request
updateCheckEnabled := os.Getenv("UPDATE_CHECK") != "false"
c.HTML(http.StatusOK, "index.html", gin.H{ c.HTML(http.StatusOK, "index.html", gin.H{
"timestamp": time.Now().Format(time.RFC1123), "timestamp": time.Now().Format(time.RFC1123),
"version": time.Now().Unix(), "version": time.Now().Unix(),
"appVersion": version.Version,
"updateCheckEnabled": updateCheckEnabled,
"disableExternalIP": disableExternalIP, "disableExternalIP": disableExternalIP,
"oidcEnabled": oidcEnabled, "oidcEnabled": oidcEnabled,
"skipLoginPage": skipLoginPage, "skipLoginPage": skipLoginPage,
}) })
} }
// githubReleaseResponse is used to parse the GitHub releases/latest API response
type githubReleaseResponse struct {
TagName string `json:"tag_name"`
}
// versionLess returns true if a < b (e.g. "1.3.5" < "1.3.6")
func versionLess(a, b string) bool {
parse := func(s string) []int {
s = strings.TrimPrefix(strings.TrimSpace(s), "v")
parts := strings.Split(s, ".")
out := make([]int, 0, len(parts))
for _, p := range parts {
n, _ := strconv.Atoi(p)
out = append(out, n)
}
return out
}
pa, pb := parse(a), parse(b)
for i := 0; i < len(pa) || i < len(pb); i++ {
va, vb := 0, 0
if i < len(pa) {
va = pa[i]
}
if i < len(pb) {
vb = pb[i]
}
if va < vb {
return true
}
if va > vb {
return false
}
}
return false
}
// GetVersionHandler returns the current app version and optionally the latest GitHub release.
// UPDATE_CHECK=false disables the external request to GitHub.
func GetVersionHandler(c *gin.Context) {
updateCheckEnabled := os.Getenv("UPDATE_CHECK") != "false"
out := gin.H{
"version": version.Version,
"update_check_enabled": updateCheckEnabled,
}
if !updateCheckEnabled {
c.JSON(http.StatusOK, out)
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 8*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.github.com/repos/swissmakers/fail2ban-ui/releases/latest", nil)
if err != nil {
c.JSON(http.StatusOK, out)
return
}
req.Header.Set("Accept", "application/vnd.github.v3+json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
c.JSON(http.StatusOK, out)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
c.JSON(http.StatusOK, out)
return
}
var gh githubReleaseResponse
if err := json.NewDecoder(resp.Body).Decode(&gh); err != nil {
c.JSON(http.StatusOK, out)
return
}
latest := strings.TrimPrefix(strings.TrimSpace(gh.TagName), "v")
out["latest_version"] = latest
out["update_available"] = versionLess(version.Version, latest)
c.JSON(http.StatusOK, out)
}
// GetJailFilterConfigHandler returns both the filter config and jail config for a given jail // GetJailFilterConfigHandler returns both the filter config and jail config for a given jail
func GetJailFilterConfigHandler(c *gin.Context) { func GetJailFilterConfigHandler(c *gin.Context) {
config.DebugLog("----------------------------") config.DebugLog("----------------------------")

View File

@@ -58,6 +58,9 @@ func RegisterRoutes(r *gin.Engine, hub *Hub) {
api.POST("/jails", CreateJailHandler) api.POST("/jails", CreateJailHandler)
api.DELETE("/jails/:jail", DeleteJailHandler) api.DELETE("/jails/:jail", DeleteJailHandler)
// Version and update check (only on page load; UPDATE_CHECK=false disables GitHub request)
api.GET("/version", GetVersionHandler)
// Settings endpoints // Settings endpoints
api.GET("/settings", GetSettingsHandler) api.GET("/settings", GetSettingsHandler)
api.POST("/settings", UpdateSettingsHandler) api.POST("/settings", UpdateSettingsHandler)

View File

@@ -81,6 +81,26 @@ function initializeApp() {
console.warn('Could not check LOTR on load:', err); console.warn('Could not check LOTR on load:', err);
}); });
// Version and update check: only on page load; UPDATE_CHECK=false disables external GitHub request
var versionContainer = document.getElementById('version-badge-container');
if (versionContainer && versionContainer.getAttribute('data-update-check') === 'true') {
fetch('/api/version')
.then(function(res) { return res.json(); })
.then(function(data) {
if (!data.update_check_enabled || versionContainer.innerHTML) return;
var latestLabel = typeof t === 'function' ? t('footer.latest', 'Latest') : 'Latest';
var updateHint = (typeof t === 'function' && translations && translations['footer.update_available'])
? translations['footer.update_available'].replace('{version}', data.latest_version || '')
: ('Update available: v' + (data.latest_version || ''));
if (data.update_available && data.latest_version) {
versionContainer.innerHTML = '<a href="https://github.com/swissmakers/fail2ban-ui/releases" target="_blank" rel="noopener" class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-800 hover:bg-amber-200" title="' + updateHint + '">' + updateHint + '</a>';
} else {
versionContainer.innerHTML = '<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800" title="' + latestLabel + '">' + latestLabel + '</span>';
}
})
.catch(function() { /* ignore; no badge on error */ });
}
Promise.all([ Promise.all([
loadServers(), loadServers(),
getTranslationsSettingsOnPageload() getTranslationsSettingsOnPageload()

View File

@@ -949,6 +949,8 @@
<footer id="footer" class="hidden bg-gray-100 py-4"> <footer id="footer" class="hidden bg-gray-100 py-4">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center text-gray-600 text-sm"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center text-gray-600 text-sm">
<p class="mb-0"> <p class="mb-0">
Fail2ban-UI v<span id="footer-app-version">{{.appVersion}}</span>
<span id="version-badge-container" class="ml-2" data-update-check="{{.updateCheckEnabled}}"></span>
&copy; <a href="https://swissmakers.ch" target="_blank" class="text-blue-600 hover:text-blue-800">Swissmakers GmbH</a> &copy; <a href="https://swissmakers.ch" target="_blank" class="text-blue-600 hover:text-blue-800">Swissmakers GmbH</a>
- -
<a href="https://github.com/swissmakers/fail2ban-ui" target="_blank" class="text-blue-600 hover:text-blue-800">GitHub</a> <a href="https://github.com/swissmakers/fail2ban-ui" target="_blank" class="text-blue-600 hover:text-blue-800">GitHub</a>