Add Polaris frontend: storefront + admin console (React 18/Vite/TS/Tailwind)

- Shared src/lib/api.ts fetch wrapper -> /api with Cache-Control: no-store
- Storefront: home hero, shop grid (category filter + search), product detail,
  cart, checkout (POST /api/orders -> confirmation + order number), order tracking
- Admin: JWT login, today KPI dashboard (revenue/orders/gross/net/margin/aov/refunds),
  top products, supplier performance, product import, lifecycle view, order list
  with tracking simulation
- Multi-stage Dockerfile (Vite build -> nginx static + /api proxy -> api:8000)
- nginx.conf with SPA fallback and no-store HTML caching
- mock/mock_api.py for local verification (excluded from image)
This commit is contained in:
drjones
2026-08-25 20:28:26 -07:00
parent 035bb3a2e9
commit b09dcfd929
39 changed files with 6067 additions and 0 deletions

359
frontend/mock/mock_api.py Normal file
View File

@@ -0,0 +1,359 @@
#!/usr/bin/env python3
"""Polaris mock API — serves /api endpoints for FRONTEND VERIFICATION ONLY.
Run: python3 mock_api.py [port] (default 8000)
Not part of the frontend Docker image (.dockerignore excludes mock/).
Mimics the contract in CONTRACT.md against db/schema.sql shapes.
"""
import json
import random
import time
import uuid
from datetime import datetime, timedelta, timezone
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import parse_qs, urlparse
ADMIN_EMAIL = "admin@polaris.shop"
ADMIN_PASSWORD = "polaris-admin"
TOKEN = "mock-jwt-payload-not-for-production"
CATS = ["Home", "Gadgets", "Office", "Audio"]
PRODUCT_SEEDS = [
("Aurora Desk Lamp", "Home", "AuroraLight", 12.50, 38.50),
("Nimbus Smart Scale", "Home", "NimbusLab", 18.20, 52.00),
("Polaris Pro Keyboard", "Gadgets", "Keystroke", 24.90, 79.00),
("Stellar Mouse MX", "Gadgets", "Keystroke", 11.30, 44.00),
("Comet USB-C Hub", "Gadgets", "PortWorks", 15.60, 59.00),
("Galaxy Standing Desk", "Office", "WorkSpan", 145.00, 389.00),
("Nova Ergonomic Chair", "Office", "WorkSpan", 98.00, 279.00),
("Lunar Notebook Set", "Office", "PaperNest", 4.80, 16.50),
("Eclipse Headphones", "Audio", "SoundOrbit", 32.00, 129.00),
("Pulsar Bluetooth Speaker", "Audio", "SoundOrbit", 21.50, 74.00),
("Quasar Webcam 4K", "Gadgets", "ClearSight", 39.00, 119.00),
("Vega Monitor Arm", "Office", "WorkSpan", 27.00, 89.00),
]
LIFECYCLE = [
"discovered", "analyzing", "approved", "imported", "content_generated",
"price_calculated", "quality_check", "published", "published", "published",
"monitored", "paused",
]
STATUS_FLOW = ["new", "paid", "fraud_check", "supplier_order", "confirmed", "shipped", "delivered"]
def now_iso(days_ago=0, hours_ago=0):
return (datetime.now(timezone.utc) - timedelta(days=days_ago, hours=hours_ago)).isoformat()
def build_products():
products = []
for i, (title, cat, brand, cost, retail) in enumerate(PRODUCT_SEEDS, start=1):
products.append({
"id": f"prd-{i:03d}",
"sku": f"POL-{i:04d}",
"title": title,
"description": f"{title} — sourced, vetted and dynamically priced by the Polaris pipeline. Ships from a top-scoring supplier with live tracking.",
"category": cat,
"brand": brand,
"manufacturer": brand,
"upc_ean_gtin": f"8{i:013d}",
"images": [],
"dimensions": {"l_cm": 20, "w_cm": 14, "h_cm": 8},
"weight_grams": 300 + i * 40,
"cost": cost,
"retail_price": retail,
"min_price": round(cost * 1.8, 2),
"max_price": round(retail * 1.35, 2),
"status": LIFECYCLE[i % len(LIFECYCLE)],
"lifecycle": {"steps_completed": i % 8},
"created_at": now_iso(days_ago=20 - i % 14),
"updated_at": now_iso(hours_ago=i),
})
return products
PRODUCTS = build_products()
SUPPLIERS = [
{"id": "sup-001", "name": "Starline Wholesale", "adapter_type": "csv_http", "api_endpoint": "https://feeds.starline.example/products.csv", "reliability_score": 0.94, "status": "active", "created_at": now_iso(days_ago=40)},
{"id": "sup-002", "name": "NorthBridge Supply", "adapter_type": "xml_ftp", "api_endpoint": "ftp://nb.example/feed.xml", "reliability_score": 0.87, "status": "active", "created_at": now_iso(days_ago=31)},
{"id": "sup-003", "name": "Meridian Goods", "adapter_type": "json_api", "api_endpoint": "https://api.meridian.example/v2", "reliability_score": 0.71, "status": "paused", "created_at": now_iso(days_ago=12)},
]
SUPPLIER_PERF = [
{"supplier_id": "sup-001", "supplier_name": "Starline Wholesale", "fulfillment_rate": 0.96, "avg_shipping_days": 2.1, "cancellation_rate": 0.02, "stock_accuracy": 0.93, "return_rate": 0.03, "updated_at": now_iso()},
{"supplier_id": "sup-002", "supplier_name": "NorthBridge Supply", "fulfillment_rate": 0.89, "avg_shipping_days": 3.4, "cancellation_rate": 0.07, "stock_accuracy": 0.84, "return_rate": 0.05, "updated_at": now_iso()},
{"supplier_id": "sup-003", "supplier_name": "Meridian Goods", "fulfillment_rate": 0.72, "avg_shipping_days": 5.2, "cancellation_rate": 0.16, "stock_accuracy": 0.66, "return_rate": 0.09, "updated_at": now_iso()},
]
ORDERS = [
{
"id": "ord-1001", "order_number": "POL-1001", "customer_id": "cus-1", "customer_email": "ada@example.com",
"items": [{"product_id": "prd-001", "qty": 2, "unit_price": 38.50, "title": "Aurora Desk Lamp"}],
"retail_total": 77.00, "supplier_id": "sup-001", "supplier_cost": 25.00, "shipping_cost": 6.99, "fees": 1.5,
"profit": 43.51, "status": "delivered", "tracking": "1Z999AA10123456784", "carrier": "UPS",
"created_at": now_iso(days_ago=6), "updated_at": now_iso(days_ago=3),
},
{
"id": "ord-1002", "order_number": "POL-1002", "customer_id": "cus-2", "customer_email": "grace@example.com",
"items": [{"product_id": "prd-003", "qty": 1, "unit_price": 79.00, "title": "Polaris Pro Keyboard"}, {"product_id": "prd-004", "qty": 1, "unit_price": 44.00, "title": "Stellar Mouse MX"}],
"retail_total": 123.00, "supplier_id": "sup-002", "supplier_cost": 36.20, "shipping_cost": 0.0, "fees": 2.4,
"profit": 84.40, "status": "shipped", "tracking": "9400100000000000000001", "carrier": "USPS",
"created_at": now_iso(days_ago=1, hours_ago=5), "updated_at": now_iso(hours_ago=2),
},
{
"id": "ord-1003", "order_number": "POL-1003", "customer_id": "cus-3", "customer_email": "linus@example.com",
"items": [{"product_id": "prd-009", "qty": 1, "unit_price": 129.00, "title": "Eclipse Headphones"}],
"retail_total": 129.00, "supplier_id": "sup-001", "supplier_cost": 32.00, "shipping_cost": 0.0, "fees": 2.5,
"profit": 94.50, "status": "confirmed", "tracking": None, "carrier": None,
"created_at": now_iso(hours_ago=9), "updated_at": now_iso(hours_ago=4),
},
{
"id": "ord-1004", "order_number": "POL-1004", "customer_id": "cus-4", "customer_email": "katherine@example.com",
"items": [{"product_id": "prd-010", "qty": 2, "unit_price": 74.00, "title": "Pulsar Bluetooth Speaker"}],
"retail_total": 148.00, "supplier_id": "sup-002", "supplier_cost": 43.00, "shipping_cost": 7.99, "fees": 2.9,
"profit": 94.11, "status": "fraud_check", "tracking": None, "carrier": None,
"created_at": now_iso(hours_ago=1), "updated_at": now_iso(hours_ago=1),
},
]
def summary_payload():
return {
"revenue": 4210.44,
"orders": 17,
"gross_profit": 987.12,
"net_profit": 812.90,
"margin": 19.3,
"aov": 247.67,
"conversion": 3.1,
"refunds": 2,
"refunded_orders": 2,
}
def top_products_payload():
rows = [
("prd-009", "Eclipse Headphones", "POL-0009", 6, 774.00, 582.00),
("prd-003", "Polaris Pro Keyboard", "POL-0003", 5, 395.00, 270.50),
("prd-001", "Aurora Desk Lamp", "POL-0001", 8, 308.00, 208.00),
("prd-006", "Galaxy Standing Desk", "POL-0006", 1, 389.00, 244.00),
("prd-010", "Pulsar Bluetooth Speaker", "POL-0010", 4, 296.00, 210.00),
]
return [
{"product_id": pid, "title": t, "sku": sku, "units_sold": u, "revenue": r, "profit": p}
for pid, t, sku, u, r, p in rows
]
class Handler(BaseHTTPRequestHandler):
server_version = "PolarisMock/0.1"
def log_message(self, fmt, *args): # quieter logs
pass
# ── helpers ──
def send_json(self, obj, status=200):
body = json.dumps(obj).encode()
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Cache-Control", "no-store")
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def read_body(self):
length = int(self.headers.get("Content-Length") or 0)
if not length:
return {}
try:
return json.loads(self.rfile.read(length))
except Exception:
return {}
def not_found(self):
self.send_json({"detail": "Not found"}, 404)
# ── routing ──
def do_OPTIONS(self):
self.send_response(204)
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Authorization, Content-Type, Cache-Control")
self.end_headers()
def do_GET(self):
u = urlparse(self.path)
path = u.path.rstrip("/") or "/"
q = parse_qs(u.query)
if path == "/api/health":
return self.send_json({"status": "ok", "service": "polaris-api-mock", "version": "0.1.0"})
if path == "/api/products":
status = (q.get("status") or [None])[0]
cat = (q.get("category") or [None])[0]
search = (q.get("search") or [None])[0]
try:
limit = int((q.get("limit") or ["100"])[0])
except ValueError:
limit = 100
rows = [p for p in PRODUCTS
if not status or p["status"] == status]
if cat:
rows = [p for p in rows if p["category"] == cat]
if search:
s = search.lower()
rows = [p for p in rows if s in p["title"].lower() or s in p["sku"].lower()]
return self.send_json(rows[:limit])
if path.startswith("/api/products/") and path.count("/") == 3 and path.endswith("/price-history"):
pid = path.split("/")[3]
prod = next((p for p in PRODUCTS if p["id"] == pid), None)
if not prod:
return self.not_found()
retail = prod["retail_price"] or 50
history = []
for i in range(8):
history.append({
"price": round(retail * (0.86 + 0.14 * (i / 7)) + random.uniform(-1.5, 1.5), 2),
"reason": "market reprice" if i % 2 else "initial pricing",
"created_at": now_iso(days_ago=7 - i),
})
return self.send_json(history)
if path.startswith("/api/products/"):
pid = path.split("/")[3]
prod = next((p for p in PRODUCTS if p["id"] == pid), None)
return self.send_json(prod) if prod else self.not_found()
if path == "/api/suppliers":
return self.send_json(SUPPLIERS)
if path == "/api/orders":
return self.send_json(ORDERS)
if path.startswith("/api/orders/"):
oid = path.split("/")[3]
order = next((o for o in ORDERS if o["id"] == oid or o["order_number"] == oid), None)
return self.send_json(order) if order else self.not_found()
if path == "/api/analytics/summary":
return self.send_json(summary_payload())
if path == "/api/analytics/top-products":
return self.send_json(top_products_payload())
if path == "/api/analytics/supplier-performance":
return self.send_json(SUPPLIER_PERF)
return self.not_found()
def do_POST(self):
u = urlparse(self.path)
path = u.path.rstrip("/") or "/"
body = self.read_body()
if path == "/api/auth/login":
email = (body.get("email") or "").strip().lower()
password = body.get("password") or ""
if email == ADMIN_EMAIL and password == ADMIN_PASSWORD:
return self.send_json({"access_token": TOKEN, "token_type": "bearer", "email": email})
return self.send_json({"detail": "Invalid email or password"}, 401)
if path == "/api/auth/register":
return self.send_json({"access_token": TOKEN, "token_type": "bearer", "email": body.get("email")})
if path == "/api/orders":
email = body.get("customer_email") or "guest@example.com"
items = []
total = 0.0
for it in body.get("items") or []:
prod = next((p for p in PRODUCTS if p["id"] == it.get("product_id")), None)
qty = max(1, int(it.get("qty") or 1))
price = (prod["retail_price"] if prod else 29.99)
total += price * qty
items.append({"product_id": it.get("product_id"), "qty": qty, "unit_price": price, "title": prod["title"] if prod else None})
order = {
"id": f"ord-{random.randint(2000, 9999)}",
"order_number": f"POL-{random.randint(100000, 999999)}",
"customer_id": None,
"customer_email": email,
"items": items,
"retail_total": round(total, 2),
"supplier_id": "sup-001",
"supplier_cost": None,
"shipping_cost": 0.0,
"fees": 0.0,
"profit": None,
"status": "new",
"tracking": None,
"carrier": None,
"created_at": now_iso(),
"updated_at": now_iso(),
}
ORDERS.insert(0, order)
return self.send_json(order, 201)
if path.startswith("/api/orders/") and path.endswith("/tracking"):
oid = path.split("/")[3]
order = next((o for o in ORDERS if o["id"] == oid or o["order_number"] == oid), None)
if not order:
return self.not_found()
idx = STATUS_FLOW.index(order["status"]) if order["status"] in STATUS_FLOW else 0
order["status"] = STATUS_FLOW[min(idx + 1, len(STATUS_FLOW) - 1)]
if order["status"] in ("shipped", "delivered") and not order["tracking"]:
order["tracking"] = f"1Z{random.randint(10**15, 10**16 - 1)}"
order["carrier"] = "UPS"
order["updated_at"] = now_iso()
return self.send_json(order)
if path.startswith("/api/products/") and path.count("/") == 3 and (
path.endswith("/publish") or path.endswith("/pause")
):
pid = path.split("/")[3]
prod = next((p for p in PRODUCTS if p["id"] == pid), None)
if not prod:
return self.not_found()
prod["status"] = "published" if path.endswith("/publish") else "paused"
prod["updated_at"] = now_iso()
return self.send_json(prod)
if path == "/api/products/import":
imported = random.randint(3, 9)
for i in range(imported):
title = f"Imported Feed Item {len(PRODUCTS) + 1}"
PRODUCTS.append({
"id": f"prd-{100 + len(PRODUCTS)}",
"sku": f"IMP-{len(PRODUCTS):04d}",
"title": title,
"description": "Imported via feed — awaiting pipeline analysis.",
"category": random.choice(CATS),
"brand": body.get("supplier_id") or "FeedImport",
"manufacturer": None,
"upc_ean_gtin": None,
"images": [],
"dimensions": None,
"weight_grams": None,
"cost": 0.0,
"retail_price": None,
"min_price": None,
"max_price": None,
"status": "discovered",
"lifecycle": {},
"created_at": now_iso(),
"updated_at": now_iso(),
})
return self.send_json({"message": f"Feed accepted — {imported} products staged for the pipeline.", "imported": imported}, 201)
return self.not_found()
if __name__ == "__main__":
port = int(__import__("sys").argv[1]) if len(__import__("sys").argv) > 1 else 8000
print(f"Polaris mock API on :{port} (admin: {ADMIN_EMAIL} / {ADMIN_PASSWORD})")
HTTPServer(("0.0.0.0", port), Handler).serve_forever()