PDC / backend /app /main.py
borndeveloper's picture
fix: remove JSON root route so StaticFiles mount serves index.html on /
052a31d
Raw
History Blame Contribute Delete
9.91 kB
import os
from fastapi import FastAPI, Query
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from app.services.data_service import data_service
from app.services.count_converter import (
convert_count,
get_available_standard_counts,
get_available_ply_counts,
nearest_standard_counts,
parse_count_string,
)
from app.services.reed_calculator import get_reed_range
app = FastAPI(title="PDC Intelligence API", version="2.0.0")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.on_event("startup")
async def startup_event() -> None:
print("Loading data...")
data_service.load_data()
print("Warming up API caches...")
data_service.get_dashboard_summary()
data_service.get_filters()
data_service.get_relativity_analytics()
data_service.get_validation_report(sample_size=100, seed=42)
print("Cache warming complete.")
@app.get("/api/health")
def health() -> dict:
return data_service.get_health()
@app.get("/api/dashboard")
def dashboard() -> dict:
return data_service.get_dashboard_summary()
@app.get("/api/filters")
def filters() -> dict:
return data_service.get_filters()
@app.get("/api/articles")
def articles(
page: int = Query(1, ge=1),
limit: int = Query(25, ge=1, le=200),
search: str = "",
weave: str = "",
blend: str = "",
loom_type: str = "",
dataset: str = "",
count_band: str = "",
) -> dict:
return data_service.get_articles(
page, limit, search, weave, blend, loom_type, dataset, count_band,
)
@app.get("/api/article/{master_article}")
def article(master_article: str) -> dict:
return data_service.get_article_detail(master_article)
@app.get("/api/analytics/relativity")
def relativity() -> dict:
return data_service.get_relativity_analytics()
@app.get("/api/process-flow")
def process_flow() -> dict:
return data_service.get_process_flow()
@app.get("/api/documentation")
def documentation() -> dict:
return data_service.get_documentation()
@app.get("/api/data-source/audit")
def data_source_audit() -> dict:
return data_service.get_data_source_audit()
@app.get("/api/validation/report")
def validation_report(
sample_size: int = Query(250, ge=30, le=2000), seed: int = 42,
) -> dict:
return data_service.get_validation_report(sample_size=sample_size, seed=seed)
@app.post("/api/predictions/construction")
def predict(payload: dict) -> dict:
return data_service.predict_construction(payload)
# ============== New endpoints ==============
@app.get("/api/count/available")
def available_counts() -> dict:
return {
"standard_ne": get_available_standard_counts(),
"ply_yarns": get_available_ply_counts(),
}
@app.get("/api/count/convert")
def count_convert(
value: float = Query(...),
from_unit: str = Query("ne"),
to_unit: str = Query("denier"),
) -> dict:
result = convert_count(value, from_unit, to_unit)
return {
"input": {"value": value, "unit": from_unit},
"output": {"value": result, "unit": to_unit},
}
@app.get("/api/count/nearest")
def nearest_counts(
value: float = Query(...),
n: int = Query(3, ge=1, le=10),
) -> dict:
nearest = nearest_standard_counts(value, n)
return {
"input_value": value,
"nearest_standard_ne": nearest,
}
@app.get("/api/count/parse")
def parse_count(count_string: str = Query(...)) -> dict:
result = parse_count_string(count_string)
if result is None:
return {"error": f"Could not parse: {count_string}"}
return {"input": count_string, "parsed": result}
@app.get("/api/reed/ranges")
def reed_ranges(loom_type: str = Query("airjet")) -> dict:
return {
"loom_type": loom_type,
"reed_counts": get_reed_range(loom_type),
"all_airjet": get_reed_range("airjet"),
"all_rapier": get_reed_range("rapier"),
}
@app.post("/api/predictions/export-xml")
def export_xml(payload: dict):
import io
import xml.etree.ElementTree as ET
from fastapi.responses import Response
rec = payload.get("recommendation", {})
inp = payload.get("input", {})
def gv(key: str, fallback=0):
val = rec.get(key)
if val:
try:
return round(float(val), 2)
except (ValueError, TypeError):
return fallback
return fallback
w_cnt = gv("warp_count", inp.get("normalized_warp_count") or inp.get("raw_warp_count") or 0)
wt_cnt = gv("weft_count", inp.get("normalized_weft_count") or inp.get("raw_weft_count") or 0)
reed = gv("reed_count", 0)
e_dent = gv("ends_per_dent", 0)
r_space = gv("reed_space", 0)
g_epi = gv("greige_epi", 0)
g_ppi = gv("greige_ppi", 0)
f_epi = gv("finish_epi", 0)
f_ppi = gv("finish_ppi", 0)
f_width = gv("finish_width", 0)
blend = str(inp.get("blend", ""))
weave = str(inp.get("weave", ""))
root = ET.Element("PenelopeDesigns", Version="v1.12")
fabric = ET.SubElement(root, "Fabric", Name="construction")
fabric.set("Folder1", "BA")
fabric.set("Folder2", "DS")
fabric.set("Folder3", "2026")
yarns = ET.SubElement(fabric, "Yarns")
for yname, count, ylabel in [
("WARP_YARN", w_cnt, "WARP"),
("WEFT_YARN", wt_cnt, "WEFT"),
]:
yarn = ET.SubElement(yarns, "Yarn", DataBaseId="0", Name=yname, Set="construction")
ET.SubElement(yarn, "SecondName").text = ylabel
ET.SubElement(yarn, "Count").text = f"{count} ne" if count else ""
ET.SubElement(yarn, "Material").text = blend
ends_total = int(g_epi * r_space * 0.0254 * 100) if g_epi and r_space else 0
warp = ET.SubElement(fabric, "Warp", NEndsRepeat=str(ends_total), NColorWays="1", NColors="1", NBeams="1")
cw_warp = ET.SubElement(warp, "ColorWay", Name="A")
ET.SubElement(cw_warp, "Yarn", Id="0", DataBaseId="0", Set="construction", Name="WARP_YARN", NEnds=str(ends_total))
weft = ET.SubElement(fabric, "Weft", NEndsRepeat="0", NColorWays="1", NColors="1")
cw_weft = ET.SubElement(weft, "ColorWay", Name="A")
ET.SubElement(cw_weft, "Yarn", Id="0", DataBaseId="0", Set="construction", Name="WEFT_YARN")
ET.SubElement(fabric, "Weave", Name=f"{weave}.", Folder1="WEAVE", NEndsRepeat="0", NPicksRepeat="0")
denting = ET.SubElement(fabric, "Denting", NDents=str(int(e_dent)), NEnds="4")
ET.SubElement(denting, "Dents").text = f"{int(e_dent)}x{int(e_dent)}" if e_dent else "0"
tech = ET.SubElement(fabric, "TechnicalData")
reed_dens_cm = round(reed / 39.37, 1) if reed else 0
ET.SubElement(tech, "ReedDensity", Value=str(reed), Unit="inch", ValueCm=str(reed_dens_cm))
reed_width_cm = round(r_space * 2.54, 2) if r_space else 0
ET.SubElement(tech, "ReedWidth", Value=str(r_space), Unit="inch", ValueCM=str(reed_width_cm))
fin_wid_cm = round(f_width * 2.54, 2) if f_width else 0
ET.SubElement(tech, "FinishedWidth", Value=str(f_width), Unit="inch", ValueCM=str(fin_wid_cm))
ET.SubElement(tech, "FinishedWarpDensity", Value=str(f_epi), Unit="inch", ValueCm=str(round(f_epi / 39.37, 1)))
ET.SubElement(tech, "WovenWarpDensity", Value=str(g_epi), Unit="inch", ValueCm=str(round(g_epi / 39.37, 1)))
ET.SubElement(tech, "WeftDensity", Value=str(g_ppi), Unit="inch", ValueCm=str(round(g_ppi / 39.37, 1)))
ET.SubElement(tech, "Composition", Warp=blend, Weft=blend, Total=blend)
ET.SubElement(tech, "Comment", Weave=weave)
tree = ET.ElementTree(root)
ET.indent(tree, space=" ", level=0)
out = io.BytesIO()
tree.write(out, encoding="utf-8", xml_declaration=True)
return Response(
content=out.getvalue(),
media_type="application/xml",
headers={"Content-Disposition": "attachment; filename=construction.xml"},
)
# Serve Next.js static frontend
FRONTEND_BUILD_DIR = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "..", "frontend", "out"
)
FRONTEND_BUILD_DIR = os.path.abspath(FRONTEND_BUILD_DIR)
if os.path.isdir(FRONTEND_BUILD_DIR):
app.mount(
"/", StaticFiles(directory=FRONTEND_BUILD_DIR, html=True), name="frontend"
)
else:
@app.get("/")
def serve_frontend():
return HTMLResponse(
content="""
<!DOCTYPE html>
<html>
<head>
<title>PDC Intelligence</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-slate-50">
<div class="p-8">
<h1 class="text-2xl font-bold text-slate-800">PDC Intelligence</h1>
<p class="text-slate-600 mt-2">Backend is running. API available at /api/*</p>
<div class="mt-4 p-4 bg-white rounded-lg border border-slate-200">
<h2 class="font-semibold">Available Endpoints:</h2>
<ul class="mt-2 space-y-1 text-sm">
<li><code class="bg-slate-100 px-2 py-1">GET /api/health</code> - Health check</li>
<li><code class="bg-slate-100 px-2 py-1">GET /api/dashboard</code> - Dashboard summary</li>
<li><code class="bg-slate-100 px-2 py-1">GET /api/filters</code> - Available filters</li>
<li><code class="bg-slate-100 px-2 py-1">GET /api/articles</code> - Article search</li>
<li><code class="bg-slate-100 px-2 py-1">POST /api/predictions/construction</code> - Construction prediction</li>
</ul>
</div>
</div>
</body>
</html>
""",
status_code=200,
)