Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ede0f0a3c | |||
| 8040973227 | |||
| 3cdc82f919 | |||
| 40595fc381 | |||
| 9ace554427 | |||
| f48c851ef0 |
32
app/main.py
32
app/main.py
@@ -90,16 +90,36 @@ STATIC_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "static")
|
|||||||
if os.path.isdir(STATIC_DIR):
|
if os.path.isdir(STATIC_DIR):
|
||||||
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
||||||
|
|
||||||
# Serve index.html at root
|
# Serve index.html at root — inject cache-busting version into static asset URLs
|
||||||
from fastapi.responses import FileResponse
|
# so the browser always loads fresh JS/CSS after a container update.
|
||||||
|
from fastapi.responses import FileResponse, HTMLResponse
|
||||||
|
from app.services import update_service
|
||||||
|
|
||||||
|
_STATIC_ASSETS = (
|
||||||
|
'"/static/js/app.js"',
|
||||||
|
'"/static/js/i18n.js"',
|
||||||
|
'"/static/css/styles.css"',
|
||||||
|
)
|
||||||
|
|
||||||
|
def _cache_bust_index(html: str, version: str) -> str:
|
||||||
|
# Inject version as a global JS variable so i18n.js can bust lang file caches too
|
||||||
|
html = html.replace("</head>", f'<script>window.STATIC_VERSION="{version}";</script>\n</head>', 1)
|
||||||
|
for asset in _STATIC_ASSETS:
|
||||||
|
busted = asset.rstrip('"') + f'?v={version}"'
|
||||||
|
html = html.replace(asset, busted)
|
||||||
|
return html
|
||||||
|
|
||||||
|
|
||||||
@app.get("/", include_in_schema=False)
|
@app.get("/", include_in_schema=False)
|
||||||
async def serve_index():
|
async def serve_index():
|
||||||
"""Serve the main dashboard."""
|
"""Serve the main dashboard with cache-busted static asset URLs."""
|
||||||
index_path = os.path.join(STATIC_DIR, "index.html")
|
index_path = os.path.join(STATIC_DIR, "index.html")
|
||||||
if os.path.isfile(index_path):
|
if not os.path.isfile(index_path):
|
||||||
return FileResponse(index_path)
|
return JSONResponse({"message": "NetBird MSP Appliance API is running."})
|
||||||
return JSONResponse({"message": "NetBird MSP Appliance API is running."})
|
version = update_service.get_current_version().get("commit", "unknown")
|
||||||
|
html = open(index_path, encoding="utf-8").read()
|
||||||
|
html = _cache_bust_index(html, version)
|
||||||
|
return HTMLResponse(content=html, headers={"Cache-Control": "no-cache"})
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -669,9 +669,9 @@ async function confirmDeleteCustomer() {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Customer Actions (start/stop/restart/deploy)
|
// Customer Actions (start/stop/restart/deploy)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
async function customerAction(id, action) {
|
async function customerAction(id, action, name) {
|
||||||
if (action === 'deploy') {
|
if (action === 'deploy') {
|
||||||
showRedeployModal(id);
|
showRedeployModal(id, name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -683,9 +683,12 @@ async function customerAction(id, action) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showRedeployModal(id) {
|
function showRedeployModal(id, name) {
|
||||||
const row = document.querySelector(`tr[data-customer-id="${id}"]`);
|
// Prefer passed name, fallback to dashboard table row, then ID
|
||||||
const name = row ? row.querySelector('td')?.textContent?.trim() : `#${id}`;
|
if (!name) {
|
||||||
|
const row = document.querySelector(`tr[data-customer-id="${id}"]`);
|
||||||
|
name = row ? row.querySelector('td')?.textContent?.trim() : `#${id}`;
|
||||||
|
}
|
||||||
document.getElementById('redeploy-customer-id').value = id;
|
document.getElementById('redeploy-customer-id').value = id;
|
||||||
document.getElementById('redeploy-customer-name').textContent = name;
|
document.getElementById('redeploy-customer-name').textContent = name;
|
||||||
new bootstrap.Modal(document.getElementById('redeploy-modal')).show();
|
new bootstrap.Modal(document.getElementById('redeploy-modal')).show();
|
||||||
@@ -786,7 +789,7 @@ async function viewCustomer(id) {
|
|||||||
<button class="btn btn-success btn-sm me-1" onclick="customerAction(${id},'start')"><i class="bi bi-play-circle me-1"></i>${t('customer.start')}</button>
|
<button class="btn btn-success btn-sm me-1" onclick="customerAction(${id},'start')"><i class="bi bi-play-circle me-1"></i>${t('customer.start')}</button>
|
||||||
<button class="btn btn-warning btn-sm me-1" onclick="customerAction(${id},'stop')"><i class="bi bi-stop-circle me-1"></i>${t('customer.stop')}</button>
|
<button class="btn btn-warning btn-sm me-1" onclick="customerAction(${id},'stop')"><i class="bi bi-stop-circle me-1"></i>${t('customer.stop')}</button>
|
||||||
<button class="btn btn-info btn-sm me-1" onclick="customerAction(${id},'restart')"><i class="bi bi-arrow-repeat me-1"></i>${t('customer.restart')}</button>
|
<button class="btn btn-info btn-sm me-1" onclick="customerAction(${id},'restart')"><i class="bi bi-arrow-repeat me-1"></i>${t('customer.restart')}</button>
|
||||||
<button class="btn btn-outline-primary btn-sm me-1" onclick="customerAction(${id},'deploy')"><i class="bi bi-rocket me-1"></i>${t('customer.reDeploy')}</button>
|
<button class="btn btn-outline-primary btn-sm me-1" data-customer-name="${esc(data.name)}" onclick="customerAction(${id},'deploy',this.dataset.customerName)"><i class="bi bi-rocket me-1"></i>${t('customer.reDeploy')}</button>
|
||||||
<button class="btn btn-outline-warning btn-sm" id="btn-update-images-detail" onclick="updateCustomerImagesFromDetail(${id})">
|
<button class="btn btn-outline-warning btn-sm" id="btn-update-images-detail" onclick="updateCustomerImagesFromDetail(${id})">
|
||||||
<span id="update-detail-spinner" class="spinner-border spinner-border-sm d-none me-1"></span>
|
<span id="update-detail-spinner" class="spinner-border spinner-border-sm d-none me-1"></span>
|
||||||
<i class="bi bi-arrow-repeat me-1"></i>${t('customer.updateImages')}
|
<i class="bi bi-arrow-repeat me-1"></i>${t('customer.updateImages')}
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ function detectLanguage() {
|
|||||||
async function loadLanguage(lang) {
|
async function loadLanguage(lang) {
|
||||||
if (translations[lang]) return;
|
if (translations[lang]) return;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`/static/lang/${lang}.json`);
|
const v = window.STATIC_VERSION ? `?v=${window.STATIC_VERSION}` : '';
|
||||||
|
const resp = await fetch(`/static/lang/${lang}.json${v}`);
|
||||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||||
translations[lang] = await resp.json();
|
translations[lang] = await resp.json();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
Reference in New Issue
Block a user