v3: PostgreSQL backend — 20K rows seeded, server-side SQL filtering

- Added PostgreSQL 16 Alpine service to Docker Swarm stack
- db.py: schema for 17 tables, auto-seed from jv_data.json on first boot
- /api/dashboard/data?start=&end= — server-side SQL filtering
  All Time: 0.43s (was 4MB JSON download stuck loading)
  Filtered (12mo): 0.20s with ~90% less data transferred
- Dashboard HTML patched: calls API instead of static JSON
- Integer casting for IsNewCustomer/HasDiscount/IsFreeShipping
- Advisory lock prevents race condition during parallel worker startup
- Returning Revenue now shows correctly: £14.5M (75% of total)
This commit is contained in:
2026-03-02 20:24:15 +08:00
parent 734142cf8f
commit 3f2b6b6188
5 changed files with 688 additions and 17 deletions

60
app.py
View File

@@ -1,7 +1,8 @@
"""JustVitamin × QuikCue — AI Content Engine
Live proposal site with real Gemini-powered demos."""
Live proposal site with real Gemini-powered demos.
PostgreSQL-backed data dashboard."""
import os, json, time, traceback
import os, json, time, traceback, logging
from pathlib import Path
from flask import (Flask, render_template, jsonify, request,
send_from_directory, redirect)
@@ -9,6 +10,9 @@ from flask import (Flask, render_template, jsonify, request,
from scraper import scrape_product, scrape_competitor
from ai_engine import (generate_asset_pack, competitor_xray, pdp_surgeon,
optimise_pdp_copy, generate_product_images)
from db import init_db, get_dashboard_data, get_meta
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
app = Flask(__name__,
static_folder="static",
@@ -17,6 +21,13 @@ app = Flask(__name__,
GEN_DIR = Path(__file__).parent / "generated"
GEN_DIR.mkdir(exist_ok=True)
# ── Init DB on startup ───────────────────────────────────────
with app.app_context():
try:
init_db()
except Exception as e:
logging.error(f"DB init failed: {e}")
# ── Page routes ──────────────────────────────────────────────
@app.route("/")
@@ -32,6 +43,37 @@ def dashboard():
def dashboard_files(filename):
return send_from_directory("static/dashboard", filename)
# ── Dashboard API (Postgres-backed) ─────────────────────────
@app.route("/api/dashboard/data")
def api_dashboard_data():
"""Return filtered dashboard data from PostgreSQL.
Query params: ?start=YYYY-MM&end=YYYY-MM
Replaces the 4MB static JSON with fast server-side SQL filtering."""
start = request.args.get("start") or None
end = request.args.get("end") or None
try:
t0 = time.time()
data = get_dashboard_data(start, end)
data["_query_time"] = f"{time.time()-t0:.3f}s"
data["_source"] = "postgresql"
resp = jsonify(data)
resp.headers['Cache-Control'] = 'public, max-age=60'
return resp
except Exception as e:
traceback.print_exc()
return jsonify({"error": str(e)}), 500
@app.route("/api/dashboard/meta")
def api_dashboard_meta():
"""Return just the meta info (fast, tiny)."""
try:
return jsonify(get_meta())
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/proposal")
def proposal():
return send_from_directory("static/proposal", "index.html")
@@ -185,9 +227,23 @@ def api_generate_images():
@app.route("/api/health")
def health():
db_ok = False
try:
from db import get_conn
conn = get_conn()
cur = conn.cursor()
cur.execute("SELECT COUNT(*) FROM monthly")
count = cur.fetchone()[0]
cur.close()
conn.close()
db_ok = count > 0
except Exception:
pass
return jsonify({
"status": "ok",
"gemini_key_set": bool(os.environ.get("GEMINI_API_KEY")),
"db_connected": db_ok,
"db_rows": count if db_ok else 0,
})