meal-order-manager/src/server/app.py
Adam Moussa f48a82c476
Some checks are pending
Deploy API / Resolve target (push) Waiting to run
Deploy API / Deploy API to (push) Blocked by required conditions
feat(api): serve meals on ECS Fargate instead of Lambda (PLAT-215) (#199)
* feat(api): serve meals on ECS Fargate instead of Lambda

Keep the Flask app always-on with in-process jobs so CloudFront no longer fronts a cold-start API Gateway.

* fix(jobs): run delayed close and reminder deliveries

Wall-clock skip windows dropped the only weekly SQS attempt when Scheduler already fired in Eastern time. Dev schedules stay disabled.

* fix(api): return JSON objects and stop logging job payloads

Flask now jsonify-s handler dicts so API responses are not HTML, and the worker logs only event and status.

* fix(ci): restore the reusable workflow so the required check is named ci / ci

Inlining the job reported `ci` instead of the org ruleset's `ci / ci`.

* fix(secrets): drop unused os import so ruff check passes

* style: apply ruff format so ci-python-app lint passes

* fix(infra): give meals its own VPC because prod has none

* chore(security): re-key ALB SG checkov suppression after vpc.tf
2026-09-21 19:34:24 +00:00

133 lines
3.8 KiB
Python

"""Production Flask app for meal-order-manager.
Local: PYTHONPATH=src:src/shared python3 -m server.app
Prod: gunicorn server.wsgi:app
"""
from __future__ import annotations
import os
from pathlib import Path
import json
from flask import Flask, Response, jsonify, request, send_file
from server import http_api
CORS_ORIGINS = [
"https://orders.seahaven.com",
"https://internal.seahaven.com",
"https://internal.dev.seahaven.com",
"http://localhost:5173",
"http://localhost:4173",
"http://127.0.0.1:5173",
"http://127.0.0.1:5050",
]
PROJECT_ROOT = Path(__file__).resolve().parents[2]
OUTPUT_DIR = PROJECT_ROOT / "output"
def create_app() -> Flask:
app = Flask(__name__)
extra = os.environ.get("CORS_ORIGINS", "")
origins = list(CORS_ORIGINS)
if extra:
origins.extend(o.strip() for o in extra.split(",") if o.strip())
form_url = os.environ.get("FORM_URL", "").rstrip("/")
if form_url and form_url not in origins:
origins.append(form_url)
@app.after_request
def add_cors(resp: Response) -> Response:
origin = request.headers.get("Origin", "")
if origin in origins:
resp.headers["Access-Control-Allow-Origin"] = origin
resp.headers["Vary"] = "Origin"
resp.headers["Access-Control-Allow-Headers"] = (
"Authorization, Content-Type, X-Meals-Publish-Key"
)
resp.headers["Access-Control-Allow-Methods"] = (
"GET, POST, PUT, DELETE, OPTIONS"
)
resp.headers["Access-Control-Max-Age"] = "3600"
return resp
@app.route("/api/health")
def health():
return jsonify(
{
"stage": os.environ.get("STAGE", "local"),
"sha": os.environ.get("GIT_SHA", "dev"),
}
)
@app.route("/", methods=["GET"])
def root():
if os.environ.get("STAGE", "local") == "local":
from datetime import datetime
form_file = OUTPUT_DIR / (
f"order-form-{datetime.now().strftime('%Y-W%U')}.html"
)
if form_file.exists():
return send_file(form_file)
return "ok", 200
def _dispatch(path: str):
if request.method == "OPTIONS":
return "", 204
qs = request.args.to_dict(flat=True)
path_params = {}
parts = path.strip("/").split("/")
if (
len(parts) >= 3
and parts[0] == "api"
and parts[1]
in {
"menu",
"orders",
"form-status",
}
):
path_params["week"] = parts[2]
event = {
"requestContext": {"http": {"method": request.method, "path": path}},
"rawPath": path,
"headers": {k: v for k, v in request.headers.items()},
"body": request.get_data(as_text=True) or "{}",
"pathParameters": path_params,
"queryStringParameters": qs or None,
}
result = http_api.lambda_handler(event, None)
try:
payload = json.loads(result["body"])
except (TypeError, KeyError, json.JSONDecodeError):
resp = jsonify({"error": "Internal error"})
resp.status_code = 500
return resp
if not isinstance(payload, dict):
resp = jsonify({"error": "Internal error"})
resp.status_code = 500
return resp
resp = jsonify(payload)
resp.status_code = int(result.get("statusCode") or 500)
return resp
@app.route("/api/<path:rest>", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"])
def api(rest: str):
return _dispatch("/api/" + rest)
return app
app = create_app()
def main():
app.run(host="0.0.0.0", port=int(os.environ.get("PORT", "5050")))
if __name__ == "__main__":
main()