All checks were successful
Build and Push / build (push) Successful in 13s
- Replace split setup (ratings_server.py on :8081 + http.server on :8080) with a single combined Flask server (server.py) on :8080 that serves static files and the /api/ratings GET/POST endpoints - Ratings are now persisted server-side: mapa_bytu.html loads ratings from GET /api/ratings on startup (API as source of truth) and POSTs on every change — enables cross-browser and cross-device state sharing while keeping localStorage as a synchronous read cache - Dockerfile: install flask, copy server.py instead of ratings_server.py, expose only port 8080 - entrypoint.sh: start single server process instead of two - Makefile: add serve / serve-debug targets for local development - scrape_psn.py: fix log label, add --max-pages stub arg for CLI parity - Refresh all scraped property data Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
120 lines
3.6 KiB
Python
120 lines
3.6 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Combined HTTP server: serves static files from DATA_DIR and
|
|
provides the ratings API at /api/ratings.
|
|
|
|
GET /api/ratings → returns ratings.json contents
|
|
POST /api/ratings → saves entire ratings object
|
|
GET /api/ratings/export → same as GET, with Content-Disposition: attachment
|
|
GET /<path> → serves static file from DATA_DIR
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import logging
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from flask import Flask, jsonify, request, send_from_directory
|
|
|
|
parser = argparse.ArgumentParser(description="Flat-search map server")
|
|
parser.add_argument("--log-level", "-l", default=None, choices=["DEBUG", "INFO", "WARNING", "ERROR"], help="Log level (default: INFO)")
|
|
parser.add_argument("--verbose", "-v", action="store_true", help="Shorthand for --log-level DEBUG")
|
|
args = parser.parse_args()
|
|
|
|
log_level = logging.DEBUG if args.verbose else getattr(logging, args.log_level or "INFO")
|
|
|
|
PORT = int(os.environ.get("PORT", 8080))
|
|
DATA_DIR = Path(os.environ.get("DATA_DIR", ".")).resolve()
|
|
RATINGS_FILE = DATA_DIR / "ratings.json"
|
|
|
|
logging.basicConfig(
|
|
level=log_level,
|
|
format="%(asctime)s [server] %(levelname)s %(message)s",
|
|
datefmt="%Y-%m-%dT%H:%M:%S",
|
|
)
|
|
log = logging.getLogger(__name__)
|
|
|
|
app = Flask(__name__, static_folder=None)
|
|
app.json.ensure_ascii = False
|
|
|
|
|
|
@app.after_request
|
|
def add_cors(response):
|
|
response.headers["Access-Control-Allow-Origin"] = "*"
|
|
response.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"
|
|
response.headers["Access-Control-Allow-Headers"] = "Content-Type"
|
|
return response
|
|
|
|
|
|
def load_ratings() -> dict:
|
|
try:
|
|
if RATINGS_FILE.exists():
|
|
return json.loads(RATINGS_FILE.read_text(encoding="utf-8"))
|
|
except Exception as e:
|
|
log.error("Failed to load ratings: %s", e)
|
|
return {}
|
|
|
|
|
|
def save_ratings(data: dict) -> None:
|
|
RATINGS_FILE.write_text(
|
|
json.dumps(data, ensure_ascii=False, indent=2),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
|
|
@app.route("/api/ratings", methods=["OPTIONS"])
|
|
@app.route("/api/ratings/export", methods=["OPTIONS"])
|
|
def ratings_options():
|
|
return ("", 204)
|
|
|
|
|
|
@app.route("/api/ratings", methods=["GET"])
|
|
def get_ratings():
|
|
ratings = load_ratings()
|
|
log.info("GET /api/ratings → %d ratings", len(ratings))
|
|
return jsonify(ratings)
|
|
|
|
|
|
@app.route("/api/ratings/export", methods=["GET"])
|
|
def export_ratings():
|
|
ratings = load_ratings()
|
|
log.info("GET /api/ratings/export → %d ratings", len(ratings))
|
|
response = jsonify(ratings)
|
|
response.headers["Content-Disposition"] = 'attachment; filename="ratings.json"'
|
|
return response
|
|
|
|
|
|
@app.route("/api/ratings", methods=["POST"])
|
|
def post_ratings():
|
|
length = request.content_length
|
|
if not length:
|
|
return jsonify({"error": "empty body"}), 400
|
|
try:
|
|
data = request.get_json(force=True, silent=False)
|
|
except Exception as e:
|
|
log.warning("Bad request body: %s", e)
|
|
return jsonify({"error": "invalid JSON"}), 400
|
|
if not isinstance(data, dict):
|
|
return jsonify({"error": "expected JSON object"}), 400
|
|
save_ratings(data)
|
|
log.info("POST /api/ratings → saved %d ratings", len(data))
|
|
return jsonify({"ok": True, "count": len(data)})
|
|
|
|
|
|
@app.route("/")
|
|
def index():
|
|
return send_from_directory(str(DATA_DIR), "mapa_bytu.html")
|
|
|
|
|
|
@app.route("/<path:filename>")
|
|
def static_files(filename):
|
|
return send_from_directory(str(DATA_DIR), filename)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
log.info("Server starting on port %d, data dir: %s", PORT, DATA_DIR)
|
|
log.info("Ratings file: %s", RATINGS_FILE)
|
|
app.run(host="0.0.0.0", port=PORT)
|