⬅ חזרה לתיקייה · הורד קובץ · ערוך

📄 admin_server_RESCUE_REPLACED_CURRENT_20260701_130606.py

C:\TripServer\backups\admin_server\admin_server_RESCUE_REPLACED_CURRENT_20260701_130606.py

# -*- coding: utf-8 -*-
"""
admin_server_PANEL_AUTO_RESCUE.py

קובץ חירום להעלאה דרך הפאנל החכם, בשדה "העלה admin_server.py חדש".
מטרתו: לשחזר אוטומטית את הגרסה העדכנית ביותר של admin_server.py מתוך הגיבויים,
בלי להחזיר את הפאנל אחורה.

שימוש מהפאנל:
1. העלה את הקובץ הזה במקום admin_server.py.
2. לחץ "אתחל פאנל".
3. המתן 10–20 שניות ורענן.

הקובץ מחפש גיבויים בעיקר ב:
C:\TripServer\backups\admin_server
ומדרג אותם לפי סימנים של הגרסה המתקדמת: /admin/usa, USA_TV, v5 וכו'.
"""

from __future__ import annotations

import os
import sys
import time
import glob
import shutil
import hashlib
import py_compile
import subprocess
import threading
from datetime import datetime
from pathlib import Path

BASE_DIR = Path(r"C:\TripServer")
TARGET = BASE_DIR / "admin_server.py"
BACKUP_DIRS = [
    BASE_DIR / "backups" / "admin_server",
    BASE_DIR / "backups",
    BASE_DIR,
]
LOG_PATH = BASE_DIR / "admin_panel_auto_rescue.log"
MIN_AUTO_SCORE = 120

MARKERS = {
    "multi-page USA phone TV separated v5 canonical self-update": 300,
    "v5 canonical self-update": 200,
    "USA_TV": 90,
    "/USA_TV": 90,
    "admin_usa": 70,
    "/admin/usa": 90,
    "upload_usa_presentation_image": 65,
    "usa_presentation_image": 55,
    "restore_admin_server_backup": 35,
    "upload_admin_server": 35,
    "restart_admin_server": 25,
    "/health": 20,
    "טיול ארצות הברית 27 מותאם לטלוויזיה": 45,
    "מצגת ארצות הברית מותאמת למסך טלוויזיה": 45,
}

PENALTIES = {
    "USA2027 panel v3 reliable restart": -120,
    "slides\": 43": -60,
}


