- /api/synastry: inter-chart aspects with weighted harmony/tension scores, house overlays, written compatibility report - Compatibility tab: biwheel (partner ring in gold), inter-aspect grid, score gauges - Export toolbar: chart .json (re-loadable via Load file), wheel SVG/PNG, print CSS covers synastry - Aspect types explained reference + pair notes for 30+ classic pairs woven into natal aspect texts - Wheel fixes: correct arc sweep direction, text-presentation zodiac glyphs (no more emoji rendering) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
99 lines
3.4 KiB
Python
99 lines
3.4 KiB
Python
"""NATEGODD — natal chart generator. Run: .venv/bin/python app.py → http://localhost:7799"""
|
|
from flask import Flask, jsonify, request, send_from_directory
|
|
|
|
import chart as chart_engine
|
|
import geo
|
|
import interp
|
|
|
|
app = Flask(__name__, static_folder="static", static_url_path="")
|
|
|
|
|
|
@app.get("/")
|
|
def index():
|
|
return send_from_directory("static", "index.html")
|
|
|
|
|
|
@app.get("/api/search")
|
|
def api_search():
|
|
q = request.args.get("q", "")
|
|
return jsonify(geo.search_cities(q))
|
|
|
|
|
|
@app.get("/api/reference")
|
|
def api_reference():
|
|
return jsonify({"aspects": [
|
|
{"name": k, "glyph": chart_engine.ASPECT_GLYPHS[k], "geometry": v[0], "text": v[1]}
|
|
for k, v in interp.ASPECT_EXPLAINER.items()
|
|
]})
|
|
|
|
|
|
def cast(d):
|
|
"""Validate one chart-input dict and compute its chart. Raises ValueError."""
|
|
try:
|
|
year, month, day = int(d["year"]), int(d["month"]), int(d["day"])
|
|
time_unknown = bool(d.get("time_unknown"))
|
|
hour = 12 if time_unknown else int(d.get("hour", 12))
|
|
minute = 0 if time_unknown else int(d.get("minute", 0))
|
|
second = int(d.get("second", 0) or 0)
|
|
lat, lon = float(d["lat"]), float(d["lon"])
|
|
house_system = d.get("house_system", "P")
|
|
if house_system not in chart_engine.HOUSE_SYSTEMS:
|
|
house_system = "P"
|
|
except (KeyError, TypeError) as e:
|
|
raise ValueError(f"bad input: {e}")
|
|
|
|
if d.get("utc_offset") not in (None, ""):
|
|
offset, tzname = float(d["utc_offset"]), "manual"
|
|
else:
|
|
try:
|
|
offset, tzname = geo.resolve_offset(lat, lon, year, month, day, hour, minute,
|
|
tz_hint=d.get("tz"))
|
|
except Exception as e:
|
|
raise ValueError(f"could not resolve timezone: {e}")
|
|
|
|
result = chart_engine.compute_chart(year, month, day, hour, minute, second,
|
|
offset, lat, lon, house_system, time_unknown)
|
|
result["input"] = {
|
|
"name": d.get("name", ""), "place": d.get("place", ""),
|
|
"lat": lat, "lon": lon, "tz": tzname, "utc_offset": offset,
|
|
"date": f"{year:04d}-{month:02d}-{day:02d}",
|
|
"time": None if time_unknown else f"{hour:02d}:{minute:02d}",
|
|
}
|
|
return result
|
|
|
|
|
|
@app.post("/api/chart")
|
|
def api_chart():
|
|
d = request.get_json(force=True)
|
|
try:
|
|
result = cast(d)
|
|
except ValueError as e:
|
|
return jsonify({"error": str(e)}), 400
|
|
except Exception as e:
|
|
return jsonify({"error": f"computation failed: {e}"}), 400
|
|
result["interpretation"] = interp.build_interpretation(result)
|
|
return jsonify(result)
|
|
|
|
|
|
@app.post("/api/synastry")
|
|
def api_synastry():
|
|
d = request.get_json(force=True)
|
|
if not isinstance(d, dict) or "a" not in d or "b" not in d:
|
|
return jsonify({"error": "expected {a: <chart input>, b: <chart input>}"}), 400
|
|
try:
|
|
chart_a, chart_b = cast(d["a"]), cast(d["b"])
|
|
except ValueError as e:
|
|
return jsonify({"error": str(e)}), 400
|
|
except Exception as e:
|
|
return jsonify({"error": f"computation failed: {e}"}), 400
|
|
syn = chart_engine.compute_synastry(chart_a, chart_b)
|
|
syn["interpretation"] = interp.synastry_interpretation(
|
|
syn, chart_a["input"]["name"], chart_b["input"]["name"])
|
|
syn["chart_a"] = chart_a
|
|
syn["chart_b"] = chart_b
|
|
return jsonify(syn)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app.run(host="0.0.0.0", port=7799, debug=False)
|