From ed1059bf8c74467a23ae4ec85a8825b659a8f107 Mon Sep 17 00:00:00 2001 From: luciano Date: Thu, 27 Aug 2026 11:27:57 +0200 Subject: [PATCH] feat: sidebar scroll + log persistente (v1.11.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sidebar diventata troppo lunga (14+ tab): overflow-y auto sulla nav, brand e footer restano sticky. Scrollbar 6px tema-coerente - Nuovo core/applog.py: TimedRotatingFileHandler daily con 7 giorni di retention. NON tocca sys.stdout/stderr (causa segfault WebKit su macOS); cattura tracebacks via sys.excepthook + threading.excepthook - bridge._log mirror su logger persistente (ui.) → tutto quello che vedi nei log delle tab e' anche su file - Bridge: get_log_file_path() + open_log_folder() per la nuova sezione Impostazioni con path visibile e bottone "Apri cartella" --- .gitignore | 3 + api/bridge.py | 33 +++++++++++ core/applog.py | 134 ++++++++++++++++++++++++++++++++++++++++++++ core/config.py | 2 +- main.py | 11 ++++ webui/css/style.css | 13 +++++ webui/index.html | 18 ++++++ webui/js/app.js | 15 +++++ 8 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 core/applog.py diff --git a/.gitignore b/.gitignore index a357474..3f12654 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,6 @@ dedup_cache.db dedup_cache.db-journal catalog_cache.db catalog_cache.db-journal + +# Log persistente (rotating file) +logs/ diff --git a/api/bridge.py b/api/bridge.py index 9cb1a0d..9fe7acc 100644 --- a/api/bridge.py +++ b/api/bridge.py @@ -136,7 +136,40 @@ class Api: pass def _log(self, view: str, msg: str) -> None: + # Emit alla UI + mirror sul file di log persistente self._emit("log", {"view": view, "msg": msg}) + try: + import logging + logging.getLogger(f"ui.{view}").info(msg) + except Exception: + pass + + # ------------------------------------------------------------------ + # Log persistente + # ------------------------------------------------------------------ + def get_log_file_path(self) -> str: + """Ritorna il path del file di log corrente (per apertura da UI).""" + try: + from core.applog import log_file_path + return str(log_file_path()) + except Exception: + return "" + + def open_log_folder(self) -> dict: + """Apre la cartella dei log nel file manager del sistema.""" + try: + from core.applog import log_file_path + import subprocess + folder = log_file_path().parent + if sys.platform == "darwin": + subprocess.Popen(["open", str(folder)]) + elif sys.platform == "win32": + os.startfile(str(folder)) # type: ignore[attr-defined] + else: + subprocess.Popen(["xdg-open", str(folder)]) + return {"ok": True, "path": str(folder)} + except Exception as e: + return {"ok": False, "error": str(e)} # ------------------------------------------------------------------ # Bootstrap diff --git a/core/applog.py b/core/applog.py new file mode 100644 index 0000000..7ead9f4 --- /dev/null +++ b/core/applog.py @@ -0,0 +1,134 @@ +"""Setup logging persistente su file rotante. + +Chiamato in `main.py` prima di qualsiasi import pesante. Il file di log +vive in: + - macOS: ~/Library/Application Support/MusicTools/logs/app.log + - Windows: %APPDATA%/MusicTools/logs/app.log + - Dev: /logs/app.log + +Rotazione: 1 file al giorno, mantiene gli ultimi 7. Cattura anche stdout +e stderr per non perdere `print()` o traceback che l'app windowed +altrimenti scarterebbe. +""" + +from __future__ import annotations + +import logging +import logging.handlers +import os +import sys +from pathlib import Path + + +_setup_done = False + + +def _logs_dir() -> Path: + """Directory dei log — parallela a config.json.""" + if getattr(sys, "frozen", False): + if sys.platform == "win32": + base = Path(os.environ.get("APPDATA", Path.home())) + else: + base = Path.home() / "Library" / "Application Support" + d = base / "MusicTools" / "logs" + else: + d = Path(__file__).resolve().parent.parent / "logs" + d.mkdir(parents=True, exist_ok=True) + return d + + +def log_file_path() -> Path: + return _logs_dir() / "app.log" + + +def _install_excepthook() -> None: + """Cattura ogni uncaught exception dentro il file di log. + NON tocca sys.stdout/stderr (romperebbe WebKit).""" + import traceback + logger = logging.getLogger("uncaught") + _orig = sys.excepthook + + def _hook(exc_type, exc_value, tb): + try: + logger.error( + "Uncaught: %s: %s\n%s", + exc_type.__name__, + exc_value, + "".join(traceback.format_exception(exc_type, exc_value, tb)), + ) + except Exception: + pass + try: + _orig(exc_type, exc_value, tb) + except Exception: + pass + + sys.excepthook = _hook + + # Anche per exception in threading (Python 3.8+) + try: + _orig_thread = threading_excepthook = getattr(__import__("threading"), "excepthook", None) + + def _thread_hook(args): + try: + logger.error( + "Thread uncaught: %s: %s\n%s", + args.exc_type.__name__, + args.exc_value, + "".join(traceback.format_exception(args.exc_type, args.exc_value, args.exc_traceback)), + ) + except Exception: + pass + if _orig_thread: + try: + _orig_thread(args) + except Exception: + pass + + if threading_excepthook is not None: + __import__("threading").excepthook = _thread_hook + except Exception: + pass + + +def setup() -> Path: + """Configura il logger root — NON tocca stdout/stderr per non rompere + WebKit (segfault). Cattura traceback via sys.excepthook + logger. + """ + global _setup_done + path = log_file_path() + if _setup_done: + return path + _setup_done = True + + root = logging.getLogger() + root.setLevel(logging.INFO) + + fmt = logging.Formatter( + fmt="%(asctime)s [%(levelname)s] %(name)s: %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + + # File handler con rotazione giornaliera (max 7 file = 7 giorni) + fh = logging.handlers.TimedRotatingFileHandler( + str(path), + when="midnight", + backupCount=7, + encoding="utf-8", + utc=False, + ) + fh.setFormatter(fmt) + fh.setLevel(logging.INFO) + root.addHandler(fh) + + # Se in dev, mantieni anche console (utile durante lavoro) + if not getattr(sys, "frozen", False): + ch = logging.StreamHandler(sys.__stderr__) + ch.setFormatter(fmt) + ch.setLevel(logging.INFO) + root.addHandler(ch) + + _install_excepthook() + + root.info(f"MusicTools log start — file: {path}") + return path diff --git a/core/config.py b/core/config.py index e5bea8a..0f82fe1 100644 --- a/core/config.py +++ b/core/config.py @@ -5,7 +5,7 @@ import os import sys from pathlib import Path -VERSION = "v1.11.0" +VERSION = "v1.11.1" APP_NAME = "MusicTools" diff --git a/main.py b/main.py index 1a29c15..74367f0 100644 --- a/main.py +++ b/main.py @@ -4,6 +4,17 @@ import os import sys from pathlib import Path +# ============================================================ +# Logging persistente su file (rotazione giornaliera, 7 giorni). +# Chiamato SUBITO cosi' ogni traceback o print da qui in poi +# finisce nel file, anche in modalita' windowed. +# ============================================================ +try: + from core.applog import setup as _setup_log + _setup_log() +except Exception as _e: + print(f"[bootstrap] log setup failed: {_e}") + # ============================================================ # Fix SSL CA bundle: quando l'app e' frozen (PyInstaller) i moduli # ssl e requests non trovano nessun CA bundle di default -> tutte diff --git a/webui/css/style.css b/webui/css/style.css index 599c4ce..5e634d2 100644 --- a/webui/css/style.css +++ b/webui/css/style.css @@ -151,6 +151,9 @@ button, input, select, textarea { font-family: inherit; font-size: inherit; } flex-direction: column; padding: 24px 12px; border-right: 1px solid var(--border); + height: 100vh; + min-height: 0; /* consente ai flex children di rimpicciolirsi sotto il contenuto */ + overflow: hidden; } .brand { @@ -192,7 +195,17 @@ button, input, select, textarea { font-family: inherit; font-size: inherit; } flex-direction: column; gap: 2px; flex: 1; + min-height: 0; /* per far funzionare overflow su flex child */ + overflow-y: auto; /* scrolla se ci sono troppi tab e la finestra è corta */ + overflow-x: hidden; + scrollbar-width: thin; } +.nav::-webkit-scrollbar { width: 6px; } +.nav::-webkit-scrollbar-thumb { + background: var(--bg-input); + border-radius: 3px; +} +.nav::-webkit-scrollbar-thumb:hover { background: var(--bg-input-hover); } .nav-item { display: flex; diff --git a/webui/index.html b/webui/index.html index c5696b7..c659e79 100644 --- a/webui/index.html +++ b/webui/index.html @@ -598,6 +598,24 @@ + +
+
+ +
+
—
+ +
+
+ ⓘ +
+ L'app scrive un log giornaliero (app.log) con rotazione automatica (7 giorni). + Utile per debug: incluide stdout, stderr, log delle tab e traceback. +
+
+
+
+
diff --git a/webui/js/app.js b/webui/js/app.js index b03fad8..4f84d7c 100644 --- a/webui/js/app.js +++ b/webui/js/app.js @@ -490,6 +490,12 @@ async function init() { if ($("#cookiesBrowserSelect")) $("#cookiesBrowserSelect").value = state.config.cookies_browser || ""; $("#outputInput").value = state.config.output_dir || ""; $("#themeSelect").value = state.config.theme || "dark"; + // Popola path del log corrente + if ($("#logPathDisplay")) { + window.pywebview.api.get_log_file_path().then((p) => { + if (p) $("#logPathDisplay").textContent = p; + }).catch(() => {}); + } applyTheme(state.config.theme); // Download tab — usa output_dir come default @@ -1284,6 +1290,15 @@ $("#browseCookiesBtn").addEventListener("click", async () => { if (path) $("#cookiesInput").value = path; }); +$("#openLogFolderBtn")?.addEventListener("click", async () => { + try { + const r = await window.pywebview.api.open_log_folder(); + if (!r || !r.ok) toast("Impossibile aprire la cartella: " + (r?.error || "errore"), "error"); + } catch (e) { + toast("Errore: " + e, "error"); + } +}); + $("#browseOutputBtn").addEventListener("click", async () => { const path = await window.pywebview.api.browse_directory(); if (path) $("#outputInput").value = path;