def log(msg: str) -> None:
    try:
        LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
        with LOG_PATH.open("a", encoding="utf-8") as f:
            f.write(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {msg}\n")
    except Exception:
        pass
    print(msg, flush=True)


def read_text(path: Path) -> str:
    data = path.read_bytes()
    for enc in ("utf-8", "utf-8-sig", "cp1255", "latin-1"):
        try:
            return data.decode(enc)
        except UnicodeDecodeError:
            continue
    return data.decode("utf-8", errors="ignore")


def sha12(path: Path) -> str:
    h = hashlib.sha256()
    with path.open("rb") as f:
        for chunk in iter(lambda: f.read(1024 * 1024), b""):
            h.update(chunk)
    return h.hexdigest()[:12]


def compile_ok(path: Path) -> tuple[bool, str]:
    try:
        py_compile.compile(str(path), doraise=True)
        return True, "OK"
    except Exception as exc:
        return False, repr(exc)


def score_file(path: Path) -> tuple[int, list[str], str]:
    try:
        text = read_text(path)
    except Exception as exc:
        return -9999, [], f"read failed: {exc!r}"

    score = 0
    found: list[str] = []

    for marker, pts in MARKERS.items():
        if marker in text:
            score += pts
            found.append(marker)

    for marker, pts in PENALTIES.items():
        if marker in text:
            score += pts
            found.append(f"PENALTY:{marker}")

    size_kb = path.stat().st_size / 1024
    if size_kb > 50:
        score += 5
    if size_kb > 75:
        score += 10
    if size_kb > 100:
        score += 10

    ok, msg = compile_ok(path)
    if not ok:
        score -= 1000
        found.append("COMPILE_FAIL")
        return score, found, msg

    return score, found, "OK"


def collect_candidates() -> list[dict]:
    files: dict[str, Path] = {}
    patterns = [
        "admin_server_backup_*.py",
        "admin_server_before_restore_*.py",
        "admin_server_CURRENT_BEFORE_RESCUE_*.py",
        "admin_server*.py",
        "trip_server*.py",
        "*_admin_server*.py",
    ]

    for root in BACKUP_DIRS:
        if not root.exists():
            continue
        for pat in patterns:
            for p in root.glob(pat):
                try:
                    rp = p.resolve()
                    # לא לבחור את קובץ החירום הפעיל עצמו
                    if rp == TARGET.resolve():
                        continue
                    if rp.is_file() and rp.suffix.lower() == ".py":
                        files[str(rp)] = rp
                except Exception:
                    continue

    rows = []
    for p in files.values():
        try:
            score, markers, status = score_file(p)
            rows.append({
                "path": p,
                "score": score,
                "markers": markers,
                "status": status,
                "mtime": p.stat().st_mtime,
                "size": p.stat().st_size,
                "sha": sha12(p),
            })
        except Exception as exc:
            rows.append({
                "path": p,
                "score": -9999,
                "markers": [],
                "status": repr(exc),
                "mtime": 0,
                "size": 0,
                "sha": "ERR",
            })

    rows.sort(key=lambda r: (r["score"], r["mtime"]), reverse=True)
    return rows


def delayed_launch_restored_server() -> None:
    py = sys.executable or "python"
    target = str(TARGET)
    if os.name == "nt":
        # נותן לתהליך החירום להשתחרר מהפורט לפני הרצת הפאנל המשוחזר
        cmd = f'ping 127.0.0.1 -n 3 > nul && "{py}" "{target}"'
        creationflags = getattr(subprocess, "CREATE_NEW_CONSOLE", 0)
        subprocess.Popen(["cmd", "/c", cmd], cwd=str(BASE_DIR), creationflags=creationflags)
    else:
        subprocess.Popen([py, target], cwd=str(BASE_DIR))


def restore_candidate(row: dict) -> tuple[bool, str]:
    src: Path = row["path"]
    ok, msg = compile_ok(src)
    if not ok:
        return False, f"המועמד לא עבר קומפילציה: {msg}"

    rescue_backup_dir = BASE_DIR / "backups" / "admin_server"
    rescue_backup_dir.mkdir(parents=True, exist_ok=True)
    stamp = datetime.now().strftime("%Y%m%d_%H%M%S")

    if TARGET.exists():
        current_backup = rescue_backup_dir / f"admin_server_RESCUE_REPLACED_CURRENT_{stamp}.py"
        try:
            shutil.copy2(TARGET, current_backup)
            log(f"current target backed up to {current_backup}")
        except Exception as exc:
            log(f"could not backup current target: {exc!r}")

    shutil.copy2(src, TARGET)
    ok2, msg2 = compile_ok(TARGET)
    if not ok2:
        return False, f"הקובץ הועתק אבל נכשל בקומפילציה: {msg2}"

    log(f"RESTORED {src} -> {TARGET} | score={row['score']} | sha={row['sha']}")
    return True, f"שוחזר בהצלחה מתוך: {src}"


def start_rescue_ui(rows: list[dict], reason: str) -> None:
    try:
        from flask import Flask, redirect
    except Exception as exc:
        log(f"Cannot start rescue UI, Flask import failed: {exc!r}")
        while True:
            time.sleep(60)

    app = Flask(__name__)

    def html_page(msg: str = "") -> str:
        trs = []
        for i, r in enumerate(rows[:30], 1):
            dt = datetime.fromtimestamp(r["mtime"]).strftime("%Y-%m-%d %H:%M:%S") if r["mtime"] else "?"
            markers = ", ".join(r["markers"][:8]) or "-"
            kb = r["size"] / 1024
            trs.append(
                f"<tr><td>{i}</td><td>{r['score']}</td><td>{dt}</td><td>{kb:.1f}KB</td>"
                f"<td style='direction:ltr;text-align:left'>{r['path']}</td><td>{markers}</td>"
                f"<td><a class='btn' href='/restore/{i}'>שחזר</a></td></tr>"
            )
        table = "\n".join(trs)
        return f"""<!doctype html><html lang='he' dir='rtl'><meta charset='utf-8'>
<title>חילוץ פאנל חכם</title>
<style>
body{{font-family:Arial,sans-serif;background:#0f172a;color:#e5e7eb;margin:0;padding:24px}}
.box{{max-width:1180px;margin:auto;background:#111827;border:1px solid #334155;border-radius:18px;padding:22px}}
h1{{color:#38bdf8}} .warn{{background:#451a03;border:1px solid #f97316;padding:12px;border-radius:12px}}
.ok{{background:#052e16;border:1px solid #22c55e;padding:12px;border-radius:12px}}
table{{width:100%;border-collapse:collapse;margin-top:16px;font-size:13px}}td,th{{border-bottom:1px solid #334155;padding:8px;vertical-align:top}}
.btn{{display:inline-block;background:#16a34a;color:white;padding:8px 12px;border-radius:10px;text-decoration:none;font-weight:bold}}
.small{{opacity:.8;font-size:13px;direction:ltr;text-align:left;white-space:pre-wrap}}
</style><div class='box'>
<h1>מצב חילוץ פאנל חכם</h1>
<div class='warn'><strong>סיבה:</strong> {reason}</div>
{f"<div class='ok'>{msg}</div>" if msg else ""}
<p>בחר לשחזר רק מועמד עם ציון גבוה. בדרך כלל הגרסה הנכונה תהיה עם סימנים כמו <strong>/admin/usa</strong>, <strong>USA_TV</strong>, או <strong>v5</strong>.</p>
<table><tr><th>#</th><th>ציון</th><th>תאריך</th><th>גודל</th><th>קובץ</th><th>סימנים</th><th>פעולה</th></tr>{table}</table>
<p class='small'>לוג: {LOG_PATH}</p>
</div></html>"""

    @app.route("/")
    def index():
        return html_page()

    @app.route("/health")
    def health():
        return {"ok": True, "mode": "admin_server_PANEL_AUTO_RESCUE", "candidates": len(rows), "best_score": rows[0]["score"] if rows else None}

    @app.route("/restore/<int:index>")
    def restore(index: int):
        if index < 1 or index > len(rows):
            return html_page("מועמד לא קיים"), 404
        row = rows[index - 1]
        ok, msg = restore_candidate(row)
        if not ok:
            return html_page(msg), 500
        threading.Thread(target=lambda: (time.sleep(1), delayed_launch_restored_server(), time.sleep(1), os._exit(0)), daemon=True).start()
        return f"""<!doctype html><html lang='he' dir='rtl'><meta charset='utf-8'>
        <body style='font-family:Arial;background:#052e16;color:white;padding:30px'>
        <h1>שוחזר</h1><p>{msg}</p><p>הפאנל יופעל מחדש בעוד כמה שניות. רענן את הדף.</p>
        </body></html>"""

    log("starting emergency rescue UI on 0.0.0.0:8000")
    app.run(host="0.0.0.0", port=8000, debug=False)


def main() -> int:
    log("=== admin_server PANEL AUTO RESCUE started ===")
    rows = collect_candidates()
    log(f"candidates found: {len(rows)}")
    for i, r in enumerate(rows[:10], 1):
        log(f"candidate {i}: score={r['score']} mtime={datetime.fromtimestamp(r['mtime']).strftime('%Y-%m-%d %H:%M:%S') if r['mtime'] else '?'} sha={r['sha']} path={r['path']} markers={r['markers'][:6]}")

    if rows and rows[0]["score"] >= MIN_AUTO_SCORE:
        ok, msg = restore_candidate(rows[0])
        log(msg)
        if ok:
            delayed_launch_restored_server()
            log("restored server launch requested; rescue process exiting")
            time.sleep(1)
            return 0

    reason = "לא נמצא גיבוי עם ציון מספיק גבוה לשחזור אוטומטי. הפעלתי ממשק חילוץ ידני."
    if rows:
        reason += f" המועמד הכי טוב קיבל ציון {rows[0]['score']}."
    start_rescue_ui(rows, reason)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())