Skip to content

Commit ae28bd7

Browse files
d3nn3s08claude
andcommitted
Add update notification: Version tab in settings + banner
- VERSION file (0.1.6) als Basis für Versionsvergleich - /api/version/check und /api/version/current Endpoints - Update-Kanal (stable/beta) in DB-Settings speicherbar - settings.html: neuer "Version" Tab mit Kanal-Auswahl + Update-Button - layout.html: dezenter Banner wenn neue Version verfügbar (dismissible) - settings.js: update_channel Radio-Handler Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4d2e371 commit ae28bd7

7 files changed

Lines changed: 290 additions & 28 deletions

File tree

VERSION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
0.1.6

app/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ def init_admin():
116116
from app.routes.monitoring_routes import router as monitoring_router
117117
from app.routes.bambu_cloud_routes import router as bambu_cloud_router
118118
from app.routes.mmu_routes import router as mmu_router # Happy Hare MMU
119+
from app.routes.version_routes import router as version_router # Update-Check
119120

120121
from app.websocket.log_stream import stream_log
121122
from sqlmodel import Session, select
@@ -664,6 +665,7 @@ async def get_response(self, path: str, scope):
664665
app.include_router(externe_spule_router)
665666
app.include_router(bambu_cloud_router) # Bambu Cloud Integration
666667
app.include_router(mmu_router) # Happy Hare MMU Integration
668+
app.include_router(version_router) # Update-Check
667669

668670

669671
# -----------------------------------------------------

app/routes/settings_routes.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"enable_file_selection_dialog": "false",
3535
"enable_multi_color_tracking": "true",
3636
"enable_ftp_gcode_download": "true",
37+
"update_channel": "stable",
3738
}
3839
PRO_CONFIG_DEFAULTS = {
3940
"debug.config.debug_logging_enabled": "false",
@@ -189,6 +190,7 @@ def get_settings(_: None = Depends(admin_required), session: Session = Depends(g
189190
"enable_file_selection_dialog": _normalize_bool(enable_file_selection_dialog, default=False),
190191
"enable_multi_color_tracking": _normalize_bool(enable_multi_color_tracking, default=True),
191192
"enable_ftp_gcode_download": _normalize_bool(enable_ftp_gcode_download, default=True),
193+
"update_channel": get_setting(session, "update_channel", DEFAULTS["update_channel"]) or DEFAULTS["update_channel"],
192194
"debug.config.debug_logging_enabled": _normalize_bool(debug_logging_enabled, default=False),
193195
"debug.config.latency_warning_threshold_ms": _normalize_int(
194196
latency_warning_threshold,
@@ -234,6 +236,7 @@ async def update_settings(payload: dict, _: None = Depends(admin_required), sess
234236
"enable_file_selection_dialog",
235237
"enable_multi_color_tracking",
236238
"enable_ftp_gcode_download",
239+
"update_channel",
237240
}
238241
if not any(k in payload for k in allowed_keys):
239242
raise HTTPException(status_code=400, detail="Keine gueltigen Settings uebergeben.")
@@ -402,4 +405,10 @@ async def update_settings(payload: dict, _: None = Depends(admin_required), sess
402405
normalized = "true" if str(val).lower() in TRUE_VALUES else "false"
403406
set_setting(session, "enable_ftp_gcode_download", normalized)
404407

408+
if "update_channel" in payload:
409+
channel = str(payload.get("update_channel", "")).lower()
410+
if channel not in {"stable", "beta"}:
411+
raise HTTPException(status_code=400, detail="update_channel muss stable oder beta sein.")
412+
set_setting(session, "update_channel", channel)
413+
405414
return get_settings(session)

app/routes/version_routes.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"""
2+
Update-Check Route
3+
==================
4+
GET /api/version/check
5+
→ Vergleicht laufende Version mit der aktuellen GitHub-Version.
6+
→ Kanal (stable/beta) wird aus der DB-Einstellung "update_channel" gelesen.
7+
→ Cached 6h damit GitHub nicht zugespammt wird.
8+
9+
GET /api/version/current
10+
→ Gibt nur die laufende Version zurück (kein GitHub-Request).
11+
"""
12+
13+
import logging
14+
import time
15+
from pathlib import Path
16+
17+
import httpx
18+
from fastapi import APIRouter, Depends
19+
from sqlmodel import Session
20+
21+
from app.database import get_session
22+
from app.routes.settings_routes import get_setting
23+
24+
logger = logging.getLogger("app")
25+
26+
router = APIRouter(prefix="/api/version", tags=["version"])
27+
28+
_CACHE_TTL = 6 * 3600
29+
_cache: dict = {"latest": None, "fetched_at": 0.0, "channel": None}
30+
31+
_VERSION_URLS = {
32+
"beta": "https://raw.githubusercontent.com/d3nn3s08/FilamentHub/beta/VERSION",
33+
"stable": "https://raw.githubusercontent.com/d3nn3s08/FilamentHub/main/VERSION",
34+
}
35+
36+
37+
def _read_current_version() -> str:
38+
try:
39+
p = Path(__file__).resolve().parent.parent.parent / "VERSION"
40+
return p.read_text(encoding="utf-8").strip()
41+
except Exception:
42+
return "0.0.0"
43+
44+
45+
def is_newer_version(current: str, latest: str) -> bool:
46+
"""True wenn latest > current (semver MAJOR.MINOR.PATCH)."""
47+
try:
48+
def parts(v: str):
49+
return [int(x) for x in v.strip().split(".")]
50+
c, l = parts(current), parts(latest)
51+
max_len = max(len(c), len(l))
52+
c += [0] * (max_len - len(c))
53+
l += [0] * (max_len - len(l))
54+
return l > c
55+
except (ValueError, AttributeError):
56+
return False
57+
58+
59+
async def _fetch_latest(channel: str) -> str | None:
60+
now = time.time()
61+
if (
62+
_cache["latest"]
63+
and _cache["channel"] == channel
64+
and (now - _cache["fetched_at"]) < _CACHE_TTL
65+
):
66+
return _cache["latest"]
67+
68+
url = _VERSION_URLS.get(channel, _VERSION_URLS["stable"])
69+
try:
70+
async with httpx.AsyncClient() as client:
71+
resp = await client.get(url, timeout=5.0, follow_redirects=True)
72+
if resp.status_code == 200:
73+
version = resp.text.strip()
74+
_cache.update({"latest": version, "fetched_at": now, "channel": channel})
75+
return version
76+
except Exception as exc:
77+
logger.debug("[Version] GitHub-Check fehlgeschlagen: %s", exc)
78+
return None
79+
80+
81+
@router.get("/current")
82+
def get_current_version():
83+
return {"version": _read_current_version()}
84+
85+
86+
@router.get("/check")
87+
async def check_for_update(session: Session = Depends(get_session)):
88+
channel = get_setting(session, "update_channel", "stable") or "stable"
89+
current = _read_current_version()
90+
latest = await _fetch_latest(channel)
91+
92+
if latest is None:
93+
return {
94+
"current": current,
95+
"latest": None,
96+
"update_available": False,
97+
"channel": channel,
98+
"error": "GitHub nicht erreichbar",
99+
}
100+
101+
return {
102+
"current": current,
103+
"latest": latest,
104+
"update_available": is_newer_version(current, latest),
105+
"channel": channel,
106+
}

frontend/static/js/settings.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ function resolveUpdateSetting() {
2424
}
2525

2626
async function bindSettingsControls() {
27-
const amsRadios = document.querySelectorAll('input[name="settings_ams_mode"][data-setting="ams_mode"]');
27+
const amsRadios = document.querySelectorAll('input[name="settings_ams_mode"][data-setting="ams_mode"]');
2828
const debugCheckbox = document.querySelector('input[type="checkbox"][data-setting="debug_ws_logging"]');
29-
const debugStatus = document.getElementById("debugStatus");
30-
if (!amsRadios.length && !debugCheckbox) return;
29+
const channelRadios = document.querySelectorAll('input[name="update_channel"][data-setting="update_channel"]');
30+
const debugStatus = document.getElementById("debugStatus");
3131

3232
const fetchFn = resolveFetchSettings();
3333
const updateFn = resolveUpdateSetting();
@@ -43,6 +43,9 @@ async function bindSettingsControls() {
4343
debugStatus.textContent = `Status: ${debugCheckbox.checked ? "aktiv" : "inaktiv"}`;
4444
}
4545
}
46+
if (channelRadios.length && settings?.update_channel) {
47+
channelRadios.forEach(r => r.checked = r.value === settings.update_channel);
48+
}
4649
} catch (e) {
4750
console.warn("Settings laden fehlgeschlagen", e);
4851
if (debugStatus) debugStatus.textContent = "Status: Fehler beim Laden";
@@ -61,4 +64,11 @@ async function bindSettingsControls() {
6164
debugStatus.textContent = `Status: ${debugCheckbox.checked ? "aktiv" : "inaktiv"}`;
6265
}
6366
});
67+
68+
channelRadios.forEach(radio => {
69+
radio.addEventListener("change", async () => {
70+
if (!radio.checked) return;
71+
await updateFn({ update_channel: radio.value });
72+
});
73+
});
6474
}

frontend/templates/layout.html

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,28 @@
206206
{# Globale Benachrichtigungen auf allen Seiten #}
207207
<div id="alert-root" class="alert-container"></div>
208208

209+
{# Update-Banner #}
210+
<div id="fh-update-banner" style="display:none;background:rgba(255,193,101,.1);border-bottom:1px solid rgba(255,193,101,.3);padding:8px 20px;display:none;align-items:center;gap:12px;font-size:13px;color:#ffc165;">
211+
<span>&#x2197;</span>
212+
<span id="fh-update-text"></span>
213+
<a href="/settings" style="color:#ffc165;font-weight:600;text-decoration:underline;">Einstellungen</a>
214+
<button onclick="document.getElementById('fh-update-banner').style.display='none';localStorage.setItem('fh_dismissed_ver',document.getElementById('fh-update-text').dataset.ver||'');" style="margin-left:auto;background:none;border:none;color:#ffc165;cursor:pointer;font-size:16px;" title="Schließen">&#x2715;</button>
215+
</div>
216+
<script>
217+
(function(){
218+
fetch('/api/version/check').then(function(r){ return r.json(); }).then(function(d){
219+
if(!d.update_available) return;
220+
var dismissed = localStorage.getItem('fh_dismissed_ver');
221+
if(dismissed === d.latest) return;
222+
var banner = document.getElementById('fh-update-banner');
223+
var txt = document.getElementById('fh-update-text');
224+
txt.textContent = 'Neue Version verfügbar: ' + d.latest + ' (du hast ' + d.current + ')';
225+
txt.dataset.ver = d.latest;
226+
banner.style.display = 'flex';
227+
}).catch(function(){});
228+
})();
229+
</script>
230+
209231
<main class="content">
210232
{% block header %}
211233
{% if active_page != "debug" %}

frontend/templates/settings.html

Lines changed: 137 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,36 +3,148 @@
33
{% set page_subtitle = "Theme, Sprache und weitere Optionen" %}
44

55
{% block content %}
6-
<section class="card-grid">
7-
<div class="panel">
8-
<p class="eyebrow">AMS Settings</p>
9-
<div style="display:flex;flex-direction:column;gap:10px;">
10-
<label class="user-menu__option">
11-
<input type="radio" name="settings_ams_mode" value="single" data-setting="ams_mode">
12-
<span>Single AMS</span>
13-
</label>
6+
7+
<style>
8+
.settings-tabs { display:flex; gap:4px; margin-bottom:20px; border-bottom:1px solid rgba(255,255,255,.1); }
9+
.settings-tab { padding:8px 18px; border-radius:8px 8px 0 0; border:1px solid transparent; border-bottom:none; cursor:pointer; font-size:13px; color:var(--text-dim,#9ab4d8); background:transparent; transition:.15s; }
10+
.settings-tab:hover { color:#fff; background:rgba(255,255,255,.05); }
11+
.settings-tab.active { color:#fff; background:rgba(255,255,255,.08); border-color:rgba(255,255,255,.12); }
12+
.settings-pane { display:none; }
13+
.settings-pane.active { display:block; }
14+
.ver-row { display:flex; align-items:center; gap:12px; margin-bottom:16px; flex-wrap:wrap; }
15+
.ver-badge { padding:3px 10px; border-radius:20px; font-size:12px; font-weight:600; }
16+
.ver-badge.current { background:rgba(154,180,216,.15); color:#9ab4d8; }
17+
.ver-badge.ok { background:rgba(106,228,133,.15); color:#6ae485; }
18+
.ver-badge.upd { background:rgba(255,193,101,.15); color:#ffc165; }
19+
#verCheckBtn { padding:7px 16px; border-radius:8px; border:1px solid rgba(255,255,255,.15); background:rgba(255,255,255,.07); color:#fff; cursor:pointer; font-size:13px; }
20+
#verCheckBtn:hover { background:rgba(255,255,255,.12); }
21+
#verCheckBtn:disabled { opacity:.5; cursor:default; }
22+
#verResult { font-size:13px; color:var(--text-dim,#9ab4d8); min-height:20px; }
23+
</style>
24+
25+
<div class="settings-tabs">
26+
<button class="settings-tab active" data-tab="general">Allgemein</button>
27+
<button class="settings-tab" data-tab="version">Version</button>
28+
</div>
29+
30+
<div id="pane-general" class="settings-pane active">
31+
<section class="card-grid">
32+
<div class="panel">
33+
<p class="eyebrow">AMS Settings</p>
34+
<div style="display:flex;flex-direction:column;gap:10px;">
35+
<label class="user-menu__option">
36+
<input type="radio" name="settings_ams_mode" value="single" data-setting="ams_mode">
37+
<span>Single AMS</span>
38+
</label>
39+
<label class="user-menu__option">
40+
<input type="radio" name="settings_ams_mode" value="multi" data-setting="ams_mode">
41+
<span>Multi AMS (Experimental)</span>
42+
</label>
43+
</div>
44+
</div>
45+
<div class="panel">
46+
<p class="eyebrow">Debug</p>
1447
<label class="user-menu__option">
15-
<input type="radio" name="settings_ams_mode" value="multi" data-setting="ams_mode">
16-
<span>Multi AMS (Experimental)</span>
48+
<input type="checkbox" data-setting="debug_ws_logging">
49+
<span>WS JSON Logging aktivieren</span>
50+
<span class="tooltip-container">
51+
<span class="tooltip-icon" tabindex="0">i</span>
52+
<span class="tooltip-box">Loggt alle eingehenden WebSocket-Events als JSON zur Fehlersuche. Empfohlen nur für Debug-Zwecke.</span>
53+
</span>
1754
</label>
55+
<p class="subtitle" id="debugStatus">Status: unbekannt</p>
56+
<a class="user-menu__link" href="/debug">Zum Debug Center</a>
1857
</div>
19-
</div>
20-
<div class="panel">
21-
<p class="eyebrow">Debug</p>
22-
<label class="user-menu__option">
23-
<input type="checkbox" data-setting="debug_ws_logging">
24-
<span>WS JSON Logging aktivieren</span>
25-
<span class="tooltip-container">
26-
<span class="tooltip-icon" tabindex="0">i</span>
27-
<span class="tooltip-box">Loggt alle eingehenden WebSocket-Events als JSON zur Fehlersuche. Empfohlen nur für Debug-Zwecke.</span>
28-
</span>
29-
</label>
30-
<p class="subtitle" id="debugStatus">Status: unbekannt</p>
31-
<a class="user-menu__link" href="/debug">Zum Debug Center</a>
32-
</div>
33-
</section>
58+
</section>
59+
</div>
60+
61+
<div id="pane-version" class="settings-pane">
62+
<section class="card-grid">
63+
<div class="panel" style="grid-column:1/-1">
64+
<p class="eyebrow">Update-Kanal</p>
65+
<div style="display:flex;flex-direction:column;gap:10px;margin-bottom:20px;">
66+
<label class="user-menu__option">
67+
<input type="radio" name="update_channel" value="stable" data-setting="update_channel">
68+
<span>Stable <span style="color:var(--text-dim);font-size:12px">— getestete Releases (empfohlen)</span></span>
69+
</label>
70+
<label class="user-menu__option">
71+
<input type="radio" name="update_channel" value="beta" data-setting="update_channel">
72+
<span>Beta <span style="color:var(--text-dim);font-size:12px">— neue Features, kann Bugs enthalten</span></span>
73+
</label>
74+
</div>
75+
<p class="eyebrow">Version</p>
76+
<div class="ver-row">
77+
<span>Installiert:</span><span class="ver-badge current" id="verCurrent">lädt…</span>
78+
<span>Aktuell:</span><span class="ver-badge ok" id="verLatest"></span>
79+
</div>
80+
<div style="display:flex;gap:10px;align-items:center;margin-bottom:12px;">
81+
<button id="verCheckBtn">Auf Updates prüfen</button>
82+
<a style="color:#6ae485;font-size:13px;text-decoration:none" href="https://github.com/d3nn3s08/FilamentHub/releases" target="_blank">Changelog ↗</a>
83+
</div>
84+
<p id="verResult"></p>
85+
<p id="verUpdateLink" style="display:none;margin-top:6px;">
86+
<a style="color:#ffc165;font-size:13px;text-decoration:none" href="https://github.com/d3nn3s08/FilamentHub/releases" target="_blank">&#x2197; Zum Release →</a>
87+
</p>
88+
</div>
89+
</section>
90+
</div>
91+
3492
{% endblock %}
3593

3694
{% block extra_scripts %}
3795
<script src="{{ url_for('frontend_static', path='js/settings.js') }}"></script>
96+
<script>
97+
document.querySelectorAll('.settings-tab').forEach(function(btn) {
98+
btn.addEventListener('click', function() {
99+
document.querySelectorAll('.settings-tab').forEach(function(b) { b.classList.remove('active'); });
100+
document.querySelectorAll('.settings-pane').forEach(function(p) { p.classList.remove('active'); });
101+
btn.classList.add('active');
102+
document.getElementById('pane-' + btn.dataset.tab).classList.add('active');
103+
});
104+
});
105+
106+
async function loadVersionInfo() {
107+
try {
108+
var r = await fetch('/api/version/current');
109+
var d = await r.json();
110+
document.getElementById('verCurrent').textContent = d.version;
111+
} catch(e) {}
112+
}
113+
114+
document.getElementById('verCheckBtn').addEventListener('click', async function() {
115+
var btn = this;
116+
var result = document.getElementById('verResult');
117+
var latest = document.getElementById('verLatest');
118+
var updateLink = document.getElementById('verUpdateLink');
119+
btn.disabled = true;
120+
btn.textContent = 'Prüfe\u2026';
121+
result.textContent = '';
122+
updateLink.style.display = 'none';
123+
try {
124+
var r = await fetch('/api/version/check');
125+
var d = await r.json();
126+
document.getElementById('verCurrent').textContent = d.current;
127+
if (d.latest) {
128+
latest.textContent = d.latest;
129+
if (d.update_available) {
130+
latest.className = 'ver-badge upd';
131+
result.textContent = 'Update verfügbar!';
132+
updateLink.style.display = 'block';
133+
} else {
134+
latest.className = 'ver-badge ok';
135+
result.textContent = '\u2713 FilamentHub ist aktuell.';
136+
}
137+
} else {
138+
result.textContent = d.error || 'GitHub nicht erreichbar.';
139+
}
140+
} catch(e) {
141+
result.textContent = 'Fehler: ' + e.message;
142+
} finally {
143+
btn.disabled = false;
144+
btn.textContent = 'Auf Updates prüfen';
145+
}
146+
});
147+
148+
loadVersionInfo();
149+
</script>
38150
{% endblock %}

0 commit comments

Comments
 (0)