⬅ חזרה לתיקייה
✏️ עריכת admin_server_backup_20260630_220920.py
C:\TripServer\backups\admin_server\admin_server_backup_20260630_220920.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" target="_blank">בדיקת מצגת ארה״ב</a> <a href="/USA/usa2027-presentation.html?from=admin" 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">אמריקה על פול ווליום / תמונת Road Trip כללית</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">שקף 3 במצגת: מפת המסלול הכללית של מערב ארה״ב. נשמר אוטומטית בשם roadtrip-map.jpg</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">נחיתה בסן פרנסיסקו / Golden Gate / Skyline</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 / מפרץ, אלקטרז, גשר הזהב</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">סן פרנסיסקו → מונטריי / כביש 1, 17-Mile 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-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">אחרי יום 4 — גשר Bixby / Big Sur / McWay Falls / כביש 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="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">Pismo → לוס אנג׳לס / חוף, דקלים, כניסה ל־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 / הוליווד, Griffith, 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-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">אחרי יום 7 — דיינר אמריקאי קלאסי / עצירת Road Trip</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 / Bryce Canyon, Hoodoos, נוף יוטה</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 / קניון אנטילופ, פרסת הסוס</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 / כניסה לווגאס, 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-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">אחרי יום 14 — הדרך מווגאס למדבר / עמק המוות / כביש פתוח</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">אחרי Death Valley ולפני Yosemite — Mono Lake / הרי הסיירה / מעבר ליוסמיטי</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 / 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">Premier Check-In + Islands of Adventure / Hulk, Hogwarts, 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-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 / גלוב יוניברסל ו־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 Universe, פורטלים, פארק חדש</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 / Krakatau, מגלשות מים, פארק מים</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 / Sawgrass, שופינג, מעבר למיאמי</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">החזרת רכב ועלייה לקרוז / נמל מיאמי ואוניית קרוז ענקית</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 / סיפון אונייה, בריכה, אוקיינוס</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 / מגלשות, בריכות ואווירת אונייה</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 / חוף קריבי, מים טורקיז, אי</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 / דולפינים וחוף קריבי</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 / שקיעה מהסיפון ואוקיינוס</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 / CocoCay, חוף טורקיז, פארק מים</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">ירידה מהקרוז + לילה ליד השדה / מיאמי, שדה, סגירת טיול</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" 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)
שמור קובץ