import sys import os from werkzeug.serving import run_simple sys.path.append(os.path.dirname(os.path.abspath(__file__))) from dotenv import load_dotenv load_dotenv(dotenv_path=os.path.join(os.path.dirname(__file__), ".env")) from flask import Flask, send_from_directory from flask_cors import CORS from constants import Constants from api.api_blueprint import api_blueprint from models.base import db # 保留原本 import(即使現在沒用到) from models.combined_labels import CombinedLabels # 同上 import logging # --- 1. 確保 sessions 目錄有合理預設值 --- SESSION_DIR = Constants.SESSIONS_DIR_NAME or "/app/sessions" Constants.SESSIONS_DIR_NAME = SESSION_DIR def create_session_dir(): path = Constants.SESSIONS_DIR_NAME if not os.path.isdir(path): os.makedirs(path, exist_ok=True) def create_app(): create_session_dir() # --- 2. 靜態檔:dist/ --- # static_folder 指向 Vite build 出來的 dist # static_url_path 設成 "",讓 /assets/... 之類的路徑正確 app = Flask(__name__, static_folder="dist", static_url_path="") # --- 3. API 一律掛在 /api --- app.register_blueprint(api_blueprint, url_prefix="/api") # --- 4. 簡單 health check --- @app.route("/api/ping") def ping(): return {"status": "ok"} # --- 5. React / Vite SPA:所有非靜態路徑 fallback 到 index.html --- @app.route("/", defaults={"path": ""}) @app.route("/") def serve_spa(path: str): """ 1. 如果 dist 裡真的有這個檔案,就當成靜態檔案回傳 2. 如果沒有,就回 dist/index.html,交給 React Router 處理 例如 /case/PanTS_00009391 這種 route """ # 先嘗試當成靜態檔 if path: full_path = os.path.join(app.static_folder, path) if os.path.isfile(full_path): return send_from_directory(app.static_folder, path) # 找不到對應實體檔案 → 交給前端路由 return send_from_directory(app.static_folder, "index.html") # --- 6. Log 過濾:不要一直印 /api/progress --- class FilterProgressRequests(logging.Filter): def filter(self, record): return "/api/progress/" not in record.getMessage() logging.getLogger("werkzeug").addFilter(FilterProgressRequests()) # --- 7. CORS --- CORS(app) # --- 8. SharedArrayBuffer / WebGL 相關 header --- @app.after_request def add_security_headers(response): response.headers["Cross-Origin-Opener-Policy"] = "same-origin" response.headers["Cross-Origin-Embedder-Policy"] = "require-corp" return response return app # 給 gunicorn 用的 WSGI app app = create_app() # --- 9. 本機開發(可選) --- def find_watch_files(): watch_dirs = ["api", "models", "services"] base_path = os.path.dirname(__file__) all_files = [] for d in watch_dirs: dir_path = os.path.join(base_path, d) for root, _, files in os.walk(dir_path): for f in files: if f.endswith(".py"): all_files.append(os.path.join(root, f)) return all_files if __name__ == "__main__": use_ssl = os.environ.get("USE_SSL", "false").lower() == "true" ssl_context = ( ("../certs/localhost-cert.pem", "../certs/localhost-key.pem") if use_ssl else None ) run_simple( hostname="0.0.0.0", port=5001, application=app, use_debugger=True, use_reloader=True, extra_files=find_watch_files(), ssl_context=ssl_context, )