""" MyICR Suite Admin — deploy, dev mode, sync DB. Lanciare con: python\python.exe admin_gui.py """ import sys import os import subprocess import sqlite3 import shutil import json from pathlib import Path from datetime import datetime from PyQt6.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QPushButton, QLabel, QCheckBox, QTextEdit, ) from PyQt6.QtCore import QThread, pyqtSignal, QTimer from PyQt6.QtGui import QFont, QColor ROOT = Path(__file__).parent.resolve() SERVER_ROOT = Path(r"I:\SOFTWARE\PYTHON_SRV\PYTHON_LOCAL\MyICR_Suite") SERVER_SYNC_DB = Path(r"I:\SOFTWARE\PYTHON_SRV\sync_db\mediatrack.db") LOCAL_SYNC_DB = ROOT / "sync_db" / "mediatrack.db" LOCAL_LINKER_DB = ROOT / "sync_db" / "linker.db" SERVER_LINKER_DB = SERVER_ROOT / "sync_db" / "linker.db" LOCAL_OTT_PARQUET = ROOT / "local_db" / "parquet" / "ott.parquet" SERVER_OTT_PARQUET = SERVER_ROOT / "local_db" / "parquet" / "ott.parquet" PYTHON_EXE = ROOT / "python" / "python.exe" PYTHONW_EXE = ROOT / "python" / "pythonw.exe" _DEPLOY_EXCLUDE_DIRS = [".git", ".claude", "sync_db", "logs", "__pycache__", "local_db"] _DEPLOY_EXCLUDE_FILES = [ "db_user.db", "deploy_DEV_LOCAL.bat", "run_DEV_LOCAL.bat", "sync_DEV_LOCAL.bat", "admin.bat", "admin_gui.py", "BRIEFING.md", "python_env.sig", "SESSION_NOTES.md", "_check_diritti.py", "_check_sr.py", "_debug_filter.py", "_fix_mercato.py", "_run_check.bat", "_run_fix.bat", ] def _ts(): return datetime.now().strftime("%H:%M:%S") class WorkerThread(QThread): line = pyqtSignal(str, str) # text, color finished = pyqtSignal(bool) def __init__(self, fn): super().__init__() self._fn = fn def run(self): ok = True try: self._fn(self.line.emit) except Exception as e: self.line.emit(f"[ERRORE] {e}", "red") ok = False self.finished.emit(ok) class AdminWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("MyICR Suite — Admin") self.setMinimumSize(800, 600) self._thread = None self._dev_proc = None # MyICR Suite (start.py --dev) self._dev_running = False # True finché l'utente non preme Ferma self._dev_mt_proc = None # bootstrap (esiste solo durante sync, poi esce) self._dev_mt_running = False # True finché run.py è in vita (porta 5000) self._build_ui() QTimer.singleShot(200, self._refresh_status) self._proc_poll_timer = QTimer(self) self._proc_poll_timer.setInterval(2000) self._proc_poll_timer.timeout.connect(self._poll_dev_proc) self._proc_poll_timer.start() # ------------------------------------------------------------------ UI --- def _build_ui(self): central = QWidget() self.setCentralWidget(central) root_lay = QVBoxLayout(central) root_lay.setSpacing(10) root_lay.setContentsMargins(12, 12, 12, 12) # title title = QLabel("MyICR Suite — Admin") title.setFont(QFont("Calibri", 13, QFont.Weight.Bold)) root_lay.addWidget(title) # status line self._status_lbl = QLabel("…") self._status_lbl.setFont(QFont("Calibri", 9)) root_lay.addWidget(self._status_lbl) sep = QLabel() sep.setFixedHeight(1) sep.setStyleSheet("background:#ccc;") root_lay.addWidget(sep) # operation boxes — riga 1 ops_row = QHBoxLayout() ops_row.setSpacing(10) ops_row.addWidget(self._build_dev_box()) ops_row.addWidget(self._build_deploy_box()) ops_row.addWidget(self._build_sync_box()) ops_row.addWidget(self._build_pubblica_box()) root_lay.addLayout(ops_row) # log log_box = QGroupBox("Log operazioni") log_lay = QVBoxLayout(log_box) self._log = QTextEdit() self._log.setReadOnly(True) self._log.setFont(QFont("Consolas", 8)) log_lay.addWidget(self._log) root_lay.addWidget(log_box, stretch=1) def _build_dev_box(self): box = QGroupBox("Dev Mode") lay = QVBoxLayout(box) lay.addWidget(QLabel("MyICR Suite (Flask debug):")) self._dev_btn = QPushButton("▶ Avvia MyICR Suite") self._dev_btn.setMinimumHeight(34) self._dev_btn.clicked.connect(self._toggle_dev_suite) lay.addWidget(self._dev_btn) lay.addSpacing(6) lay.addWidget(QLabel("MediaTrack (flusso utente):")) self._dev_mt_btn = QPushButton("▶ Avvia MediaTrack") self._dev_mt_btn.setMinimumHeight(34) self._dev_mt_btn.clicked.connect(self._toggle_dev_mediatrack) lay.addWidget(self._dev_mt_btn) lay.addStretch() return box def _build_deploy_box(self): box = QGroupBox("Deploy → Server") lay = QVBoxLayout(box) lay.addWidget(QLabel( "Copia file A → server.\n" "Esclude: .git, sync_db, logs, local_db." )) self._dryrun_cb = QCheckBox("Dry run (solo anteprima)") lay.addWidget(self._dryrun_cb) self._mediatrack_cb = QCheckBox("Includi MediaTrack") self._mediatrack_cb.setChecked(True) lay.addWidget(self._mediatrack_cb) self._deploy_btn = QPushButton("⬆ Deploy") self._deploy_btn.setMinimumHeight(38) self._deploy_btn.clicked.connect(self._run_deploy) lay.addWidget(self._deploy_btn) lay.addStretch() return box def _build_sync_box(self): box = QGroupBox("Sync DB ← Server") lay = QVBoxLayout(box) lay.addWidget(QLabel( "Copia mediatrack.db + parquet\n" "dal server a locale (per test)." )) self._schema_lbl = QLabel("Schema: —") self._schema_lbl.setFont(QFont("Calibri", 8)) lay.addWidget(self._schema_lbl) self._sync_btn = QPushButton("⬇ Sync DB") self._sync_btn.setMinimumHeight(38) self._sync_btn.clicked.connect(self._run_sync) lay.addWidget(self._sync_btn) lay.addStretch() return box def _build_pubblica_box(self): box = QGroupBox("Pubblica DB → Server [DEV]") lay = QVBoxLayout(box) lay.addWidget(QLabel( "Copia linker.db e ott.parquet\n" "dal locale al server.\n" "Solo in DEV (in prod è automatico)." )) self._pub_linker_cb = QCheckBox("linker.db") self._pub_linker_cb.setChecked(True) lay.addWidget(self._pub_linker_cb) self._pub_ott_cb = QCheckBox("ott.parquet") self._pub_ott_cb.setChecked(True) lay.addWidget(self._pub_ott_cb) self._pub_btn = QPushButton("⬆ Pubblica DB") self._pub_btn.setMinimumHeight(38) self._pub_btn.clicked.connect(self._run_pubblica) lay.addWidget(self._pub_btn) lay.addStretch() return box # ----------------------------------------------------------- status check - def _refresh_status(self): server_ok = SERVER_ROOT.exists() db_local = LOCAL_SYNC_DB.exists() db_server = SERVER_SYNC_DB.exists() if server_ok else False parts = [] parts.append("✓ Server" if server_ok else "✗ Server non raggiungibile") parts.append("✓ DB locale" if db_local else "✗ DB locale assente") self._status_lbl.setText(" | ".join(parts)) self._deploy_btn.setEnabled(server_ok) self._sync_btn.setEnabled(server_ok and db_local and db_server) self._pub_btn.setEnabled(server_ok) if db_local and db_server: ok, msg = self._compare_schemas() self._schema_lbl.setText(f"Schema: {'✓ OK' if ok else '⚠ ' + msg}") self._sync_btn.setEnabled(server_ok) # allow even if diff (will warn) elif server_ok: self._schema_lbl.setText("Schema: server non raggiungibile") else: self._schema_lbl.setText("Schema: —") def _compare_schemas(self): try: def _tables(path): c = sqlite3.connect(str(path)) tables = {} for (t,) in c.execute( "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name" ): cols = tuple( (r[1], r[2]) for r in c.execute(f"PRAGMA table_info('{t}')") ) tables[t] = cols c.close() return tables local = _tables(LOCAL_SYNC_DB) server = _tables(SERVER_SYNC_DB) if set(local) != set(server): missing = set(server) - set(local) extra = set(local) - set(server) diff = [] if missing: diff.append(f"mancanti localmente: {', '.join(missing)}") if extra: diff.append(f"solo in locale: {', '.join(extra)}") return False, "; ".join(diff) for t in local: if local[t] != server[t]: return False, f"colonne diverse in '{t}'" return True, "OK" except Exception as e: return False, str(e) # ---------------------------------------------------------------- log ---- def _log_line(self, text, color="black"): self._log.setTextColor(QColor(color if color != "gray" else "#666")) self._log.append(f"[{_ts()}] {text}") sb = self._log.verticalScrollBar() sb.setValue(sb.maximum()) # ----------------------------------------------------------- busy lock ---- def _run_pubblica(self): do_linker = self._pub_linker_cb.isChecked() do_ott = self._pub_ott_cb.isChecked() if not do_linker and not do_ott: return def fn(log): log("Pubblicazione DB locale → server...", "blue") if not SERVER_ROOT.exists(): log("Server non raggiungibile. Abortito.", "red") return if do_linker: if not LOCAL_LINKER_DB.exists(): log(f"linker.db locale non trovato: {LOCAL_LINKER_DB}", "red") else: try: dst = sqlite3.connect(str(SERVER_LINKER_DB)) src = sqlite3.connect(str(LOCAL_LINKER_DB)) src.backup(dst) src.close() dst.close() log(f"linker.db pubblicato → {SERVER_LINKER_DB}", "green") except Exception as e: log(f"Errore linker.db: {e}", "red") if do_ott: if not LOCAL_OTT_PARQUET.exists(): log(f"ott.parquet locale non trovato: {LOCAL_OTT_PARQUET}", "red") else: try: SERVER_OTT_PARQUET.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(str(LOCAL_OTT_PARQUET), str(SERVER_OTT_PARQUET)) log(f"ott.parquet pubblicato → {SERVER_OTT_PARQUET}", "green") except Exception as e: log(f"Errore ott.parquet: {e}", "red") self._run_in_thread(fn) def _set_busy(self, busy): self._deploy_btn.setEnabled(not busy) self._sync_btn.setEnabled(not busy) self._pub_btn.setEnabled(not busy) # dev btn managed separately def _on_thread_done(self, _): self._set_busy(False) self._refresh_status() def _run_in_thread(self, fn): self._set_busy(True) self._thread = WorkerThread(fn) self._thread.line.connect(self._log_line) self._thread.finished.connect(self._on_thread_done) self._thread.start() def _poll_dev_proc(self): """Controlla ogni 2s se MyICR Suite è ancora in esecuzione.""" if self._dev_running and self._dev_proc and self._dev_proc.poll() is not None: self._dev_proc = None self._dev_running = False self._dev_btn.setText("▶ Avvia MyICR Suite") self._log_line("MyICR Suite terminato (chiuso dall'utente).", "gray") # --------------------------------------------------------------- Dev Mode - def _toggle_dev_suite(self): if self._dev_running: pid = self._dev_proc.pid if self._dev_proc else None try: if pid: subprocess.run( ["taskkill", "/F", "/T", "/PID", str(pid)], capture_output=True ) elif self._dev_proc: self._dev_proc.terminate() except Exception: pass self._dev_proc = None self._dev_running = False self._dev_btn.setText("▶ Avvia MyICR Suite") self._log_line(f"MyICR Suite terminato{f' (PID {pid})' if pid else ''}.", "blue") return self._log_line("Avvio MyICR Suite (Flask debug)...", "blue") env = os.environ.copy() env["FLASK_DEBUG"] = "1" env["MYICR_DEV_MODE"] = "1" try: self._dev_proc = subprocess.Popen( [str(PYTHONW_EXE), str(ROOT / "app" / "start.py"), "--dev"], env=env, cwd=str(ROOT), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, creationflags=subprocess.CREATE_NO_WINDOW, ) self._dev_running = True self._dev_btn.setText("⬛ Ferma MyICR Suite") self._log_line(f"Avviato (PID {self._dev_proc.pid}).", "green") except Exception as e: self._log_line(f"Impossibile avviare MyICR Suite: {e}", "red") def _toggle_dev_mediatrack(self): if self._dev_mt_running: self._log_line("Arresto MediaTrack (porta 5000)...", "blue") try: # bootstrap.py esce subito dopo aver spawnatun run.py figlio — # non abbiamo il PID di run.py, quindi troviamo il processo # che ascolta sulla porta 5000 e lo terminiamo. result = subprocess.run( ["netstat", "-ano"], capture_output=True, text=True ) pid = None for line in result.stdout.splitlines(): if ":5000 " in line and "LISTENING" in line: pid = line.strip().split()[-1] break if pid: subprocess.run( ["taskkill", "/F", "/T", "/PID", pid], capture_output=True ) self._log_line(f"MediaTrack terminato (PID {pid}).", "blue") else: self._log_line("Nessun processo trovato su porta 5000.", "gray") except Exception as e: self._log_line(f"Errore nel terminare MediaTrack: {e}", "red") self._dev_mt_running = False self._dev_mt_proc = None self._dev_mt_btn.setText("▶ Avvia MediaTrack") return self._log_line("Avvio bootstrap.py --mediatrack dal sorgente dev...", "blue") self._log_line(f" SERVER_ROOT = {ROOT}", "gray") self._log_line(" Sync → C:\\PYTHON_LOCAL\\MyICR_Suite poi avvia MediaTrack.", "gray") try: self._dev_mt_proc = subprocess.Popen( [str(PYTHONW_EXE), str(ROOT / "bootstrap.py"), "--mediatrack"], cwd=str(ROOT), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) self._dev_mt_running = True self._dev_mt_btn.setText("⬛ Ferma MediaTrack") self._log_line(f"Bootstrap avviato (PID {self._dev_mt_proc.pid}).", "green") except Exception as e: self._log_line(f"Impossibile avviare bootstrap: {e}", "red") # ------------------------------------------------------------- Deploy ----- def _run_deploy(self): dry = self._dryrun_cb.isChecked() incl_mt = self._mediatrack_cb.isChecked() def fn(log): tag = "[DRY RUN] " if dry else "" mt_tag = "" if incl_mt else " (senza MediaTrack)" log(f"Deploy {tag}A → Server{mt_tag}...", "blue") if not SERVER_ROOT.exists(): log("Server non raggiungibile. Abortito.", "red") return # Avviso se lo schema del DB locale è diverso da quello sul server if LOCAL_SYNC_DB.exists() and SERVER_SYNC_DB.exists(): ok, msg = self._compare_schemas() if not ok: log("", "black") log("⚠ ATTENZIONE: schema DB diverso tra locale e server.", "red") log(f" Dettaglio: {msg}", "red") log(" Dopo il deploy ricordati di allineare manualmente il DB del server.", "red") log("", "black") exclude_dirs = list(_DEPLOY_EXCLUDE_DIRS) if not incl_mt: exclude_dirs.append("mediatrack") # Reset sessione Linker: lascia solo il foglio iniziale linker_session = ROOT / "app" / "modules" / "linker" / "session.json" if linker_session.exists(): try: with open(linker_session, encoding="utf-8") as f: sess = json.load(f) sheets = sess.get("sheets", []) first = next((s for s in sheets if s.get("app_type") == "linker"), None) if first is None: first = {"app_type": "linker", "name": "Linker 1", "rows": []} else: first = {k: ([] if k == "rows" else v) for k, v in first.items()} clean = {"version": sess.get("version", 3), "sheets": [first]} with open(linker_session, "w", encoding="utf-8") as f: json.dump(clean, f, ensure_ascii=False, indent=2) log("Sessione Linker resettata (1 foglio).", "gray") except Exception as e: log(f"⚠ Reset sessione Linker fallito: {e}", "red") cmd = ["robocopy", str(ROOT), str(SERVER_ROOT), "/MIR", "/XO", "/NP"] if dry: cmd.append("/L") for d in exclude_dirs: cmd += ["/XD", d] for f in _DEPLOY_EXCLUDE_FILES: cmd += ["/XF", f] log(f"robocopy {' '.join(cmd[3:])}", "gray") result = subprocess.run(cmd, capture_output=True, encoding='cp1252', errors='replace') for line in (result.stdout or '').splitlines(): stripped = line.strip() if stripped and not stripped.startswith("---"): log(stripped, "black") rc = result.returncode if rc <= 3: if dry: log("Dry run completato.", "blue") else: log("Deploy completato.", "green") # rigenera python_env.sig sul server sig = SERVER_ROOT / "_gen_env_sig.py" spyt = SERVER_ROOT / "python" / "python.exe" if sig.exists() and spyt.exists(): subprocess.run([str(spyt), str(sig), str(SERVER_ROOT)], check=False, capture_output=True) log("python_env.sig aggiornato.", "gray") else: log(f"robocopy exit={rc} — possibile errore.", "red") for line in result.stderr.splitlines(): if line.strip(): log(line, "red") self._run_in_thread(fn) # --------------------------------------------------------------- Sync DB -- def _run_sync(self): def fn(log): log("Verifica schema DB prima della sync...", "blue") ok, msg = self._compare_schemas() if not ok: log(f"⚠ Schema diverso: {msg}", "red") log( "Sync bloccata — le strutture non coincidono.\n" "Allineare il DB locale prima di procedere.", "red", ) return log("Schema OK — procedo.", "green") log("[1/3] Copia mediatrack.db dal server...", "black") shutil.copy2(str(SERVER_SYNC_DB), str(LOCAL_SYNC_DB)) log("OK.", "green") log("[2/3] Sync parquet...", "black") srv_pq = SERVER_ROOT / "local_db" / "parquet" loc_pq = ROOT / "local_db" / "parquet" if srv_pq.exists(): loc_pq.mkdir(parents=True, exist_ok=True) cmd = ["robocopy", str(srv_pq), str(loc_pq), "*.parquet", "/MIR", "/NP", "/NJH", "/NJS", "/NFL", "/NDL"] subprocess.run(cmd, capture_output=True) log("OK.", "green") else: log("Cartella parquet non trovata sul server, skip.", "gray") log("[3/3] Sync locandine...", "black") srv_po = SERVER_ROOT / "local_db" / "posters" loc_po = ROOT / "local_db" / "posters" if srv_po.exists(): loc_po.mkdir(parents=True, exist_ok=True) cmd = ["robocopy", str(srv_po), str(loc_po), "/MIR", "/NP", "/NJH", "/NJS", "/NFL", "/NDL"] subprocess.run(cmd, capture_output=True) log("OK.", "green") else: log("Cartella posters non trovata sul server, skip.", "gray") log("Sync completata.", "green") self._run_in_thread(fn) # -------------------------------------------------------- window close ---- def closeEvent(self, event): if self._dev_proc and self._dev_proc.poll() is None: try: subprocess.run( ["taskkill", "/F", "/T", "/PID", str(self._dev_proc.pid)], capture_output=True ) except Exception: self._dev_proc.terminate() if self._dev_mt_running: self._toggle_dev_mediatrack() event.accept() if __name__ == "__main__": import traceback _log_path = Path(__file__).parent / "logs" / "admin_crash.log" _log_path.parent.mkdir(exist_ok=True) def _excepthook(exc_type, exc_value, exc_tb): msg = "".join(traceback.format_exception(exc_type, exc_value, exc_tb)) with open(_log_path, "a", encoding="utf-8") as f: f.write(f"\n[{datetime.now()}]\n{msg}") sys.__excepthook__(exc_type, exc_value, exc_tb) sys.excepthook = _excepthook app = QApplication(sys.argv) app.setStyle("Fusion") win = AdminWindow() win.show() sys.exit(app.exec())