C:\TripServer\backups\admin_server\admin_server_backup_20260701_105541_1.py
# -*- coding: utf-8 -*-
from flask import Flask, request, redirect, send_from_directory, make_response
from werkzeug.utils import secure_filename
import os
import time
import sys
import shutil
import py_compile
import threading
import html as html_lib
import glob
import zipfile
import subprocess
import hashlib
import json
app = Flask(__name__)
BASE_DIR = r"C:\TripServer"
FOLDERS = {
"JAPAN": os.path.join(BASE_DIR, "JAPAN"),
"USA": os.path.join(BASE_DIR, "USA"),
"USA_TV": os.path.join(BASE_DIR, "USA_TV"),
"Moneyapp": os.path.join(BASE_DIR, "Moneyapp"),
"MoneyHome": os.path.join(BASE_DIR, "MoneyHome"),
}
APP_FOLDERS = {"JAPAN", "USA", "USA_TV", "Moneyapp", "MoneyHome"}
JAPAN_PRESENTATION_FILES = {
"flight_intro": "japan-flight-intro.html",
"japan_experience": "japan-experience.html",
}
USA_PRESENTATION_FILENAME = "usa2027-presentation.html"
USA_MEDIA_IMAGE_DIR = os.path.join("media", "usa-presentation", "images")
USA_MEDIA_MUSIC_DIR = os.path.join("media", "usa-presentation", "music")
USA_MUSIC_FILES = {
"usa_music": os.path.join(USA_MEDIA_MUSIC_DIR, "usa-roadtrip-theme.mp3"),
"usa_music_part2": os.path.join(USA_MEDIA_MUSIC_DIR, "usa-part2-theme.mp3"),
}
USA_TARGETS = {
"phone": {
"folder": "USA",
"title": "📱 אייפון / שרת פרטי רגיל",
"short": "אייפון",
"public": "/USA/",
"desc": r"C:\TripServer\USA",
},
"tv": {
"folder": "USA_TV",
"title": "📺 טלוויזיה / TV 77OLED",
"short": "TV",
"public": "/USA_TV/",
"desc": r"C:\TripServer\USA_TV",
},
}
USA_IMAGE_KEYS = ({"intro", "budget", "roadtrip-map", "summary", "extra-big-sur", "extra-diner", "extra-death-valley", "extra-mono-lake"} | {f"day-{i:02d}" for i in range(1, 37)})
USA_IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp"}
USA_IMAGE_META = [
("intro", "פתיחה", "אמריקה על פול ווליום / תמונת Road Trip כללית"),
("roadtrip-map", "מפת המסלול", "מפת Road Trip / מבט כללי על המסלול"),
("day-01", "יום 1", "סן פרנסיסקו / נחיתה ופתיחה"),
("day-02", "יום 2", "סן פרנסיסקו + Alcatraz / מפרץ, אלקטרז, גשר הזהב"),
("day-03", "יום 3", "סן פרנסיסקו → מונטריי / כביש 1, חוף קליפורניה"),
("day-04", "יום 4", "Monterey Bay Whale Watch + Big Sur"),
("extra-big-sur", "שקף Big Sur", "Bixby Bridge / Big Sur / McWay Falls"),
("day-05", "יום 5", "Pismo → לוס אנג׳לס"),
("day-06", "יום 6", "Warner Bros + Beverly Hills + Griffith"),
("day-07", "יום 7", "לוס אנג׳לס → Zion"),
("extra-diner", "שקף Peggy Sue’s Diner", "דיינר אמריקאי קלאסי / עצירת Road Trip"),
("day-08", "יום 8", "Zion → Bryce → Page"),
("day-09", "יום 9", "Antelope Canyon + Horseshoe Bend"),
("day-10", "יום 10", "Page → Grand Canyon South Rim"),
("day-11", "יום 11", "Grand Canyon → Las Vegas"),
("day-12", "יום 12", "Vegas Outlets + Sphere"),
("day-13", "יום 13", "Las Vegas / יום חופשי"),
("day-14", "יום 14", "Las Vegas → Death Valley"),
("extra-death-valley", "שקף Death Valley", "Badwater Basin / מדבר / נופים דרמטיים"),
("day-15", "יום 15", "Death Valley → Mammoth Lakes"),
("extra-mono-lake", "שקף Mono Lake", "Mono Lake / Tioga Road / מעבר ליוסמיטי"),
("day-16", "יום 16", "Yosemite"),
("day-17", "יום 17", "Yosemite → San Francisco"),
("day-18", "יום 18", "טיסה לפלורידה / Orlando"),
("day-19", "יום 19", "Universal Orlando"),
("day-20", "יום 20", "Universal / פארקים"),
("day-21", "יום 21", "Orlando / מנוחה וקניות"),
("day-22", "יום 22", "Disney / פארק"),
("day-23", "יום 23", "Orlando"),
("day-24", "יום 24", "Orlando"),
("day-25", "יום 25", "Orlando → Miami"),
("day-26", "יום 26", "Miami"),
("day-27", "יום 27", "עלייה לקרוז"),
("day-28", "יום 28", "קרוז"),
("day-29", "יום 29", "קרוז / יום ים"),
("day-30", "יום 30", "קרוז / יום ים"),
("day-31", "יום 31", "CocoCay"),
("day-32", "יום 32", "Nassau"),
("day-33", "יום 33", "קרוז / יום ים"),
("day-34", "יום 34", "ירידה מהקרוז / Miami"),
("day-35", "יום 35", "Miami / קניות וסיום"),
("day-36", "יום 36", "חזרה הביתה"),
("budget", "תקציב", "שקף תקציב"),
("summary", "סיום", "תמונת סיום משפחתית"),
]
# Canonical admin path: future self-updates must always replace the real server file
# under C:\TripServer, not an accidental copy determined by the current working directory.
CANONICAL_ADMIN_SERVER_PATH = os.path.join(BASE_DIR, "admin_server.py")
RUNNING_SCRIPT_PATH = os.path.abspath(__file__)
ARGV_SCRIPT_PATH = os.path.abspath(sys.argv[0]) if sys.argv and sys.argv[0] else RUNNING_SCRIPT_PATH
ADMIN_SERVER_PATH = CANONICAL_ADMIN_SERVER_PATH
ADMIN_BACKUP_DIR = os.path.join(BASE_DIR, "backups", "admin_server")
ADMIN_UPLOAD_TMP_DIR = os.path.join(BASE_DIR, "tmp", "admin_server_uploads")
ADMIN_RESTART_CMD_PATH = os.path.join(BASE_DIR, "restart_admin_server.cmd")
ADMIN_RESTART_LOG_PATH = os.path.join(BASE_DIR, "admin_restart.log")
ADMIN_UPDATE_MARKER_PATH = os.path.join(BASE_DIR, "admin_update_marker.json")
ADMIN_PANEL_VERSION = "multi-page USA phone TV separated v5 canonical self-update"
ADMIN_MAX_UPLOAD_BYTES = 2 * 1024 * 1024
USA_IMAGES_ZIP_MAX_BYTES = 80 * 1024 * 1024
USA_IMAGES_ZIP_MAX_FILES = 180
USA_IMAGES_ZIP_MAX_UNCOMPRESSED_BYTES = 160 * 1024 * 1024
USA_IMAGES_ZIP_MAX_SINGLE_FILE_BYTES = 12 * 1024 * 1024
USA_PACKAGE_ZIP_MAX_BYTES = 220 * 1024 * 1024
USA_PACKAGE_ZIP_MAX_FILES = 360
USA_PACKAGE_ZIP_MAX_UNCOMPRESSED_BYTES = 320 * 1024 * 1024
for folder_path in FOLDERS.values():
os.makedirs(folder_path, exist_ok=True)
for target in ("USA", "USA_TV"):
os.makedirs(os.path.join(FOLDERS[target], USA_MEDIA_IMAGE_DIR), exist_ok=True)
os.makedirs(os.path.join(FOLDERS[target], USA_MEDIA_MUSIC_DIR), exist_ok=True)
os.makedirs(ADMIN_BACKUP_DIR, exist_ok=True)
os.makedirs(ADMIN_UPLOAD_TMP_DIR, exist_ok=True)
def no_cache_response(response):
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "0"
return response
def find_html_file(folder_path):
index_path = os.path.join(folder_path, "index.html")
if os.path.exists(index_path):
return "index.html"
if not os.path.isdir(folder_path):
return None
html_files = [f for f in os.listdir(folder_path) if f.lower().endswith((".html", ".htm"))]
return html_files[0] if html_files else None
def clean_wrong_main_files(folder_path):
if not os.path.isdir(folder_path):
return
wrong_names = {"html", "htm", "index", "index.htm", "download", "unknown"}
for name in os.listdir(folder_path):
full_path = os.path.join(folder_path, name)
if os.path.isfile(full_path) and name.lower() in wrong_names:
try:
os.remove(full_path)
except Exception:
pass
def save_uploaded_file(folder, uploaded_file):
if folder not in FOLDERS:
return None
folder_path = FOLDERS[folder]
os.makedirs(folder_path, exist_ok=True)
filename = "index.html"
uploaded_file.save(os.path.join(folder_path, filename))
clean_wrong_main_files(folder_path)
return filename
def normalize_usa_mode(mode):
mode = (mode or "").strip().lower()
if mode in ("phone", "usa", "iphone", "server", "private", "usa_presentation"):
return "phone"
if mode in ("tv", "usa_tv", "television", "oled"):
return "tv"
return None
def usa_target_folder(mode):
mode = normalize_usa_mode(mode)
if not mode:
return None
return FOLDERS[USA_TARGETS[mode]["folder"]]
def save_japan_presentation(presentation_key, uploaded_file):
if presentation_key not in JAPAN_PRESENTATION_FILES:
return None
folder = FOLDERS["JAPAN"]
os.makedirs(folder, exist_ok=True)
filename = JAPAN_PRESENTATION_FILES[presentation_key]
uploaded_file.save(os.path.join(folder, filename))
return filename
def save_usa_presentation(mode, uploaded_file):
folder = usa_target_folder(mode)
if not folder:
return None
os.makedirs(folder, exist_ok=True)
uploaded_file.save(os.path.join(folder, USA_PRESENTATION_FILENAME))
return USA_PRESENTATION_FILENAME
def save_usa_music(mode, music_key, uploaded_file):
folder = usa_target_folder(mode)
if not folder or music_key not in USA_MUSIC_FILES:
return None
rel_path = USA_MUSIC_FILES[music_key]
full_path = os.path.join(folder, rel_path)
os.makedirs(os.path.dirname(full_path), exist_ok=True)
uploaded_file.save(full_path)
return rel_path
def save_usa_presentation_image(mode, image_key, uploaded_file):
folder = usa_target_folder(mode)
if not folder or image_key not in USA_IMAGE_KEYS:
return None
image_dir = os.path.join(folder, USA_MEDIA_IMAGE_DIR)
os.makedirs(image_dir, exist_ok=True)
target_ext = ".jpg"
for old_ext in USA_IMAGE_EXTENSIONS:
old_path = os.path.join(image_dir, image_key + old_ext)
if os.path.exists(old_path):
try:
os.remove(old_path)
except Exception:
pass
target_path = os.path.join(image_dir, image_key + target_ext)
try:
from PIL import Image
uploaded_file.stream.seek(0)
img = Image.open(uploaded_file.stream)
if img.mode not in ("RGB", "L"):
img = img.convert("RGB")
elif img.mode == "L":
img = img.convert("RGB")
img.save(target_path, "JPEG", quality=90, optimize=True)
except Exception:
try:
uploaded_file.stream.seek(0)
except Exception:
pass
uploaded_file.save(target_path)
return os.path.join(USA_MEDIA_IMAGE_DIR, image_key + target_ext)
def save_usa_presentation_images_zip(mode, uploaded_file):
folder = usa_target_folder(mode)
if not folder:
raise ValueError("bad_usa_mode")
image_dir = os.path.join(folder, USA_MEDIA_IMAGE_DIR)
os.makedirs(image_dir, exist_ok=True)
stamp = time.strftime("%Y%m%d_%H%M%S")
tmp_zip = os.path.join(ADMIN_UPLOAD_TMP_DIR, f"usa_images_{mode}_{stamp}.zip")
os.makedirs(ADMIN_UPLOAD_TMP_DIR, exist_ok=True)
uploaded_file.save(tmp_zip)
zip_size = os.path.getsize(tmp_zip)
if zip_size > USA_IMAGES_ZIP_MAX_BYTES:
try:
os.remove(tmp_zip)
except Exception:
pass
raise ValueError(f"zip_size_exceeded:{zip_size}")
imported, skipped, seen = [], [], set()
try:
with zipfile.ZipFile(tmp_zip, "r") as zf:
infos = [info for info in zf.infolist() if not info.is_dir()]
if len(infos) > USA_IMAGES_ZIP_MAX_FILES:
raise ValueError(f"too_many_files:{len(infos)}")
total_uncompressed = sum(max(0, info.file_size) for info in infos)
if total_uncompressed > USA_IMAGES_ZIP_MAX_UNCOMPRESSED_BYTES:
raise ValueError(f"zip_uncompressed_size_exceeded:{total_uncompressed}")
for info in infos:
raw_name = info.filename.replace("\\", "/")
base = os.path.basename(raw_name)
if not base or base.startswith(".") or raw_name.startswith("__MACOSX/"):
continue
if info.file_size > USA_IMAGES_ZIP_MAX_SINGLE_FILE_BYTES:
skipped.append(base + " — גדול מדי")
continue
stem, ext = os.path.splitext(base)
stem = stem.strip()
ext = ext.lower()
if ext not in USA_IMAGE_EXTENSIONS:
skipped.append(base + " — סיומת לא נתמכת")
continue
if stem not in USA_IMAGE_KEYS:
skipped.append(base + " — שם לא שייך למצגת")
continue
if stem in seen:
skipped.append(base + " — כפילות ב־ZIP")
continue
seen.add(stem)
for old_ext in USA_IMAGE_EXTENSIONS:
old_path = os.path.join(image_dir, stem + old_ext)
if os.path.exists(old_path):
try:
os.remove(old_path)
except Exception:
pass
target = os.path.join(image_dir, stem + ".jpg")
try:
from PIL import Image
with zf.open(info, "r") as source:
img = Image.open(source)
img.verify()
with zf.open(info, "r") as source:
img = Image.open(source)
if img.mode not in ("RGB", "L"):
img = img.convert("RGB")
elif img.mode == "L":
img = img.convert("RGB")
img.save(target, "JPEG", quality=90, optimize=True, progressive=True)
except Exception:
with zf.open(info, "r") as source, open(target, "wb") as dest:
shutil.copyfileobj(source, dest)
imported.append(stem + ".jpg")
finally:
try:
os.remove(tmp_zip)
except Exception:
pass
imported.sort()
skipped.sort()
missing = sorted(key + ".jpg" for key in USA_IMAGE_KEYS if key + ".jpg" not in imported)
return imported, skipped, missing
def validate_admin_server_upload(tmp_path):
size = os.path.getsize(tmp_path)
if size <= 0:
return False, "הקובץ ריק."
if size > ADMIN_MAX_UPLOAD_BYTES:
return False, f"הקובץ גדול מדי: {size:,} bytes. המגבלה היא {ADMIN_MAX_UPLOAD_BYTES:,} bytes."
try:
py_compile.compile(tmp_path, doraise=True)
except py_compile.PyCompileError as exc:
return False, "בדיקת קומפילציה נכשלה:\n" + str(exc)
except Exception as exc:
return False, "שגיאה בבדיקת הקובץ:\n" + repr(exc)
try:
with open(tmp_path, "r", encoding="utf-8", errors="ignore") as fh:
content = fh.read(500000)
except Exception as exc:
return False, "לא ניתן לקרוא את הקובץ:\n" + repr(exc)
required_tokens = ["Flask", "@app.route", "app.run"]
missing = [token for token in required_tokens if token not in content]
if missing:
return False, "הקובץ עבר קומפילציה, אבל לא נראה כמו admin_server.py של TripServer. חסרים סימנים: " + ", ".join(missing)
return True, "OK"
def admin_status_page(title, message, ok=True, details="", back="/admin"):
color = "#16a34a" if ok else "#dc2626"
bg = "#052e1b" if ok else "#3f1d1d"
safe_title = html_lib.escape(str(title))
safe_message = html_lib.escape(str(message)).replace("\n", "<br>")
safe_details = html_lib.escape(str(details)).replace("\n", "<br>") if details else ""
safe_back = html_lib.escape(back or "/admin")
details_html = f"<pre>{safe_details}</pre>" if safe_details else ""
return f"""
<!DOCTYPE html><html lang="he" dir="rtl"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>{safe_title}</title>
<style>body{{margin:0;font-family:Arial;background:#0f172a;color:white;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}}.card{{width:min(860px,94vw);background:#1e293b;border-radius:24px;padding:26px;box-shadow:0 18px 70px rgba(0,0,0,.35)}}.badge{{display:inline-block;background:{bg};border:1px solid {color};color:white;border-radius:999px;padding:8px 14px;font-weight:bold;margin-bottom:12px}}h1{{margin:0 0 12px;font-size:30px}}p{{line-height:1.7;color:#e2e8f0}}pre{{white-space:pre-wrap;direction:ltr;text-align:left;background:#020617;color:#fde68a;border:1px solid #334155;border-radius:14px;padding:14px;overflow:auto;max-height:360px}}a,button{{display:inline-block;margin:8px 0 0 8px;background:#2563eb;color:white;border:0;border-radius:12px;padding:12px 16px;text-decoration:none;font-weight:bold;cursor:pointer}}button.restart{{background:#ea580c}}form{{display:inline}}</style></head>
<body><div class="card"><div class="badge">TripServer Admin</div><h1>{safe_title}</h1><p>{safe_message}</p>{details_html}<a href="{safe_back}">חזרה</a><a href="/admin">דף ראשי</a><form action="/restart_admin_server" method="post" onsubmit="return confirm('לאתחל את הפאנל עכשיו?')"><button class="restart">אתחל פאנל עכשיו</button></form></div></body></html>"""
def file_sha256(path):
try:
h = hashlib.sha256()
with open(path, "rb") as fh:
for chunk in iter(lambda: fh.read(1024 * 1024), b""):
h.update(chunk)
return h.hexdigest()
except Exception:
return ""
def admin_script_paths_to_update():
"""Return every plausible admin script path that should be replaced.
The old panel used os.path.abspath(__file__), which can point to the file
that happened to start the process. For reliable future self-updates we
update both the canonical TripServer admin file and the currently running
script path when they differ.
"""
candidates = [CANONICAL_ADMIN_SERVER_PATH, RUNNING_SCRIPT_PATH, ARGV_SCRIPT_PATH]
out = []
for path in candidates:
if not path:
continue
path = os.path.abspath(path)
if not path.lower().endswith(".py"):
continue
if path not in out:
out.append(path)
return out
def write_admin_update_marker(upload_hash, updated_paths, backup_paths):
try:
payload = {
"ok": True,
"version": ADMIN_PANEL_VERSION,
"time": time.strftime("%Y-%m-%d %H:%M:%S"),
"pid_that_updated": os.getpid(),
"cwd": os.getcwd(),
"running_script_path": RUNNING_SCRIPT_PATH,
"argv_script_path": ARGV_SCRIPT_PATH,
"canonical_admin_server_path": CANONICAL_ADMIN_SERVER_PATH,
"upload_sha256": upload_hash,
"updated_paths": updated_paths,
"backup_paths": backup_paths,
}
with open(ADMIN_UPDATE_MARKER_PATH, "w", encoding="utf-8") as fh:
json.dump(payload, fh, ensure_ascii=False, indent=2)
except Exception as exc:
log_admin_restart("failed writing admin_update_marker: " + repr(exc))
def backup_current_admin_server(prefix="admin_server_backup"):
os.makedirs(ADMIN_BACKUP_DIR, exist_ok=True)
stamp = time.strftime("%Y%m%d_%H%M%S")
source = ADMIN_SERVER_PATH if os.path.exists(ADMIN_SERVER_PATH) else RUNNING_SCRIPT_PATH
backup_path = os.path.join(ADMIN_BACKUP_DIR, f"{prefix}_{stamp}.py")
if os.path.exists(source):
shutil.copy2(source, backup_path)
else:
with open(backup_path, "w", encoding="utf-8") as fh:
fh.write("# no previous admin_server.py file was found during backup\n")
return backup_path
def latest_admin_backup():
os.makedirs(ADMIN_BACKUP_DIR, exist_ok=True)
backups = glob.glob(os.path.join(ADMIN_BACKUP_DIR, "admin_server_backup_*.py"))
return max(backups, key=os.path.getmtime) if backups else None
def log_admin_restart(message):
try:
os.makedirs(BASE_DIR, exist_ok=True)
with open(ADMIN_RESTART_LOG_PATH, "a", encoding="utf-8") as fh:
fh.write(time.strftime("%Y-%m-%d %H:%M:%S") + " | " + str(message) + "\n")
except Exception:
pass
def restart_process_later():
time.sleep(0.8)
script_path = os.path.abspath(ADMIN_SERVER_PATH)
python_exe = sys.executable or "python"
work_dir = os.path.dirname(script_path) or BASE_DIR
try:
log_admin_restart(f"restart requested | script={script_path} | python={python_exe} | argv={sys.argv}")
if os.name == "nt":
cmd = "\r\n".join([
"@echo off",
'cd /d "{}"'.format(work_dir),
"timeout /t 2 /nobreak >nul",
'start "TripServer Admin" /min "{}" "{}"'.format(python_exe, script_path),
""
])
with open(ADMIN_RESTART_CMD_PATH, "w", encoding="utf-8") as fh:
fh.write(cmd)
subprocess.Popen(["cmd", "/c", ADMIN_RESTART_CMD_PATH], cwd=work_dir, close_fds=True)
os._exit(0)
else:
subprocess.Popen([python_exe, script_path], cwd=work_dir, close_fds=True, start_new_session=True)
os._exit(0)
except Exception as exc:
log_admin_restart("restart failed: " + repr(exc))
try:
os.execv(python_exe, [python_exe, script_path])
except Exception as exc2:
log_admin_restart("execv fallback failed: " + repr(exc2))
os._exit(1)
def serve_app(folder):
file_name = find_html_file(FOLDERS[folder])
if not file_name:
return f"No HTML file found in {folder} folder", 404
response = make_response(send_from_directory(FOLDERS[folder], file_name))
return no_cache_response(response)
def css():
return """
body{margin:0;font-family:Arial,Helvetica,sans-serif;background:#0f172a;color:white;min-height:100vh}.wrap{width:min(1180px,92%);margin:auto;padding:34px 0 60px}.top{display:flex;justify-content:space-between;gap:14px;align-items:center;margin-bottom:22px}.crumbs a,.back{color:#93c5fd;text-decoration:none;font-weight:900}.hero{text-align:center;margin:18px 0 28px}.hero h1{font-size:clamp(30px,5vw,52px);margin:0 0 8px}.hero p{color:#cbd5e1;line-height:1.65;margin:0}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:18px}.card{display:block;background:linear-gradient(180deg,#1e293b,#111827);border:1px solid #334155;border-radius:24px;padding:24px;text-decoration:none;color:white;box-shadow:0 14px 38px rgba(0,0,0,.25);min-height:145px}.card:hover{border-color:#60a5fa;transform:translateY(-2px)}.icon{font-size:40px;margin-bottom:12px}.card h2{margin:0 0 10px;font-size:24px}.card p{margin:0;color:#cbd5e1;line-height:1.5}.box{background:#1e293b;border:1px solid #334155;border-radius:22px;padding:24px;margin-bottom:22px}.box h2{margin-top:0}.box h3{margin-bottom:10px}.note{color:#cbd5e1;font-size:14px;line-height:1.6}.warning{background:#3f1d1d;border:1px solid #7f1d1d;color:#fecaca;border-radius:14px;padding:12px;line-height:1.6;margin-bottom:14px}.good{background:#143326;border:1px solid #1f7a54;color:#bbf7d0;border-radius:14px;padding:12px;line-height:1.6;margin:12px 0}.path{direction:ltr;text-align:left;background:#020617;color:#fde68a;border:1px solid #334155;border-radius:12px;padding:10px;display:block;margin:10px 0;overflow:auto}input[type=file],select{width:100%;box-sizing:border-box;padding:14px;background:#0f172a;color:white;border-radius:12px;border:1px solid #334155}button,.btn{display:inline-block;margin-top:14px;border:0;background:#2563eb;color:white;padding:14px 20px;border-radius:12px;font-size:16px;font-weight:900;cursor:pointer;text-decoration:none}.btn.secondary{background:#334155}.btn.orange,button.orange{background:#ea580c}.preview-links{display:flex;gap:10px;flex-wrap:wrap;margin-top:14px}.preview-links a{display:inline-block;color:white;background:#334155;border:1px solid #475569;border-radius:12px;padding:10px 14px;text-decoration:none;font-weight:bold}.image-upload-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:12px;margin-top:16px}.image-upload-card{margin:0}.image-file-input{position:absolute;opacity:0;width:1px;height:1px;pointer-events:none}.image-upload-label{display:block;height:100%;cursor:pointer;background:#0f172a;border:1px solid #334155;border-radius:18px;padding:15px;transition:.18s transform,.18s border-color,.18s background;box-shadow:0 8px 22px rgba(0,0,0,.14)}.image-upload-label:hover{transform:translateY(-2px);border-color:#60a5fa;background:#13213a}.image-key{display:inline-flex;background:#1d4ed8;color:#dbeafe;border-radius:999px;padding:4px 9px;font-size:12px;font-weight:900;margin-bottom:8px}.image-title{display:block;color:#fff;font-size:17px;font-weight:950;margin-bottom:7px;line-height:1.25}.image-desc{display:block;color:#cbd5e1;font-size:14px;line-height:1.45;min-height:42px}.upload-status{display:block;margin-top:12px;color:#fde68a;font-size:13px;font-weight:900;border-top:1px solid #334155;padding-top:10px}.split{display:grid;grid-template-columns:1fr 1fr;gap:16px}@media(max-width:740px){.split{grid-template-columns:1fr}.top{display:block}.image-upload-grid{grid-template-columns:1fr}.image-title{font-size:16px}.image-desc{min-height:auto}}
"""
def page(title, content, subtitle="", back="/admin"):
safe_title = html_lib.escape(title)
safe_subtitle = html_lib.escape(subtitle)
back_html = f'<a class="back" href="{html_lib.escape(back)}">⬅ חזרה</a>' if back else ""
return f"""
<!DOCTYPE html><html lang="he" dir="rtl"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>{safe_title}</title><style>{css()}</style></head>
<body><div class="wrap"><div class="top"><div>{back_html}</div><div class="crumbs"><a href="/admin">דף ראשי</a></div></div><div class="hero"><h1>{safe_title}</h1><p>{safe_subtitle}</p></div>{content}</div></body></html>"""
def card(icon, title, text, href):
return f'<a class="card" href="{html_lib.escape(href)}"><div class="icon">{icon}</div><h2>{html_lib.escape(title)}</h2><p>{html_lib.escape(text)}</p></a>'
def upload_box(title, action, button, accept=".html,.htm", note="", extra=""):
return f"""<div class="box"><h2>{title}</h2>{extra}<form action="{html_lib.escape(action)}" method="post" enctype="multipart/form-data"><input type="file" name="file" accept="{html_lib.escape(accept)}"><button>{html_lib.escape(button)}</button></form>{('<p class="note">'+note+'</p>') if note else ''}</div>"""
def usa_preview_links(mode):
t = USA_TARGETS[mode]
public = t["public"]
return f"""<div class="preview-links"><a href="{public}" target="_blank">פתיחת קובץ טיול</a><a href="{public}{USA_PRESENTATION_FILENAME}?from=admin" target="_blank">בדיקת מצגת</a></div>"""
def image_cards(mode):
cards = []
for key, title, desc in USA_IMAGE_META:
safe_key = html_lib.escape(key)
safe_title = html_lib.escape(title)
safe_desc = html_lib.escape(desc)
input_id = f"image-file-{mode}-{key}"
cards.append(f"""<form class="image-upload-card" action="/upload_usa_image/{mode}" method="post" enctype="multipart/form-data"><input type="hidden" name="image_key" value="{safe_key}"><input class="image-file-input" id="{input_id}" type="file" name="file" accept=".jpg,.jpeg,.png,.webp,image/*" onchange="if(this.files && this.files.length){{this.closest('form').querySelector('.upload-status').textContent='מעלה תמונה...';this.closest('form').submit();}}"><label for="{input_id}" class="image-upload-label"><span class="image-key">{safe_key}</span><span class="image-title">{safe_title}</span><span class="image-desc">{safe_desc}</span><span class="upload-status">לחץ כאן ובחר תמונה</span></label></form>""")
return "<div class=\"image-upload-grid\">" + "".join(cards) + "</div>"
def admin_main_html():
content = '<div class="grid">' + "".join([
card("🇺🇸", "טיול ארצות הברית 27", "כניסה לדף משנה ייעודי לאייפון, TV, מצגות, תמונות ושירים.", "/admin/usa"),
card("🇯🇵", "טיול יפן 26", "קובץ הטיול ומצגות יפן.", "/admin/japan"),
card("💰", "אפליקציית תקציב חופשה", "עדכון אפליקציית תקציב הטיולים.", "/admin/moneyapp"),
card("🏠", "אפליקציית תקציב חודשי משפחתי", "עדכון אפליקציית התקציב המשפחתי.", "/admin/moneyhome"),
card("🛠️", "עדכון פנל החכם", "העלאת admin_server.py, גיבוי, שחזור ואתחול.", "/admin/update"),
]) + '</div>'
return page("פנל חכם", content, "דף ראשי נקי — בחר אזור ניהול", back="/")
@app.route("/admin")
@app.route("/admin/")
def admin():
return admin_main_html()
@app.route("/")
def home():
content = '<div class="grid">' + "".join([
card("📁", "פנל חכם", "ניהול ועדכון קבצים.", "/admin"),
card("🇺🇸", "טיול ארה״ב — אייפון", "פתיחת קובץ הטיול הרגיל.", "/USA/"),
card("📺", "טיול ארה״ב — TV", "פתיחת גרסת מסך טלוויזיה.", "/USA_TV/"),
card("🇯🇵", "טיול יפן", "לוז, ניווטים ומסמכי הטיול.", "/JAPAN/"),
card("💰", "תקציב טיולים", "ניהול תקציב חופשות.", "/Moneyapp/"),
card("🏠", "תקציב משפחתי", "ניהול תקציב חודשי.", "/MoneyHome/"),
]) + '</div>'
return page("TripServer", content, "מסך פתיחה", back="")
@app.route("/admin/usa")
def admin_usa():
content = '<div class="grid">' + "".join([
card("📱", "עדכון קובץ טיול ארצות הברית למסך אייפון", r"יעד: C:\TripServer\USA\index.html", "/admin/usa/trip/phone"),
card("🎬", "עדכון מצגת ארצות הברית למסך אייפון", r"יעד: C:\TripServer\USA\usa2027-presentation.html", "/admin/usa/presentation/phone"),
card("📺", "עדכון קובץ טיול ארצות הברית למסך טלוויזיה", r"יעד: C:\TripServer\USA_TV\index.html", "/admin/usa/trip/tv"),
card("🎞️", "עדכון מצגת ארצות הברית למסך טלוויזיה", r"יעד: C:\TripServer\USA_TV\usa2027-presentation.html", "/admin/usa/presentation/tv"),
]) + '</div>'
return page("ארצות הברית 2027", content, "בחירה נפרדת לפי יעד תצוגה — אייפון מול TV", back="/admin")
@app.route("/admin/usa/trip/<mode>")
def admin_usa_trip(mode):
mode = normalize_usa_mode(mode)
if not mode:
return "Bad USA mode", 404
t = USA_TARGETS[mode]
action = "/upload/" + t["folder"]
target_path = t["desc"] + r"\index.html"
extra = f'<div class="good">זה דף עדכון קובץ הטיול בלבד עבור <strong>{t["title"]}</strong>.</div><code class="path">{target_path}</code>'
content = upload_box(f"{t['title']} — קובץ טיול", action, "העלה קובץ טיול", note="הקובץ יישמר בשם index.html בנתיב שמופיע למעלה.", extra=extra)
content += usa_preview_links(mode)
return page(f"קובץ טיול ארה״ב — {t['short']}", content, "אין כאן מצגות, תמונות או שירים — רק קובץ הטיול", back="/admin/usa")
@app.route("/admin/usa/presentation/<mode>")
def admin_usa_presentation(mode):
mode = normalize_usa_mode(mode)
if not mode:
return "Bad USA mode", 404
t = USA_TARGETS[mode]
target_root = t["desc"]
content = f"""<div class="box"><h2>{t['title']} — מצגת, תמונות ושירים</h2><div class="good">כל מה שנמצא בדף הזה נשמר רק תחת היעד הזה, ללא ערבוב בין אייפון לבין TV.</div><code class="path">{target_root}</code>{usa_preview_links(mode)}</div>"""
content += upload_box("קובץ מצגת", f"/upload_usa_presentation/{mode}", "העלה קובץ מצגת", note=f"יישמר בשם {USA_PRESENTATION_FILENAME}")
content += upload_box("ZIP תמונות מלא", f"/upload_usa_images_zip/{mode}", "העלה ZIP תמונות", accept=".zip,application/zip,application/x-zip-compressed", note="התמונות יישמרו בתיקיית media/usa-presentation/images של היעד הנוכחי בלבד.")
content += """<div class="box"><h2>תמונות בודדות לפי שקף</h2><p class="note">אפשר להחליף נקודתית כל תמונה. הקובץ יישמר כ־JPG בשם השקף.</p>""" + image_cards(mode) + "</div>"
content += f"""<div class="box"><h2>שירים למצגת</h2><div class="split"><div><h3>שיר חלק 1</h3><form action="/upload_usa_music/{mode}/usa_music" method="post" enctype="multipart/form-data"><input type="file" name="file" accept=".mp3,audio/mpeg"><button>העלה שיר חלק 1</button></form><p class="note">usa-roadtrip-theme.mp3</p></div><div><h3>שיר חלק 2</h3><form action="/upload_usa_music/{mode}/usa_music_part2" method="post" enctype="multipart/form-data"><input type="file" name="file" accept=".mp3,audio/mpeg"><button>העלה שיר חלק 2</button></form><p class="note">usa-part2-theme.mp3</p></div></div></div>"""
if mode == "tv":
content += upload_box("ZIP מלא של תיקיית USA_TV", "/upload_usa_tv_package_zip", "העלה ZIP מלא של USA_TV", accept=".zip,application/zip,application/x-zip-compressed", note="מחליף את כל תיקיית USA_TV לאחר יצירת גיבוי. ה־ZIP חייב לכלול index.html ו־usa2027-presentation.html.", extra="<div class='warning'>זה אזור TV בלבד. הוא לא משנה את תיקיית USA של האייפון.</div>")
return page(f"מצגת ארה״ב — {t['short']}", content, "מצגת + מדיה ביעד נפרד", back="/admin/usa")
@app.route("/admin/japan")
def admin_japan():
content = upload_box("קובץ טיול יפן", "/upload/JAPAN", "העלה קובץ טיול יפן", note="יישמר כ־index.html בתיקיית JAPAN.")
content += upload_box("פתיח טיסה יפן", "/upload_presentation/flight_intro", "העלה פתיח טיסה", note="יישמר כ־japan-flight-intro.html")
content += upload_box("מצגת יפן", "/upload_presentation/japan_experience", "העלה מצגת יפן", note="יישמר כ־japan-experience.html")
content += '<div class="preview-links"><a href="/JAPAN/" target="_blank">פתיחת יפן</a><a href="/JAPAN/japan-flight-intro.html" target="_blank">בדיקת פתיח</a><a href="/JAPAN/japan-experience.html" target="_blank">בדיקת מצגת</a></div>'
return page("טיול יפן 2026", content, "קובץ טיול ומצגות", back="/admin")
@app.route("/admin/moneyapp")
def admin_moneyapp():
content = upload_box("אפליקציית תקציב חופשה", "/upload/Moneyapp", "העלה אפליקציית תקציב חופשה", note="יישמר כ־index.html בתיקיית Moneyapp.")
content += '<div class="preview-links"><a href="/Moneyapp/" target="_blank">פתיחת אפליקציית תקציב חופשה</a></div>'
return page("אפליקציית תקציב חופשה", content, "עדכון קובץ האפליקציה", back="/admin")
@app.route("/admin/moneyhome")
def admin_moneyhome():
content = upload_box("אפליקציית תקציב חודשי משפחתי", "/upload/MoneyHome", "העלה אפליקציית תקציב משפחתי", note="יישמר כ־index.html בתיקיית MoneyHome.")
content += '<div class="preview-links"><a href="/MoneyHome/" target="_blank">פתיחת אפליקציית תקציב משפחתי</a></div>'
return page("אפליקציית תקציב חודשי משפחתי", content, "עדכון קובץ האפליקציה", back="/admin")
@app.route("/admin/update")
def admin_update():
content = """<div class="box"><h2>עדכון פנל החכם</h2><div class="warning">העלאת admin_server.py מחליפה את הפאנל הפעיל רק לאחר בדיקת קומפילציה ויצירת גיבוי.</div><form action="/upload_admin_server" method="post" enctype="multipart/form-data"><input type="file" name="file" accept=".py"><button class="orange">העלה admin_server.py</button></form><div class="preview-links"><form action="/restart_admin_server" method="post"><button class="orange">אתחל פאנל</button></form><form action="/restore_admin_server_backup" method="post"><button>שחזר גיבוי אחרון</button></form></div></div>"""
return page("עדכון פנל החכם", content, "עדכון עצמי, גיבויים ואתחול", back="/admin")
@app.route("/health")
def health():
marker = None
try:
if os.path.exists(ADMIN_UPDATE_MARKER_PATH):
with open(ADMIN_UPDATE_MARKER_PATH, "r", encoding="utf-8") as fh:
marker = json.load(fh)
except Exception as exc:
marker = {"read_error": repr(exc)}
return {
"ok": True,
"service": "TripServer Admin",
"version": ADMIN_PANEL_VERSION,
"usa_tv": True,
"pid": os.getpid(),
"cwd": os.getcwd(),
"admin_server_path": ADMIN_SERVER_PATH,
"running_script_path": RUNNING_SCRIPT_PATH,
"argv_script_path": ARGV_SCRIPT_PATH,
"admin_sha256": file_sha256(ADMIN_SERVER_PATH),
"running_sha256": file_sha256(RUNNING_SCRIPT_PATH),
"update_marker": marker,
}
@app.route("/upload_admin_server", methods=["POST"])
def upload_admin_server():
file = request.files.get("file")
if not file or file.filename == "":
return admin_status_page("לא נבחר קובץ", "בחר קובץ admin_server.py להעלאה.", ok=False, back="/admin/update"), 400
original_name = file.filename or ""
if not original_name.lower().endswith(".py"):
return admin_status_page("קובץ לא תקין", "ניתן להעלות כאן רק קובץ Python עם סיומת .py", ok=False, back="/admin/update"), 400
if request.content_length and request.content_length > ADMIN_MAX_UPLOAD_BYTES + 250000:
return admin_status_page("קובץ גדול מדי", f"הקובץ גדול מהמותר. המגבלה היא בערך {ADMIN_MAX_UPLOAD_BYTES:,} bytes.", ok=False, back="/admin/update"), 413
os.makedirs(ADMIN_UPLOAD_TMP_DIR, exist_ok=True)
stamp = time.strftime("%Y%m%d_%H%M%S")
tmp_path = os.path.join(ADMIN_UPLOAD_TMP_DIR, f"admin_server_upload_{stamp}.py")
try:
file.save(tmp_path)
valid, details = validate_admin_server_upload(tmp_path)
if not valid:
try:
os.remove(tmp_path)
except Exception:
pass
return admin_status_page("העלאת admin_server.py נדחתה", "הקובץ הקיים לא הוחלף. נמצאה בעיה בקובץ שהעלית.", ok=False, details=details, back="/admin/update"), 400
upload_hash = file_sha256(tmp_path)
paths_to_update = admin_script_paths_to_update()
backup_paths = []
updated_paths = []
errors = []
os.makedirs(ADMIN_BACKUP_DIR, exist_ok=True)
for target_path in paths_to_update:
try:
os.makedirs(os.path.dirname(target_path), exist_ok=True)
if os.path.exists(target_path):
backup_path = os.path.join(ADMIN_BACKUP_DIR, f"admin_server_backup_{stamp}_{len(backup_paths)+1}.py")
shutil.copy2(target_path, backup_path)
backup_paths.append(backup_path)
shutil.copy2(tmp_path, target_path)
written_hash = file_sha256(target_path)
if written_hash != upload_hash:
errors.append(f"{target_path}: sha256 mismatch after copy")
else:
updated_paths.append(target_path)
except Exception as exc:
errors.append(f"{target_path}: {repr(exc)}")
try:
os.remove(tmp_path)
except Exception:
pass
if errors or not updated_paths:
return admin_status_page(
"שגיאה בעדכון admin_server.py",
"הקובץ החדש עבר קומפילציה, אבל לא הצלחנו לוודא שהוא נכתב לכל נתיבי ההפעלה הדרושים.",
ok=False,
details="נתיבי יעד:\n" + "\n".join(paths_to_update) + "\n\nשגיאות:\n" + "\n".join(errors),
back="/admin/update"
), 500
write_admin_update_marker(upload_hash, updated_paths, backup_paths)
log_admin_restart("admin upload completed | hash=" + upload_hash + " | updated=" + repr(updated_paths))
return admin_status_page(
"admin_server.py עודכן בהצלחה",
"הקובץ החדש עבר קומפילציה, נכתב לנתיב הקנוני וגם לנתיב שמריץ כרגע את הפאנל, וה־SHA256 אומת. לחץ אתחל פאנל כדי להפעיל אותו.",
ok=True,
details=(
"גרסה חדשה:\n" + ADMIN_PANEL_VERSION +
"\n\nSHA256:\n" + upload_hash +
"\n\nנתיבים שעודכנו:\n" + "\n".join(updated_paths) +
"\n\nגיבויים:\n" + ("\n".join(backup_paths) if backup_paths else "לא היה קובץ קודם לגיבוי") +
"\n\nקובץ סימון:\n" + ADMIN_UPDATE_MARKER_PATH
),
back="/admin/update"
)
except Exception as exc:
try:
if os.path.exists(tmp_path):
os.remove(tmp_path)
except Exception:
pass
return admin_status_page("שגיאה בעדכון admin_server.py", "הקובץ הקיים לא הוחלף.", ok=False, details=repr(exc), back="/admin/update"), 500
@app.route("/restart_admin_server", methods=["POST"])
def restart_admin_server():
threading.Thread(target=restart_process_later, daemon=True).start()
return admin_status_page("הפאנל מופעל מחדש", "המתן 5–10 שניות ואז רענן את הפאנל.", ok=True, back="/admin/update")
@app.route("/restore_admin_server_backup", methods=["POST"])
def restore_admin_server_backup():
backup_path = latest_admin_backup()
if not backup_path:
return admin_status_page("אין גיבוי לשחזור", "לא נמצא קובץ גיבוי בתיקיית הגיבויים.", ok=False, back="/admin/update"), 404
try:
valid, details = validate_admin_server_upload(backup_path)
if not valid:
return admin_status_page("הגיבוי האחרון לא תקין", "הגיבוי לא שוחזר כי הוא לא עבר בדיקת תקינות.", ok=False, details=details, back="/admin/update"), 400
safety_backup = backup_current_admin_server("admin_server_before_restore")
shutil.copy2(backup_path, ADMIN_SERVER_PATH)
return admin_status_page("שחזור גיבוי הסתיים", "הגיבוי האחרון שוחזר אל admin_server.py. לחץ אתחל פאנל כדי להפעיל אותו.", ok=True, details=f"שוחזר מתוך:\n{backup_path}\n\nגיבוי בטיחות:\n{safety_backup}", back="/admin/update")
except Exception as exc:
return admin_status_page("שגיאה בשחזור גיבוי", "השחזור לא הושלם.", ok=False, details=repr(exc), back="/admin/update"), 500
@app.route("/upload/<folder>", methods=["POST"])
def upload(folder):
if folder not in FOLDERS:
return "Folder not found", 404
file = request.files.get("file")
if not file or file.filename == "":
return "No file selected", 400
save_uploaded_file(folder, file)
return redirect(request.referrer or "/admin")
@app.route("/upload_presentation/<presentation_key>", methods=["POST"])
def upload_presentation(presentation_key):
file = request.files.get("file")
if not file or file.filename == "":
return "No file selected", 400
filename = save_japan_presentation(presentation_key, file)
if not filename:
return "Presentation type not found", 404
return redirect(request.referrer or "/admin/japan")
@app.route("/upload_usa_presentation/<mode>", methods=["POST"])
def upload_usa_presentation(mode):
mode = normalize_usa_mode(mode)
if not mode:
return "USA presentation target not found", 404
file = request.files.get("file")
if not file or file.filename == "":
return "No file selected", 400
filename = save_usa_presentation(mode, file)
if not filename:
return "USA presentation type not found", 404
return redirect(request.referrer or f"/admin/usa/presentation/{mode}")
@app.route("/upload_usa_image/<mode>", methods=["POST"])
def upload_usa_image_mode(mode):
mode = normalize_usa_mode(mode)
if not mode:
return "USA image target not found", 404
image_key = request.form.get("image_key", "").strip()
file = request.files.get("file")
if not file or file.filename == "":
return "No file selected", 400
filename = save_usa_presentation_image(mode, image_key, file)
if not filename:
return "USA image key not found", 404
return redirect(request.referrer or f"/admin/usa/presentation/{mode}")
@app.route("/upload_usa_image", methods=["POST"])
def upload_usa_image_legacy():
return upload_usa_image_mode("phone")
@app.route("/upload_usa_images_zip/<mode>", methods=["POST"])
def upload_usa_images_zip_mode(mode):
mode = normalize_usa_mode(mode)
if not mode:
return "USA image target not found", 404
file = request.files.get("file")
if not file or file.filename == "":
return "No file selected", 400
original_name = file.filename or ""
if not original_name.lower().endswith(".zip"):
return admin_status_page("קובץ לא תקין", "ניתן להעלות כאן רק קובץ ZIP של תמונות מצגת.", ok=False, back=f"/admin/usa/presentation/{mode}"), 400
if request.content_length and request.content_length > USA_IMAGES_ZIP_MAX_BYTES + 1024 * 1024:
return admin_status_page("קובץ גדול מדי", f"קובץ התמונות גדול מהמגבלה: {USA_IMAGES_ZIP_MAX_BYTES:,} bytes.", ok=False, back=f"/admin/usa/presentation/{mode}"), 413
try:
imported, skipped, missing = save_usa_presentation_images_zip(mode, file)
details = "נשמרו בתיקיית התמונות:\n" + ("\n".join(imported) if imported else "לא נשמרו תמונות.")
if skipped:
details += "\n\nדולגו:\n" + "\n".join(skipped[:120])
if missing:
details += "\n\nלא נמצאו ב־ZIP, ולכן לא הוחלפו:\n" + "\n".join(missing[:120])
return admin_status_page("ZIP תמונות ארה״ב נטען", f"נשמרו {len(imported)} תמונות למצגת {USA_TARGETS[mode]['short']}.", ok=True, details=details, back=f"/admin/usa/presentation/{mode}")
except zipfile.BadZipFile:
return admin_status_page("ZIP לא תקין", "הקובץ שהועלה אינו ZIP תקין או שלא ניתן לפתוח אותו.", ok=False, back=f"/admin/usa/presentation/{mode}"), 400
except ValueError as exc:
return admin_status_page("ZIP לא תקין", "הקובץ לא עומד במגבלות העלאה של הפאנל.", ok=False, details=str(exc), back=f"/admin/usa/presentation/{mode}"), 400
except Exception as exc:
return admin_status_page("שגיאה בהעלאת ZIP", "לא ניתן היה לעבד את קובץ התמונות.", ok=False, details=repr(exc), back=f"/admin/usa/presentation/{mode}"), 500
@app.route("/upload_usa_images_zip", methods=["POST"])
def upload_usa_images_zip_legacy():
return upload_usa_images_zip_mode("phone")
@app.route("/upload_usa_music/<mode>/<music_key>", methods=["POST"])
def upload_usa_music_mode(mode, music_key):
mode = normalize_usa_mode(mode)
if not mode:
return "USA music target not found", 404
file = request.files.get("file")
if not file or file.filename == "":
return "No file selected", 400
filename = save_usa_music(mode, music_key, file)
if not filename:
return "USA music type not found", 404
return redirect(request.referrer or f"/admin/usa/presentation/{mode}")
@app.route("/upload_usa_music/<music_key>", methods=["POST"])
def upload_usa_music_legacy(music_key):
return upload_usa_music_mode("phone", music_key)
def safe_extract_zip_to_dir(zip_path, dest_dir, strip_root_folder=None):
os.makedirs(dest_dir, exist_ok=True)
with zipfile.ZipFile(zip_path, "r") as zf:
infos = [info for info in zf.infolist() if not info.is_dir()]
if len(infos) > USA_PACKAGE_ZIP_MAX_FILES:
raise ValueError(f"too_many_files:{len(infos)}")
total = sum(max(0, i.file_size) for i in infos)
if total > USA_PACKAGE_ZIP_MAX_UNCOMPRESSED_BYTES:
raise ValueError(f"zip_uncompressed_size_exceeded:{total}")
for info in infos:
raw = info.filename.replace("\\", "/")
if raw.startswith("__MACOSX/") or "/.." in raw or raw.startswith("../") or raw.startswith("/"):
continue
rel = raw
if strip_root_folder and rel.startswith(strip_root_folder.rstrip("/") + "/"):
rel = rel[len(strip_root_folder.rstrip("/")) + 1:]
if not rel or rel.endswith("/"):
continue
target = os.path.abspath(os.path.join(dest_dir, rel))
dest_abs = os.path.abspath(dest_dir)
if not target.startswith(dest_abs + os.sep) and target != dest_abs:
continue
os.makedirs(os.path.dirname(target), exist_ok=True)
with zf.open(info, "r") as source, open(target, "wb") as dest:
shutil.copyfileobj(source, dest)
def zip_has_file(zip_path, name):
name = name.replace("\\", "/").strip("/")
with zipfile.ZipFile(zip_path, "r") as zf:
names = {n.replace("\\", "/").strip("/") for n in zf.namelist() if not n.endswith("/")}
return name in names
@app.route("/upload_usa_tv_package_zip", methods=["POST"])
def upload_usa_tv_package_zip():
file = request.files.get("file")
if not file or file.filename == "":
return "No file selected", 400
original_name = file.filename or ""
if not original_name.lower().endswith(".zip"):
return admin_status_page("קובץ לא תקין", "ניתן להעלות כאן רק ZIP מלא של USA_TV.", ok=False, back="/admin/usa/presentation/tv"), 400
if request.content_length and request.content_length > USA_PACKAGE_ZIP_MAX_BYTES + 1024 * 1024:
return admin_status_page("קובץ גדול מדי", f"קובץ ה־ZIP גדול מהמגבלה: {USA_PACKAGE_ZIP_MAX_BYTES:,} bytes.", ok=False, back="/admin/usa/presentation/tv"), 413
stamp = time.strftime("%Y%m%d_%H%M%S")
tmp_zip = os.path.join(ADMIN_UPLOAD_TMP_DIR, f"usa_tv_full_package_{stamp}.zip")
tmp_extract = os.path.join(ADMIN_UPLOAD_TMP_DIR, f"usa_tv_extract_{stamp}")
os.makedirs(ADMIN_UPLOAD_TMP_DIR, exist_ok=True)
try:
file.save(tmp_zip)
if os.path.getsize(tmp_zip) > USA_PACKAGE_ZIP_MAX_BYTES:
raise ValueError("zip_size_exceeded")
with zipfile.ZipFile(tmp_zip, "r") as zf:
names = {n.replace("\\", "/").strip("/") for n in zf.namelist() if not n.endswith("/")}
strip = "USA_TV" if "USA_TV/index.html" in names else None
if strip:
required = {"USA_TV/index.html", "USA_TV/usa2027-presentation.html"}
else:
required = {"index.html", "usa2027-presentation.html"}
missing = sorted(required - names)
if missing:
return admin_status_page("ZIP חסר קבצים", "ה־ZIP לא הוחלף כי חסרים קבצי בסיס.", ok=False, details="\n".join(missing), back="/admin/usa/presentation/tv"), 400
safe_extract_zip_to_dir(tmp_zip, tmp_extract, strip_root_folder=strip)
target = FOLDERS["USA_TV"]
backup_dir = os.path.join(BASE_DIR, "backups", "USA_TV", f"USA_TV_backup_{stamp}")
if os.path.exists(target):
os.makedirs(os.path.dirname(backup_dir), exist_ok=True)
if os.path.exists(backup_dir):
shutil.rmtree(backup_dir)
shutil.copytree(target, backup_dir)
shutil.rmtree(target)
shutil.copytree(tmp_extract, target)
return admin_status_page("חבילת USA_TV הועלתה", "תיקיית USA_TV הוחלפה אחרי יצירת גיבוי.", ok=True, details=f"יעד:\n{target}\n\nגיבוי:\n{backup_dir}", back="/admin/usa/presentation/tv")
except zipfile.BadZipFile:
return admin_status_page("ZIP לא תקין", "הקובץ שהועלה אינו ZIP תקין.", ok=False, back="/admin/usa/presentation/tv"), 400
except Exception as exc:
return admin_status_page("שגיאה בהעלאת USA_TV", "תיקיית USA_TV לא הוחלפה.", ok=False, details=repr(exc), back="/admin/usa/presentation/tv"), 500
finally:
for p in (tmp_zip,):
try:
if os.path.exists(p): os.remove(p)
except Exception:
pass
try:
if os.path.exists(tmp_extract): shutil.rmtree(tmp_extract)
except Exception:
pass
@app.route("/JAPAN")
@app.route("/JAPAN/")
def japan():
return serve_app("JAPAN")
@app.route("/USA")
@app.route("/USA/")
def usa():
return serve_app("USA")
@app.route("/USA_TV")
@app.route("/USA_TV/")
def usa_tv():
return serve_app("USA_TV")
@app.route("/Moneyapp")
@app.route("/Moneyapp/")
def moneyapp():
return serve_app("Moneyapp")
@app.route("/MoneyHome")
@app.route("/MoneyHome/")
def moneyhome():
return serve_app("MoneyHome")
@app.route("/JAPAN/<path:path>")
def japan_static(path):
return no_cache_response(make_response(send_from_directory(FOLDERS["JAPAN"], path)))
@app.route("/USA/<path:path>")
def usa_static(path):
return no_cache_response(make_response(send_from_directory(FOLDERS["USA"], path)))
@app.route("/USA_TV/<path:path>")
def usa_tv_static(path):
return no_cache_response(make_response(send_from_directory(FOLDERS["USA_TV"], path)))
@app.route("/Moneyapp/<path:path>")
def money_static(path):
return no_cache_response(make_response(send_from_directory(FOLDERS["Moneyapp"], path)))
@app.route("/MoneyHome/<path:path>")
def moneyhome_static(path):
return no_cache_response(make_response(send_from_directory(FOLDERS["MoneyHome"], path)))
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000, threaded=True, use_reloader=False)