From 382a637b951d27e6747511ed37271d8a894b96da Mon Sep 17 00:00:00 2001 From: Simon Date: Wed, 24 Jun 2026 20:54:16 +0000 Subject: [PATCH] version updates --- backend/main.py | 58 +++++++++++++++++ frontend/css/style.css | 50 +++++++++++++++ frontend/index.html | 6 ++ frontend/js/main.js | 5 ++ frontend/js/version.js | 138 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 257 insertions(+) create mode 100644 frontend/js/version.js diff --git a/backend/main.py b/backend/main.py index 88c5749..6de38e3 100644 --- a/backend/main.py +++ b/backend/main.py @@ -12,6 +12,7 @@ from curl_cffi import requests as impersonate_requests import threading import io import time +import hashlib from urllib.parse import urljoin # Browser to impersonate at the TLS/HTTP layer. Some origins (e.g. the @@ -355,6 +356,63 @@ def index(): def favicon(): return send_from_directory(app.static_folder, 'favicon.ico') +# --- Frontend asset version tracking ------------------------------------- +# The client polls /api/version and, when a tracked file's content hash +# changes, hot-swaps CSS in place or reloads the page. This lets a deploy +# reach already-open tabs without a manual refresh. +_FRONTEND_DIR = os.path.abspath(app.static_folder) +_VERSION_EXTS = ('.html', '.css', '.js') +_version_cache = {'mtime': None, 'payload': None} +_version_lock = threading.Lock() + + +def _scan_frontend_files(): + """Map served relative paths -> absolute paths for tracked frontend files.""" + files = {} + for root, _dirs, names in os.walk(_FRONTEND_DIR): + for name in names: + if os.path.splitext(name)[1].lower() not in _VERSION_EXTS: + continue + path = os.path.join(root, name) + rel = os.path.relpath(path, _FRONTEND_DIR).replace(os.sep, '/') + files[rel] = path + return files + + +def _compute_version_payload(files): + """Hash each tracked file's contents plus a combined version fingerprint.""" + file_hashes = {} + combined = hashlib.md5() + for rel in sorted(files): + try: + with open(files[rel], 'rb') as fh: + digest = hashlib.md5(fh.read()).hexdigest() + except OSError: + continue + file_hashes[rel] = digest + combined.update(rel.encode('utf-8')) + combined.update(digest.encode('utf-8')) + return {'version': combined.hexdigest(), 'files': file_hashes} + + +@app.route('/api/version', methods=['GET']) +def frontend_version(): + files = _scan_frontend_files() + # Use the newest mtime across tracked files as a cheap cache key so frequent + # polls only re-hash contents when something on disk actually changed. + try: + latest_mtime = max((os.path.getmtime(p) for p in files.values()), default=0) + except OSError: + latest_mtime = 0 + with _version_lock: + if _version_cache['mtime'] != latest_mtime or _version_cache['payload'] is None: + _version_cache['payload'] = _compute_version_payload(files) + _version_cache['mtime'] = latest_mtime + payload = _version_cache['payload'] + resp = jsonify(payload) + resp.headers['Cache-Control'] = 'no-store' + return resp + @app.route('/api/stream', methods=['POST', 'GET', 'HEAD']) def stream_video(): # Note: