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

📄 admin_server_before_restore_20260701_110328.py

C:\TripServer\backups\admin_server\admin_server_before_restore_20260701_110328.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

app = Flask(__name__)

BASE_DIR = r"C:\TripServer"

FOLDERS = {
    "JAPAN": os.path.join(BASE_DIR, "JAPAN"),
    "USA": os.path.join(BASE_DIR, "USA"),
    "Moneyapp": os.path.join(BASE_DIR, "Moneyapp"),
    "MoneyHome": os.path.join(BASE_DIR, "MoneyHome"),
}

APP_FOLDERS = {"JAPAN", "USA", "Moneyapp", "MoneyHome"}

JAPAN_PRESENTATION_FILES = {
    "flight_intro": "japan-flight-intro.html",
    "japan_experience": "japan-experience.html",
}

USA_PRESENTATION_FILES = {
    "usa_presentation": "usa2027-presentation.html",
}

USA_MUSIC_FILES = {
    "usa_music": os.path.join("media", "usa-presentation", "music", "usa-roadtrip-theme.mp3"),
    "usa_music_part2": os.path.join("media", "usa-presentation", "music", "usa-part2-theme.mp3"),
}

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"}

ADMIN_SERVER_PATH = os.path.abspath(__file__)
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_MAX_UPLOAD_BYTES = 2 * 1024 * 1024
USA_IMAGES_ZIP_MAX_BYTES = 80 * 1024 * 1024
USA_IMAGES_ZIP_MAX_FILES = 160
USA_IMAGES_ZIP_MAX_UNCOMPRESSED_BYTES = 140 * 1024 * 1024
USA_IMAGES_ZIP_MAX_SINGLE_FILE_BYTES = 12 * 1024 * 1024
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")


def save_usa_presentation_image(image_key, uploaded_file):
    if image_key not in USA_IMAGE_KEYS:
        return None

    image_dir = os.path.join(FOLDERS["USA"], "media", "usa-presentation", "images")
    os.makedirs(image_dir, exist_ok=True)

    # Keep the presentation predictable: every single-card upload is normalized to .jpg.
    # This is especially important for roadtrip-map, because the presentation expects:
    # media/usa-presentation/images/roadtrip-map.jpg
    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

    filename = image_key + target_ext
    target_path = os.path.join(image_dir, filename)

    # Prefer real JPEG conversion when Pillow exists; if not installed, save raw bytes with .jpg.
    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("media", "usa-presentation", "images", filename)


