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())