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:
1 parent
cdbb1d85d5
commit
ed1059bf8c
8 files changed
+228
-1
No files matched your search
@@ -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/
|
||||||
@@ -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
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in new issue
Block a user