feat: sidebar scroll + log persistente (v1.11.1)

- 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.<view>) → 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"
This commit is contained in:
luciano committed 2026-08-27 11:27:57 +02:00
1 parent cdbb1d85d5
commit ed1059bf8c
8 files changed
+228 -1

No files matched your search

+3
View File
@@ -55,3 +55,6 @@ dedup_cache.db
dedup_cache.db-journal dedup_cache.db-journal
catalog_cache.db catalog_cache.db
catalog_cache.db-journal catalog_cache.db-journal
# Log persistente (rotating file)
logs/
+33
View File
@@ -136,7 +136,40 @@ class Api:
pass pass
def _log(self, view: str, msg: str) -> None: def _log(self, view: str, msg: str) -> None:
# Emit alla UI + mirror sul file di log persistente
self._emit("log", {"view": view, "msg": msg}) 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 # Bootstrap
+134
View File
@@ -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: <project_root>/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
+1 -1
View File
@@ -5,7 +5,7 @@ import os
import sys import sys
from pathlib import Path from pathlib import Path
VERSION = "v1.11.0" VERSION = "v1.11.1"
APP_NAME = "MusicTools" APP_NAME = "MusicTools"
+11
View File
@@ -4,6 +4,17 @@ import os
import sys import sys
from pathlib import Path 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 # Fix SSL CA bundle: quando l'app e' frozen (PyInstaller) i moduli
# ssl e requests non trovano nessun CA bundle di default -> tutte # ssl e requests non trovano nessun CA bundle di default -> tutte
+13
View File
@@ -151,6 +151,9 @@ button, input, select, textarea { font-family: inherit; font-size: inherit; }
flex-direction: column; flex-direction: column;
padding: 24px 12px; padding: 24px 12px;
border-right: 1px solid var(--border); border-right: 1px solid var(--border);
height: 100vh;
min-height: 0; /* consente ai flex children di rimpicciolirsi sotto il contenuto */
overflow: hidden;
} }
.brand { .brand {
@@ -192,7 +195,17 @@ button, input, select, textarea { font-family: inherit; font-size: inherit; }
flex-direction: column; flex-direction: column;
gap: 2px; gap: 2px;
flex: 1; 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 { .nav-item {
display: flex; display: flex;
+18
View File
@@ -598,6 +598,24 @@
</div> </div>
</div> </div>
<h2 class="section-label">Log</h2>
<div class="card">
<div class="field">
<label class="field-label">Cartella dei log</label>
<div class="row">
<div class="path-display" id="logPathDisplay" style="flex:1;">—</div>
<button class="btn btn-ghost pill btn-sm" id="openLogFolderBtn">📁 Apri cartella</button>
</div>
<div class="hint">
<span class="hint-ico">ⓘ</span>
<div>
L'app scrive un log giornaliero (<code>app.log</code>) con rotazione automatica (7 giorni).
Utile per debug: incluide stdout, stderr, log delle tab e traceback.
</div>
</div>
</div>
</div>
<h2 class="section-label">Aspetto</h2> <h2 class="section-label">Aspetto</h2>
<div class="card"> <div class="card">
<div class="field"> <div class="field">
+15
View File
@@ -490,6 +490,12 @@ async function init() {
if ($("#cookiesBrowserSelect")) $("#cookiesBrowserSelect").value = state.config.cookies_browser || ""; if ($("#cookiesBrowserSelect")) $("#cookiesBrowserSelect").value = state.config.cookies_browser || "";
$("#outputInput").value = state.config.output_dir || ""; $("#outputInput").value = state.config.output_dir || "";
$("#themeSelect").value = state.config.theme || "dark"; $("#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); applyTheme(state.config.theme);
// Download tab — usa output_dir come default // Download tab — usa output_dir come default
@@ -1284,6 +1290,15 @@ $("#browseCookiesBtn").addEventListener("click", async () => {
if (path) $("#cookiesInput").value = path; 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 () => { $("#browseOutputBtn").addEventListener("click", async () => {
const path = await window.pywebview.api.browse_directory(); const path = await window.pywebview.api.browse_directory();
if (path) $("#outputInput").value = path; if (path) $("#outputInput").value = path;