Cleanup in js #16

Merged
gru merged 11 commits from cleanup_in_js into master 2026-06-02 23:02:29 +02:00
6 changed files with 73 additions and 31 deletions
Showing only changes of commit 62572ec273 - Show all commits
+3 -2
View File
@@ -16,7 +16,7 @@ from .config import (
SOCKETIO_CORS_ALLOWED_ORIGINS, SOCKETIO_CORS_ALLOWED_ORIGINS,
) )
from .db import init_db from .db import init_db
from .services.frontend_assets import asset_path, bootstrap_css_path, static_hash, validate_offline_assets from .services.frontend_assets import asset_path, bootstrap_css_path, initialize_static_hash, static_hash, validate_offline_assets
socketio = SocketIO(cors_allowed_origins=SOCKETIO_CORS_ALLOWED_ORIGINS, ping_timeout=30, async_mode="threading") socketio = SocketIO(cors_allowed_origins=SOCKETIO_CORS_ALLOWED_ORIGINS, ping_timeout=30, async_mode="threading")
@@ -56,6 +56,7 @@ def register_error_pages(app: Flask) -> None:
def create_app() -> Flask: def create_app() -> Flask:
validate_offline_assets() validate_offline_assets()
app = Flask(__name__) app = Flask(__name__)
initialize_static_hash(Path(app.static_folder or ""))
from .logging_config import configure_logging from .logging_config import configure_logging
configure_logging(app) configure_logging(app)
if PROXY_FIX_ENABLE: if PROXY_FIX_ENABLE:
@@ -83,7 +84,7 @@ def create_app() -> Flask:
path = Path(app.static_folder or "") / filename path = Path(app.static_folder or "") / filename
try: try:
path.stat() path.stat()
# Note: A single static hash keeps module imports, CSS and local libraries on the same cache version. # Note: A single JS/CSS hash keeps module imports, stylesheets and local libraries on the same cache version.
return url_for("static", filename=filename, v=current_static_hash()) return url_for("static", filename=filename, v=current_static_hash())
except OSError: except OSError:
return url_for("static", filename=filename) return url_for("static", filename=filename)
+6 -4
View File
@@ -7499,8 +7499,8 @@
"tags": [ "tags": [
"System" "System"
], ],
"summary": "Get static asset version hash", "summary": "Get current frontend JS/CSS hash",
"description": "Returns the current hash for app static assets. Clients can compare it with window.PYTORRENT.staticHash and reload when it changes.", "description": "Returns the startup-computed hash for app JavaScript and CSS assets. The value is kept in memory and returned without scanning static files per request.",
"responses": { "responses": {
"200": { "200": {
"description": "Static asset hash", "description": "Static asset hash",
@@ -7513,10 +7513,12 @@
"type": "boolean" "type": "boolean"
}, },
"static_hash": { "static_hash": {
"type": "string" "type": "string",
"description": "Short SHA-256-based hash of frontend JavaScript and CSS files computed once at app startup."
}, },
"version": { "version": {
"type": "string" "type": "string",
"description": "Alias of static_hash for simple client version checks."
} }
} }
} }
+2 -4
View File
@@ -1,8 +1,6 @@
from __future__ import annotations from __future__ import annotations
from ._shared import * from ._shared import *
from flask import current_app
from pathlib import Path
from ..services import operation_logs from ..services import operation_logs
from ..services.frontend_assets import static_hash from ..services.frontend_assets import static_hash
@@ -51,8 +49,8 @@ def system_status():
@bp.get("/static_hash") @bp.get("/static_hash")
def static_hash_get(): def static_hash_get():
# Note: The frontend uses this lightweight version to detect changed static assets on mobile browsers. # Note: This returns the startup-computed JS/CSS version without scanning files per request.
value = static_hash(Path(current_app.static_folder or "")) value = static_hash()
return ok({"static_hash": value, "version": value}) return ok({"static_hash": value, "version": value})
+53 -19
View File
@@ -190,34 +190,68 @@ def validate_offline_assets() -> None:
) )
_STATIC_HASH_CACHE: dict[tuple[str, int], str] = {} _STATIC_HASH_VALUE = "dev"
_STATIC_HASH_READY = False
def static_hash(static_root: Path | None = None) -> str:
"""Return one short hash for all app static files.
Note: This value is used as the shared browser-cache version, so any static file def _versioned_static_files(root: Path) -> list[Path]:
change invalidates app.js imports, CSS and local frontend assets together. """Return static files that should invalidate frontend JS/CSS caches.
Note: Only JavaScript and CSS affect the executable frontend version. Images,
favicons and user-provided tracker icons stay outside this lightweight hash.
"""
return [
path
for path in root.rglob("*")
if path.is_file()
and path.suffix.lower() in {".js", ".css"}
and "tracker_favicons" not in path.parts
]
def compute_static_hash(static_root: Path | None = None) -> str:
"""Compute one short startup hash for frontend JavaScript and CSS files.
Note: This function reads JS/CSS files and should be called during app
startup, not from frequent request handlers.
""" """
import hashlib import hashlib
root = static_root or (BASE_DIR / "pytorrent" / "static") root = static_root or (BASE_DIR / "pytorrent" / "static")
files = [path for path in root.rglob("*") if path.is_file() and "tracker_favicons" not in path.parts]
fingerprint = f"{root}:{sum(path.stat().st_mtime_ns for path in files)}:{sum(path.stat().st_size for path in files)}"
cached = _STATIC_HASH_CACHE.get((fingerprint, len(files)))
if cached:
return cached
digest = hashlib.sha256() digest = hashlib.sha256()
for path in sorted(files): files = sorted(_versioned_static_files(root), key=lambda item: item.as_posix())
for path in files:
rel = path.relative_to(root).as_posix() rel = path.relative_to(root).as_posix()
stat = path.stat()
digest.update(rel.encode("utf-8"))
digest.update(str(stat.st_size).encode("ascii"))
digest.update(str(stat.st_mtime_ns).encode("ascii"))
try: try:
digest.update(path.read_bytes()) stat = path.stat()
content = path.read_bytes()
except OSError: except OSError:
continue continue
digest.update(rel.encode("utf-8"))
digest.update(str(stat.st_size).encode("ascii"))
digest.update(content)
value = digest.hexdigest()[:16] value = digest.hexdigest()[:16]
_STATIC_HASH_CACHE.clear() return value or "dev"
_STATIC_HASH_CACHE[(fingerprint, len(files))] = value
return value
def initialize_static_hash(static_root: Path | None = None) -> str:
"""Compute and store the frontend static hash once for this process.
Note: The API endpoint and template helpers only return this in-memory value,
which keeps mobile version checks ultra-light.
"""
global _STATIC_HASH_VALUE, _STATIC_HASH_READY
_STATIC_HASH_VALUE = compute_static_hash(static_root)
_STATIC_HASH_READY = True
return _STATIC_HASH_VALUE
def static_hash(static_root: Path | None = None) -> str:
"""Return the startup frontend static hash without rescanning files.
Note: The optional argument is kept for compatibility with existing callers;
it is only used for a lazy fallback before app startup initialization.
"""
if not _STATIC_HASH_READY:
return initialize_static_hash(static_root)
return _STATIC_HASH_VALUE
+8 -1
View File
@@ -99,9 +99,16 @@ async function loadModuleSources(){
return moduleSourcesPromise; return moduleSourcesPromise;
} }
function normalizeRuntimeSource(source){
const text = String(source || '');
// Note: Some generated source chunks may end with a literal \\n marker;
// normalize only this trailing marker to avoid invalid Function() source.
return text.endsWith('\\n') ? `${text.slice(0, -2)}\n` : text;
}
export async function buildRuntimeSource(){ export async function buildRuntimeSource(){
const sources = await loadModuleSources(); const sources = await loadModuleSources();
return `(() => {\n${sources.join('\n')}\n})();\n`; return `(() => {\n${sources.map(normalizeRuntimeSource).join('\n')}\n})();\n`;
} }
export async function startApp(){ export async function startApp(){
+1 -1
View File
@@ -1 +1 @@
export const bootstrapRuntimeSource = " async function checkStaticAssetVersion(){ try{ const r=await fetch('/api/static_hash',{cache:'no-store'}); const j=await r.json(); const current=String(window.PYTORRENT?.staticHash||''); const next=String(j.static_hash||j.version||''); if(current && next && current!==next){ window.PYTORRENT.staticHash=next; toast('A new frontend version is available. Reloading...','info'); setTimeout(()=>window.location.reload(), 600); } }catch(e){} }\n setInterval(checkStaticAssetVersion, 120000);\n window.addEventListener('focus', checkStaticAssetVersion);\n updateSortHeaders(); setupColumnResizers(); applyColumnVisibility(); renderColumnManager(); restoreFooterStatusCache(); refreshFooterStatusNow(); renderFooterPreferences(); applyFooterPreferences(); updateFooterClock(); updateBrowserSpeedTitle(); setupTorrentDropZone(); setInterval(updateFooterClock,1000); scheduleRender(true); if(!hasActiveProfile) renderNoProfileState(); loadLabels().catch(()=>{}); loadRatios().catch(()=>{}); loadSmartQueue().catch(()=>{}); loadAutomations().catch(()=>{}); ensureDashboardToolsUI(); if(portCheckEnabled) loadPortCheck(false); else renderPortCheck({status:'disabled',enabled:false}); if(hasActiveProfile) applyDefaultDownloadPath(false).catch(()=>{}); if(hasActiveProfile) refreshUserDiskUsage(true).catch(()=>{}); scheduleTrackerSummary(true);\\n"; export const bootstrapRuntimeSource = " let lastStaticAssetVersionCheck=0;\n async function checkStaticAssetVersion(force=false){ const now=Date.now(); if(!force && now-lastStaticAssetVersionCheck<60000) return; lastStaticAssetVersionCheck=now; try{ const r=await fetch('/api/static_hash',{cache:'no-store'}); const j=await r.json(); const current=String(window.PYTORRENT?.staticHash||''); const next=String(j.static_hash||j.version||''); if(current && next && current!==next){ window.PYTORRENT.staticHash=next; toast('A new frontend version is available. Reloading...','info'); setTimeout(()=>window.location.reload(), 600); } }catch(e){} }\n setInterval(()=>checkStaticAssetVersion(true), 900000);\n window.addEventListener('focus',()=>checkStaticAssetVersion(false));\n updateSortHeaders(); setupColumnResizers(); applyColumnVisibility(); renderColumnManager(); restoreFooterStatusCache(); refreshFooterStatusNow(); renderFooterPreferences(); applyFooterPreferences(); updateFooterClock(); updateBrowserSpeedTitle(); setupTorrentDropZone(); setInterval(updateFooterClock,1000); scheduleRender(true); if(!hasActiveProfile) renderNoProfileState(); loadLabels().catch(()=>{}); loadRatios().catch(()=>{}); loadSmartQueue().catch(()=>{}); loadAutomations().catch(()=>{}); ensureDashboardToolsUI(); if(portCheckEnabled) loadPortCheck(false); else renderPortCheck({status:'disabled',enabled:false}); if(hasActiveProfile) applyDefaultDownloadPath(false).catch(()=>{}); if(hasActiveProfile) refreshUserDiskUsage(true).catch(()=>{}); scheduleTrackerSummary(true);\n";