"""smb-crm back-office service. Postgres is the source of truth. This serves the operator dashboard + a small JSON API (read now; add/edit/delete and the DB->Sheets mirror layered on next). Browser access is gated by Caddy basic-auth on onboard.mivanchenko.de; the machine-to-machine ingest path (n8n) is gated by the CRM_API_TOKEN header. """ import os from datetime import datetime, date from decimal import Decimal from flask import Flask, jsonify, request, Response, send_from_directory from waitress import serve import db app = Flask(__name__, static_folder="static", static_url_path="") LIST_ORDER = { "leads": "received_at DESC NULLS LAST", "clients": "client_id", "projects": "project_id", "bookings": "start_time DESC NULLS LAST", "invoices": "issued_date DESC NULLS LAST", "activity_log": "ts DESC NULLS LAST", } def jsonable(v): if isinstance(v, (datetime, date)): return v.isoformat() if isinstance(v, Decimal): return float(v) return v def rows_json(rows): return [{k: jsonable(v) for k, v in r.items()} for r in rows] @app.get("/healthz") def healthz(): try: with db.connect() as conn, conn.cursor() as cur: cur.execute("SELECT 1") cur.fetchone() return Response("ok\n", mimetype="text/plain") except Exception as e: # noqa: BLE001 return Response(f"db error: {e}\n", status=500, mimetype="text/plain") @app.get("/api/") def list_entity(entity): if entity not in db.TABLES: return jsonify({"error": "unknown entity"}), 404 order = LIST_ORDER.get(entity, db.TABLES[entity]["pk"]) with db.connect() as conn, conn.cursor() as cur: cur.execute(f"SELECT * FROM {entity} ORDER BY {order}") rows = cur.fetchall() return jsonify({"entity": entity, "count": len(rows), "rows": rows_json(rows)}) @app.get("/") def index(): return send_from_directory(app.static_folder, "index.html") if __name__ == "__main__": serve(app, host="0.0.0.0", port=8080)