def save_usa_presentation_images_zip(uploaded_file):
    image_dir = os.path.join(FOLDERS["USA"], "media", "usa-presentation", "images")
    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_{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

for folder_path in FOLDERS.values():
    os.makedirs(folder_path, exist_ok=True)

os.makedirs(os.path.join(FOLDERS["USA"], "media", "usa-presentation", "music"), exist_ok=True)
os.makedirs(os.path.join(FOLDERS["USA"], "media", "usa-presentation", "images"), 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"

    html_files = [f for f in os.listdir(folder_path) if f.lower().endswith((".html", ".htm"))]
    if html_files:
        return html_files[0]
    return None


def clean_wrong_main_files(folder_path):
    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 not os.path.isfile(full_path):
            continue
        if name.lower().strip() in wrong_names:
            try:
                os.remove(full_path)
            except Exception:
                pass


def save_uploaded_file(folder, uploaded_file):
    folder_path = FOLDERS[folder]
    os.makedirs(folder_path, exist_ok=True)

    original_filename = uploaded_file.filename or ""
    safe_name = secure_filename(original_filename)

    if folder in APP_FOLDERS:
        clean_wrong_main_files(folder_path)
        filename = "index.html"
    else:
        filename = safe_name or f"upload_{int(time.time())}"

    uploaded_file.save(os.path.join(folder_path, filename))
    return filename


def save_japan_presentation(presentation_key, uploaded_file):
    if presentation_key not in JAPAN_PRESENTATION_FILES:
        return None
    japan_folder = FOLDERS["JAPAN"]
    os.makedirs(japan_folder, exist_ok=True)
    filename = JAPAN_PRESENTATION_FILES[presentation_key]
    uploaded_file.save(os.path.join(japan_folder, filename))
    return filename


def save_usa_presentation(presentation_key, uploaded_file):
    if presentation_key not in USA_PRESENTATION_FILES:
        return None
    usa_folder = FOLDERS["USA"]
    os.makedirs(usa_folder, exist_ok=True)
    filename = USA_PRESENTATION_FILES[presentation_key]
    uploaded_file.save(os.path.join(usa_folder, filename))
    return filename


def save_usa_music(music_key, uploaded_file):
    if music_key not in USA_MUSIC_FILES:
        return None
    usa_folder = FOLDERS["USA"]
    rel_path = USA_MUSIC_FILES[music_key]
    full_path = os.path.join(usa_folder, rel_path)
    os.makedirs(os.path.dirname(full_path), exist_ok=True)
    uploaded_file.save(full_path)
    return rel_path


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(400000)
    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=""):
    color = "#16a34a" if ok else "#dc2626"
    bg = "#052e1b" if ok else "#3f1d1d"
    safe_title = html_lib.escape(title)
    safe_message = html_lib.escape(message).replace("\n", "<br>")
    safe_details = html_lib.escape(details).replace("\n", "<br>")
    details_html = f"<pre>{safe_details}</pre>" if 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(780px,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="/admin">חזרה לפאנל</a>
<form action="/restart_admin_server" method="post" onsubmit="return confirm('לאתחל את הפאנל עכשיו?')"><button class="restart">אתחל פאנל עכשיו</button></form>
</div>
</body>
</html>
"""


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")
    backup_path = os.path.join(ADMIN_BACKUP_DIR, f"{prefix}_{stamp}.py")
    shutil.copy2(ADMIN_SERVER_PATH, backup_path)
    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"))
    if not backups:
        return None
    return max(backups, key=os.path.getmtime)


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():
    """Reliable self-restart for Windows/iPhone admin workflow."""
    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":
            # Use a detached helper so the server can exit first and free port 8000.
            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:
        # Fallback: do not silently die before recording what happened.
        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)


@app.route("/")
def home():
    return """
<!DOCTYPE html>
<html lang="he" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>מרכז ניהול</title>
<style>
body{margin:0;font-family:Arial;background:linear-gradient(135deg,#081229,#102447);color:white;min-height:100vh}
.hero{text-align:center;padding:50px 20px 20px}
.hero h1{font-size:42px;margin:0}
.hero p{color:#d8e6ff;font-size:18px}
.grid{width:min(1100px,92%);margin:40px auto;display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:22px}
.card{text-decoration:none;color:white;border-radius:28px;padding:28px;min-height:180px;box-shadow:0 10px 30px rgba(0,0,0,.25);transition:.2s transform,.2s box-shadow}
.card:hover{transform:translateY(-4px);box-shadow:0 16px 40px rgba(0,0,0,.35)}
.card h2{font-size:30px;margin:0 0 12px}
.card p{line-height:1.6;margin:0}
.icon{font-size:52px;margin-bottom:15px}
.admin{background:linear-gradient(135deg,#0f9b0f,#38ef7d)}
.money{background:linear-gradient(135deg,#5f2c82,#49a09d)}
.homebudget{background:linear-gradient(135deg,#11998e,#38ef7d)}
.japan{background:linear-gradient(135deg,#ff416c,#ff4b2b)}
.usa{background:linear-gradient(135deg,#0052d4,#4364f7,#6fb1fc)}
.footer{text-align:center;color:#b8c8e6;padding:25px}

.image-guide{margin:12px 0 16px;background:#0f172a;border:1px solid #334155;border-radius:14px;padding:12px;color:#e2e8f0}
.image-guide summary{cursor:pointer;font-weight:bold;color:#bfdbfe;margin-bottom:10px}
.image-guide-table{width:100%;border-collapse:collapse;margin-top:10px;font-size:14px}
.image-guide-table th,.image-guide-table td{border-bottom:1px solid #334155;padding:9px;text-align:right;vertical-align:top}
.image-guide-table th{color:#93c5fd;background:#111827}
.image-guide-table td:first-child{white-space:nowrap;color:#fde68a}


.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}
@media(max-width:640px){.image-upload-grid{grid-template-columns:1fr}.image-title{font-size:16px}.image-desc{min-height:auto}}

</style>
</head>
<body>
<div class="hero">
<h1>מרכז ניהול</h1>
<p>מערכת מרכזית לניהול קבצים, תקציבים ולוחות זמנים</p>
</div>
<div class="grid">
<a class="card admin" href="/admin"><div class="icon">📁</div><h2>ניהול קבצים</h2><p>העלאת קבצים ישירות למחשב.</p></a>
<a class="card money" href="/Moneyapp/"><div class="icon">💰</div><h2>תקציב טיולים</h2><p>ניהול תקציב חופשות וסנכרון נתונים.</p></a>
<a class="card homebudget" href="/MoneyHome/"><div class="icon">🏠</div><h2>תקציב משפחתי</h2><p>ניהול תקציב חודשי, Limits, הוצאות בפועל וארכיון חודשים.</p></a>
<a class="card japan" href="/JAPAN/"><div class="icon">🇯🇵</div><h2>טיול יפן</h2><p>לוז, ניווטים ומסמכי הטיול.</p></a>
<a class="card usa" href="/USA/"><div class="icon">🇺🇸</div><h2>טיול ארה״ב</h2><p>טיול בר מצווה 2027.</p></a>
</div>
<div class="footer">TripServer</div>
</body>
</html>
"""


@app.route("/admin")
@app.route("/admin/")
def admin():
    return """
<!DOCTYPE html>
<html lang="he" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ניהול קבצים</title>
<style>
body{margin:0;font-family:Arial;background:#0f172a;color:white}
.wrap{width:min(950px,92%);margin:auto;padding:40px 0}
h1{text-align:center}
.box{background:#1e293b;border-radius:22px;padding:24px;margin-bottom:24px}
.box h2{margin-top:0}
input[type=file]{width:100%;padding:14px;background:#0f172a;color:white;border-radius:12px;border:1px solid #334155}
button{margin-top:16px;border:0;background:#0b84f3;color:white;padding:14px 22px;border-radius:12px;font-size:16px;font-weight:bold;cursor:pointer}
.back{display:inline-block;margin-bottom:24px;color:#93c5fd;text-decoration:none}
.note{color:#cbd5e1;font-size:14px;line-height:1.6}
.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}
.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-top:14px}
.danger{background:#dc2626!important}
.orange{background:#ea580c!important}
.admin-update-actions{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-top:12px}
.admin-update-actions form{margin:0}
.admin-mini{font-size:13px;color:#bfdbfe;line-height:1.6;background:#0f172a;border:1px solid #334155;border-radius:12px;padding:10px;margin-top:12px}

.image-guide{margin:12px 0 16px;background:#0f172a;border:1px solid #334155;border-radius:14px;padding:12px;color:#e2e8f0}
.image-guide summary{cursor:pointer;font-weight:bold;color:#bfdbfe;margin-bottom:10px}
.image-guide-table{width:100%;border-collapse:collapse;margin-top:10px;font-size:14px}
.image-guide-table th,.image-guide-table td{border-bottom:1px solid #334155;padding:9px;text-align:right;vertical-align:top}
.image-guide-table th{color:#93c5fd;background:#111827}
.image-guide-table td:first-child{white-space:nowrap;color:#fde68a}


.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}
@media(max-width:640px){.image-upload-grid{grid-template-columns:1fr}.image-title{font-size:16px}.image-desc{min-height:auto}}

</style>
</head>
<body>
<div class="wrap">
<a class="back" href="/">⬅ חזרה למסך הראשי</a>
<h1>ניהול והעלאת קבצים</h1>

<div class="box">
<h2>🇯🇵 טיול יפן</h2>
<form action="/upload/JAPAN" method="post" enctype="multipart/form-data">
<input type="file" name="file" accept=".html,.htm">
<button>העלה קובץ טיול יפן ראשי</button>
</form>
<p class="note">כל קובץ שתעלה כאן יישמר כ־index.html.</p>
</div>

<div class="box">
<h2>🎬 מצגות יפן</h2>
<div class="warning">כאן מעלים רק את קבצי המצגות, לא את קובץ הטיול הראשי. הקבצים יישמרו אוטומטית בשמות הנכונים.</div>
<h3>✈️ פתיח טיסה</h3>
<form action="/upload_presentation/flight_intro" method="post" enctype="multipart/form-data">
<input type="file" name="file" accept=".html,.htm">
<button>העלה פתיח טיסה</button>
</form>
<p class="note">יישמר אוטומטית בשם:<br><strong>japan-flight-intro.html</strong></p>
<h3>🎌 מצגת יפן</h3>
<form action="/upload_presentation/japan_experience" method="post" enctype="multipart/form-data">
<input type="file" name="file" accept=".html,.htm">
<button>העלה מצגת יפן</button>
</form>
<p class="note">יישמר אוטומטית בשם:<br><strong>japan-experience.html</strong></p>
<div class="preview-links">
<a href="/JAPAN/japan-flight-intro.html" target="_blank">בדיקת פתיח טיסה</a>
<a href="/JAPAN/japan-experience.html" target="_blank">בדיקת מצגת יפן</a>
<a href="/JAPAN/" target="_blank">פתיחת קובץ יפן</a>
</div>
</div>

<div class="box">
<h2>🇺🇸 טיול ארה״ב</h2>
<form action="/upload/USA" method="post" enctype="multipart/form-data">
<input type="file" name="file" accept=".html,.htm">
<button>העלה קובץ טיול ארה״ב</button>
</form>
<p class="note">כל קובץ שתעלה כאן יישמר כ־index.html.</p>
<div class="preview-links">
<a href="/USA/" target="_blank">פתיחת קובץ ארה״ב</a>
<a href="/USA/usa2027-presentation.html?from=admin" target="_blank">בדיקת מצגת ארה״ב</a>
</div>
</div>

<div class="box">
<h2>🎬 מצגת ארה״ב — אייפון / טלוויזיה</h2>
<div class="warning">כאן מעלים את קובץ מצגת ארה״ב. אותו קובץ משמש גם לאייפון וגם למצב טלוויזיה דרך פרמטר תצוגה. גם אם שם הקובץ אצלך שגוי – הוא יישמר אוטומטית בשם התקין, כדי שכפתור המצגת בקובץ הטיול יעבוד מיד.</div>
<form action="/upload_usa_presentation/usa_presentation" method="post" enctype="multipart/form-data">
<input type="file" name="file" accept=".html,.htm">
<button>העלה מצגת ארה״ב</button>
</form>
<p class="note">יישמר אוטומטית בשם:<br><strong>usa2027-presentation.html</strong></p>
<div class="preview-links">
<a href="/USA/usa2027-presentation.html?from=admin&load=cellular" target="_blank">בדיקת מצגת ארה״ב — אייפון</a>
<a href="/USA/usa2027-presentation.html?from=admin&load=wifi&tv=1" target="_blank">בדיקת מצגת ארה״ב — טלוויזיה</a>
<a href="/USA/" target="_blank">פתיחת קובץ ארה״ב</a>
</div>
</div>



<div class="box">
<h2>🖼️ תמונות מצגת ארה״ב — אייפון / טלוויזיה</h2>
<div class="warning">השורה הראשונה מיועדת להעלאת ZIP מלא של תמונות המצגת. אותן תמונות משמשות גם את גרסת האייפון וגם את מצב הטלוויזיה, לפי שמות הקבצים הקבועים. ניתן להעלות ZIP עם התמונות ישירות בשורש הקובץ, או ZIP עם המבנה <strong>USA/media/usa-presentation/images</strong>. הקבצים יישמרו במקום הנכון ויחליפו קבצים קיימים באותו שם.</div>
<form action="/upload_usa_images_zip" method="post" enctype="multipart/form-data">
<input type="file" name="file" accept=".zip,application/zip,application/x-zip-compressed">
<button>העלה ZIP מלא של תמונות מצגת ארה״ב</button>
</form>
<p class="note">שמות נתמכים: <strong>intro.jpg</strong>, <strong>budget.jpg</strong>, <strong>roadtrip-map.jpg</strong>, <strong>extra-big-sur.jpg</strong>, <strong>extra-diner.jpg</strong>, <strong>extra-death-valley.jpg</strong>, <strong>extra-mono-lake.jpg</strong>, <strong>day-01.jpg</strong> עד <strong>day-36.jpg</strong>, ו־<strong>summary.jpg</strong>.</p>
<div class="image-upload-grid">
<form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="intro">
<input class="image-file-input" id="image-file-intro" 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="image-file-intro" class="image-upload-label">
<span class="image-key">intro</span>
<span class="image-title">פתיחה</span>
<span class="image-desc">פתיחה — ארה״ב 2027 / Road Trip, Orlando, Miami, Cruise</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="budget">
<input class="image-file-input" id="image-file-budget" 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="image-file-budget" class="image-upload-label">
<span class="image-key">budget</span>
<span class="image-title">תקציב</span>
<span class="image-desc">תקציב — תמונת מצב פיננסית / עלות כוללת ובקרת פרויקט</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="roadtrip-map">
<input class="image-file-input" id="image-file-roadtrip-map" 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="image-file-roadtrip-map" class="image-upload-label">
<span class="image-key">roadtrip-map</span>
<span class="image-title">🗺️ מפת רוד־טריפ</span>
<span class="image-desc">מפת הרוד־טריפ — מערב ארה״ב / מסלול כללי</span>
<span class="upload-status">לחץ כאן ובחר את המפה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-01">
<input class="image-file-input" id="image-file-day-01" 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="image-file-day-01" class="image-upload-label">
<span class="image-key">day-01</span>
<span class="image-title">יום 1</span>
<span class="image-desc">נחיתה בסן פרנסיסקו / פתיחת הטיול</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-02">
<input class="image-file-input" id="image-file-day-02" 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="image-file-day-02" class="image-upload-label">
<span class="image-key">day-02</span>
<span class="image-title">יום 2</span>
<span class="image-desc">סן פרנסיסקו + Alcatraz / Golden Gate, מפרץ, אלקטרז</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-03">
<input class="image-file-input" id="image-file-day-03" 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="image-file-day-03" class="image-upload-label">
<span class="image-key">day-03</span>
<span class="image-title">יום 3</span>
<span class="image-desc">סן פרנסיסקו → מונטריי / 17‑Mile Drive, Cannery Row, חוף קליפורניה</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-04">
<input class="image-file-input" id="image-file-day-04" 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="image-file-day-04" class="image-upload-label">
<span class="image-key">day-04</span>
<span class="image-title">יום 4</span>
<span class="image-desc">Monterey Bay Whale Watch + Big Sur / לווייתנים, כביש 1, נופי חוף</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="extra-big-sur">
<input class="image-file-input" id="image-file-extra-big-sur" 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="image-file-extra-big-sur" class="image-upload-label">
<span class="image-key">extra-big-sur</span>
<span class="image-title">שקף חדש: Big Sur / Bixby Bridge</span>
<span class="image-desc">שקף ביניים — Big Sur / Bixby Bridge, McWay Falls</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-05">
<input class="image-file-input" id="image-file-day-05" 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="image-file-day-05" class="image-upload-label">
<span class="image-key">day-05</span>
<span class="image-title">יום 5</span>
<span class="image-desc">סנטה מוניקה → לוס אנג׳לס / Pier, חוף, גלגל ענק, LA</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-06">
<input class="image-file-input" id="image-file-day-06" 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="image-file-day-06" class="image-upload-label">
<span class="image-key">day-06</span>
<span class="image-title">יום 6</span>
<span class="image-desc">Warner Bros + Beverly Hills + Griffith / אולפנים, בוורלי הילס, תצפית</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-07">
<input class="image-file-input" id="image-file-day-07" 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="image-file-day-07" class="image-upload-label">
<span class="image-key">day-07</span>
<span class="image-title">יום 7</span>
<span class="image-desc">לוס אנג׳לס → Zion / יום מעבר למדבר וליוטה</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="extra-diner">
<input class="image-file-input" id="image-file-extra-diner" 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="image-file-extra-diner" class="image-upload-label">
<span class="image-key">extra-diner</span>
<span class="image-title">שקף חדש: Peggy Sue’s Diner</span>
<span class="image-desc">שקף ביניים — Peggy Sue’s 50’s Diner / דיינר רוד־טריפ אמריקאי</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-08">
<input class="image-file-input" id="image-file-day-08" 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="image-file-day-08" class="image-upload-label">
<span class="image-key">day-08</span>
<span class="image-title">יום 8</span>
<span class="image-desc">Zion → Bryce → Page / צוקים, קניונים, צבעי מדבר</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-09">
<input class="image-file-input" id="image-file-day-09" 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="image-file-day-09" class="image-upload-label">
<span class="image-key">day-09</span>
<span class="image-title">יום 9</span>
<span class="image-desc">Antelope Canyon + Horseshoe Bend / Page Arizona, קניון חריץ, תצפית</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-10">
<input class="image-file-input" id="image-file-day-10" 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="image-file-day-10" class="image-upload-label">
<span class="image-key">day-10</span>
<span class="image-title">יום 10</span>
<span class="image-desc">Page → Grand Canyon South Rim / גרנד קניון ותצפיות דרמטיות</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-11">
<input class="image-file-input" id="image-file-day-11" 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="image-file-day-11" class="image-upload-label">
<span class="image-key">day-11</span>
<span class="image-title">יום 11</span>
<span class="image-desc">Grand Canyon → Las Vegas / מעבר מהטבע לווגאס</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-12">
<input class="image-file-input" id="image-file-day-12" 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="image-file-day-12" class="image-upload-label">
<span class="image-key">day-12</span>
<span class="image-title">יום 12</span>
<span class="image-desc">Vegas Outlets + Sphere / שופינג, Sphere, אורות וגאס</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-13">
<input class="image-file-input" id="image-file-day-13" 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="image-file-day-13" class="image-upload-label">
<span class="image-key">day-13</span>
<span class="image-title">יום 13</span>
<span class="image-desc">Vegas Extra Day + Strip / Bellagio, Strip, מלונות, קניות</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-14">
<input class="image-file-input" id="image-file-day-14" 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="image-file-day-14" class="image-upload-label">
<span class="image-key">day-14</span>
<span class="image-title">יום 14</span>
<span class="image-desc">Las Vegas → Mammoth Lakes / מעבר הרים, אגמים, Mammoth</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="extra-death-valley">
<input class="image-file-input" id="image-file-extra-death-valley" 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="image-file-extra-death-valley" class="image-upload-label">
<span class="image-key">extra-death-valley</span>
<span class="image-title">שקף חדש: Death Valley</span>
<span class="image-desc">שקף ביניים — Death Valley / מדבר, כביש פתוח, נוף דרמטי</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="extra-mono-lake">
<input class="image-file-input" id="image-file-extra-mono-lake" 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="image-file-extra-mono-lake" class="image-upload-label">
<span class="image-key">extra-mono-lake</span>
<span class="image-title">שקף חדש: Mono Lake / Eastern Sierra</span>
<span class="image-desc">שקף ביניים — Mono Lake / Eastern Sierra, אגמים והרים</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-15">
<input class="image-file-input" id="image-file-day-15" 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="image-file-day-15" class="image-upload-label">
<span class="image-key">day-15</span>
<span class="image-title">יום 15</span>
<span class="image-desc">Mammoth → Yosemite Valley / Tunnel View, El Capitan, Half Dome</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-16">
<input class="image-file-input" id="image-file-day-16" 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="image-file-day-16" class="image-upload-label">
<span class="image-key">day-16</span>
<span class="image-title">יום 16</span>
<span class="image-desc">Yosemite Highlights + Mariposa Grove / מפלים, סקויה, יוסמיטי</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-17">
<input class="image-file-input" id="image-file-day-17" 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="image-file-day-17" class="image-upload-label">
<span class="image-key">day-17</span>
<span class="image-title">יום 17</span>
<span class="image-desc">Yosemite → SFO → Orlando / יום מעבר וטיסה לאורלנדו</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-18">
<input class="image-file-input" id="image-file-day-18" 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="image-file-day-18" class="image-upload-label">
<span class="image-key">day-18</span>
<span class="image-title">יום 18</span>
<span class="image-desc">התאוששות + Disney Springs בלבד / קניות, אוכל ואווירה קלילה באורלנדו</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-19">
<input class="image-file-input" id="image-file-day-19" 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="image-file-day-19" class="image-upload-label">
<span class="image-key">day-19</span>
<span class="image-title">יום 19</span>
<span class="image-desc">Islands of Adventure / Universal Orlando, Harry Potter, Hulk</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-20">
<input class="image-file-input" id="image-file-day-20" 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="image-file-day-20" class="image-upload-label">
<span class="image-key">day-20</span>
<span class="image-title">יום 20</span>
<span class="image-desc">Universal Studios Florida + CityWalk / מתקנים, Harry Potter, CityWalk</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-21">
<input class="image-file-input" id="image-file-day-21" 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="image-file-day-21" class="image-upload-label">
<span class="image-key">day-21</span>
<span class="image-title">יום 21</span>
<span class="image-desc">מנוחה + Hollywood Drive‑In Mini Golf / אורלנדו רגוע ומיני גולף</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-22">
<input class="image-file-input" id="image-file-day-22" 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="image-file-day-22" class="image-upload-label">
<span class="image-key">day-22</span>
<span class="image-title">יום 22</span>
<span class="image-desc">Epic Universe + Express / פארק Epic החדש, פורטלים, Universal</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-23">
<input class="image-file-input" id="image-file-day-23" 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="image-file-day-23" class="image-upload-label">
<span class="image-key">day-23</span>
<span class="image-title">יום 23</span>
<span class="image-desc">פארק חבלים + בריכה / International Drive, אקשן קליל ומנוחה</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-24">
<input class="image-file-input" id="image-file-day-24" 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="image-file-day-24" class="image-upload-label">
<span class="image-key">day-24</span>
<span class="image-title">יום 24</span>
<span class="image-desc">Volcano Bay + Express / פארק מים, מגלשות, בריכות</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-25">
<input class="image-file-input" id="image-file-day-25" 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="image-file-day-25" class="image-upload-label">
<span class="image-key">day-25</span>
<span class="image-title">יום 25</span>
<span class="image-desc">יום רגוע + Halloween Horror Nights / HHN, אימה, Universal בלילה</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-26">
<input class="image-file-input" id="image-file-day-26" 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="image-file-day-26" class="image-upload-label">
<span class="image-key">day-26</span>
<span class="image-title">יום 26</span>
<span class="image-desc">Orlando → Sawgrass Mills → Miami / שופינג, מעבר למיאמי</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-27">
<input class="image-file-input" id="image-file-day-27" 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="image-file-day-27" class="image-upload-label">
<span class="image-key">day-27</span>
<span class="image-title">יום 27</span>
<span class="image-desc">Miami Chill Before Cruise / Miami Beach, Ocean Drive, דקלים</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-28">
<input class="image-file-input" id="image-file-day-28" 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="image-file-day-28" class="image-upload-label">
<span class="image-key">day-28</span>
<span class="image-title">יום 28</span>
<span class="image-desc">החזרת רכב ועלייה לקרוז / PortMiami, Hero of the Seas</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-29">
<input class="image-file-input" id="image-file-day-29" 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="image-file-day-29" class="image-upload-label">
<span class="image-key">day-29</span>
<span class="image-title">יום 29</span>
<span class="image-desc">קרוז — יום ים 1 / Hero of the Seas, סיפון, בריכות, ים פתוח</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-30">
<input class="image-file-input" id="image-file-day-30" 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="image-file-day-30" class="image-upload-label">
<span class="image-key">day-30</span>
<span class="image-title">יום 30</span>
<span class="image-desc">קרוז — יום ים 2 / Hero of the Seas, מגלשות, בריכות ואונייה</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-31">
<input class="image-file-input" id="image-file-day-31" 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="image-file-day-31" class="image-upload-label">
<span class="image-key">day-31</span>
<span class="image-title">יום 31</span>
<span class="image-desc">קרוז — St. Maarten / Philipsburg, Maho Beach, חוף מטוסים</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-32">
<input class="image-file-input" id="image-file-day-32" 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="image-file-day-32" class="image-upload-label">
<span class="image-key">day-32</span>
<span class="image-title">יום 32</span>
<span class="image-desc">קרוז — St. Thomas + Dolphin Splash And Swim / דולפינים, Charlotte Amalie, חוף קריבי</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-33">
<input class="image-file-input" id="image-file-day-33" 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="image-file-day-33" class="image-upload-label">
<span class="image-key">day-33</span>
<span class="image-title">יום 33</span>
<span class="image-desc">קרוז — יום ים 3 / Hero of the Seas, סיפון, שקיעה, ים פתוח</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-34">
<input class="image-file-input" id="image-file-day-34" 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="image-file-day-34" class="image-upload-label">
<span class="image-key">day-34</span>
<span class="image-title">יום 34</span>
<span class="image-desc">קרוז — Perfect Day at CocoCay / פארק מים, חוף טורקיז, Oasis Lagoon</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-35">
<input class="image-file-input" id="image-file-day-35" 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="image-file-day-35" class="image-upload-label">
<span class="image-key">day-35</span>
<span class="image-title">יום 35</span>
<span class="image-desc">ירידה מהקרוז במיאמי + לילה ליד השדה / PortMiami, מלון שדה</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="day-36">
<input class="image-file-input" id="image-file-day-36" 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="image-file-day-36" class="image-upload-label">
<span class="image-key">day-36</span>
<span class="image-title">יום 36</span>
<span class="image-desc">טיסה חזרה לישראל / אל על, מטוס מעל עננים, סיום הטיול</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form><form class="image-upload-card" action="/upload_usa_image" method="post" enctype="multipart/form-data">
<input type="hidden" name="image_key" value="summary">
<input class="image-file-input" id="image-file-summary" 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="image-file-summary" class="image-upload-label">
<span class="image-key">summary</span>
<span class="image-title">סיכום</span>
<span class="image-desc">סיכום — טיול חלום משפחתי / Road Trip, פארקים, קרוז, רגעי שיא</span>
<span class="upload-status">לחץ כאן ובחר תמונה מהטלפון</span>
</label>
</form>
</div>
<p class="note">התמונות נשמרות אוטומטית בתיקייה:<br><strong>C:\\TripServer\\USA\\media\\usa-presentation\\images</strong><br>ובשם קבוע לפי השקף, למשל <strong>day-04.jpg</strong>, <strong>day-12.jpg</strong>, <strong>day-34.jpg</strong>, <strong>extra-big-sur.jpg</strong>, <strong>extra-diner.jpg</strong>, <strong>extra-death-valley.jpg</strong>, <strong>extra-mono-lake.jpg</strong>.</p>
<div class="preview-links">
<a href="/USA/usa2027-presentation.html?from=admin&load=cellular" target="_blank">בדיקת מצגת אייפון אחרי העלאת תמונה</a>
<a href="/USA/usa2027-presentation.html?from=admin&load=wifi&tv=1" target="_blank">בדיקת מצגת טלוויזיה אחרי העלאת תמונה</a>
</div>
</div>

<div class="box">
<h2>🎵 מוזיקת מצגת ארה״ב</h2>
<div class="good">עודכן למצגת שני השירים: אפשר להעלות מהפאנל את שיר חלק 1 ואת שיר חלק 2. לא משנה מה שם הקובץ בטלפון — הפאנל שומר בשם הקבוע שהמצגת מחפשת.</div>
<h3>🎸 שיר חלק 1 — Road Trip</h3>
<form action="/upload_usa_music/usa_music" method="post" enctype="multipart/form-data">
<input type="file" name="file" accept=".mp3,.mpeg,.wav,.m4a,.aac,.ogg">
<button>העלה שיר חלק 1</button>
</form>
<p class="note">יישמר כ־<strong>C:\\TripServer\\USA\\media\\usa-presentation\\music\\usa-roadtrip-theme.mp3</strong></p>
<h3>🎧 שיר חלק 2 — Orlando / Miami / Cruise</h3>
<form action="/upload_usa_music/usa_music_part2" method="post" enctype="multipart/form-data">
<input type="file" name="file" accept=".mp3,.mpeg,.wav,.m4a,.aac,.ogg">
<button>העלה שיר חלק 2</button>
</form>
<p class="note">יישמר כ־<strong>C:\\TripServer\\USA\\media\\usa-presentation\\music\\usa-part2-theme.mp3</strong></p>
<div class="preview-links">
<a href="/USA/media/usa-presentation/music/usa-roadtrip-theme.mp3" target="_blank">בדיקת שיר חלק 1</a>
<a href="/USA/media/usa-presentation/music/usa-part2-theme.mp3" target="_blank">בדיקת שיר חלק 2</a>
<a href="/USA/usa2027-presentation.html?from=admin" target="_blank">בדיקת המצגת עם שני השירים</a>
</div>
</div>

<div class="box">
<h2>⚙️ עדכון קובץ הפאנל / admin_server.py</h2>
<div class="warning"><strong>זהירות:</strong> זה הקובץ שמפעיל את הפאנל עצמו. ההעלאה תתבצע רק אחרי בדיקת קומפילציה, גיבוי אוטומטי, ורק אז החלפה.</div>
<div class="good">הזרימה: העלאת admin_server.py חדש → בדיקת Python → גיבוי לקובץ הקיים → החלפה → אתחול פאנל.</div>
<form action="/upload_admin_server" method="post" enctype="multipart/form-data" onsubmit="return confirm('להעלות admin_server.py חדש ולהחליף את קובץ הפאנל הפעיל אחרי בדיקה וגיבוי?')">
<input type="file" name="file" accept=".py,text/x-python,text/plain">
<button class="orange">העלה admin_server.py חדש</button>
</form>
<div class="admin-update-actions">
<form action="/restart_admin_server" method="post" onsubmit="return confirm('לאתחל את הפאנל עכשיו?')">
<button class="orange">אתחל פאנל</button>
</form>
<form action="/restore_admin_server_backup" method="post" onsubmit="return confirm('לשחזר את הגיבוי האחרון של admin_server.py?')">
<button class="danger">שחזר גיבוי אחרון</button>
</form>
</div>
<p class="note">הקובץ הפעיל: <strong>C:\\TripServer\\admin_server.py</strong><br>גיבויים נשמרים אוטומטית בתיקייה: <strong>C:\\TripServer\\backups\\admin_server</strong></p>
<div class="admin-mini">הערה: אחרי החלפת הקובץ, חובה ללחוץ “אתחל פאנל” כדי שהשרת יעבור לגרסה החדשה. עד האתחול, הפאנל שרץ בזיכרון ממשיך לעבוד על הגרסה הישנה.</div>
</div>

<div class="box">
<h2>💰 תקציב טיולים</h2>
<form action="/upload/Moneyapp" method="post" enctype="multipart/form-data">
<input type="file" name="file" accept=".html,.htm">
<button>העלה קובץ</button>
</form>
<p class="note">כל קובץ שתעלה כאן יישמר כ־index.html.</p>
</div>

<div class="box">
<h2>🏠 תקציב משפחתי</h2>
<form action="/upload/MoneyHome" method="post" enctype="multipart/form-data">
<input type="file" name="file" accept=".html,.htm">
<button>העלה קובץ</button>
</form>
<p class="note">כל קובץ שתעלה כאן יישמר כ־index.html.</p>
</div>
</div>
</body>
</html>
"""



@app.route("/health")
def health():
    return {"ok": True, "service": "TripServer Admin", "version": "USA2027 panel v3 reliable restart", "slides": 43}

@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), 400

    original_name = file.filename or ""
    if not original_name.lower().endswith(".py"):
        return admin_status_page("קובץ לא תקין", "ניתן להעלות כאן רק קובץ Python עם סיומת .py", ok=False), 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), 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), 400

        backup_path = backup_current_admin_server("admin_server_backup")
        os.replace(tmp_path, ADMIN_SERVER_PATH)
        message = (
            "הקובץ החדש עבר בדיקת קומפילציה והחליף את admin_server.py הפעיל.\n"
            "נוצר גיבוי לקובץ הקודם.\n\n"
            "כדי שהשינוי ייכנס בפועל, לחץ עכשיו על אתחל פאנל."
        )
        details = f"קובץ פעיל:\n{ADMIN_SERVER_PATH}\n\nגיבוי:\n{backup_path}"
        return admin_status_page("admin_server.py עודכן בהצלחה", message, ok=True, details=details)
    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)), 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(
        "הפאנל מופעל מחדש",
        "האיפוס התחיל דרך מנגנון Restart חיצוני. המתן 5–10 שניות ואז רענן את הפאנל. אם לא עולה, בדוק את C:\\TripServer\\admin_restart.log.",
        ok=True,
    )


@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), 404

    try:
        valid, details = validate_admin_server_upload(backup_path)
        if not valid:
            return admin_status_page("הגיבוי האחרון לא תקין", "הגיבוי לא שוחזר כי הוא לא עבר בדיקת תקינות.", ok=False, details=details), 400
        safety_backup = backup_current_admin_server("admin_server_before_restore")
        shutil.copy2(backup_path, ADMIN_SERVER_PATH)
        msg = "הגיבוי האחרון שוחזר אל admin_server.py. לחץ אתחל פאנל כדי להפעיל אותו."
        det = f"שוחזר מתוך:\n{backup_path}\n\nגיבוי בטיחות לגרסה שהייתה פעילה לפני השחזור:\n{safety_backup}"
        return admin_status_page("שחזור גיבוי הסתיים", msg, ok=True, details=det)
    except Exception as exc:
        return admin_status_page("שגיאה בשחזור גיבוי", "השחזור לא הושלם.", ok=False, details=repr(exc)), 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("/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("/admin")


@app.route("/upload_usa_presentation/<presentation_key>", methods=["POST"])
def upload_usa_presentation(presentation_key):
    file = request.files.get("file")
    if not file or file.filename == "":
        return "No file selected", 400
    filename = save_usa_presentation(presentation_key, file)
    if not filename:
        return "USA presentation type not found", 404
    return redirect("/admin")



@app.route("/upload_usa_image", methods=["POST"])
def upload_usa_image():
    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(image_key, file)
    if not filename:
        return "USA image key not found", 404
    return redirect("/admin")


@app.route("/upload_usa_images_zip", methods=["POST"])
def upload_usa_images_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 של תמונות מצגת.", ok=False), 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), 413
    try:
        imported, skipped, missing = save_usa_presentation_images_zip(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)} תמונות למצגת. קבצים קיימים באותו שם הוחלפו.",
            ok=True,
            details=details,
        )
    except zipfile.BadZipFile:
        return admin_status_page("ZIP לא תקין", "הקובץ שהועלה אינו ZIP תקין או שלא ניתן לפתוח אותו.", ok=False), 400
    except ValueError as exc:
        return admin_status_page("ZIP לא תקין", "הקובץ לא עומד במגבלות העלאה של הפאנל.", ok=False, details=str(exc)), 400
    except Exception as exc:
        return admin_status_page("שגיאה בהעלאת ZIP", "לא ניתן היה לעבד את קובץ התמונות.", ok=False, details=repr(exc)), 500


@app.route("/upload_usa_music/<music_key>", methods=["POST"])
def upload_usa_music(music_key):
    file = request.files.get("file")
    if not file or file.filename == "":
        return "No file selected", 400
    filename = save_usa_music(music_key, file)
    if not filename:
        return "USA music type not found", 404
    return redirect("/admin")


@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("/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):
    response = make_response(send_from_directory(FOLDERS["JAPAN"], path))
    return no_cache_response(response)


@app.route("/USA/<path:path>")
def usa_static(path):
    response = make_response(send_from_directory(FOLDERS["USA"], path))
    return no_cache_response(response)


@app.route("/Moneyapp/<path:path>")
def money_static(path):
    response = make_response(send_from_directory(FOLDERS["Moneyapp"], path))
    return no_cache_response(response)


@app.route("/MoneyHome/<path:path>")
def moneyhome_static(path):
    response = make_response(send_from_directory(FOLDERS["MoneyHome"], path))
    return no_cache_response(response)


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8000, threaded=True, use_reloader=False)