nategodd/app.py
type-two 338614ee06 Aspect patterns + click-info interactivity
- detect_patterns(): stelliums, grand trines, kites, T-squares, grand
  crosses, mystic rectangles, yods — with element/mode/apex flavor
- Patterns panel on the Wheel tab: hover/click highlights the
  configuration on the wheel (gold overlay, other aspect lines dimmed)
  and pops an explainer
- Click-info popovers everywhere: planet glyphs (position, house,
  dignities, interpretation, its aspects), aspect lines, natal and
  synastry grid cells (with harmony score), biwheel glyphs attribute
  to the right person's chart
- Patterns section added to the written report; annotate_chart() ships
  popover text with the chart JSON

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 13:45:09 +10:00

106 lines
3.6 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)
interp.annotate_chart(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)
na = chart_a["input"]["name"] or "A"
nb = chart_b["input"]["name"] or "B"
for x in syn["interaspects"]:
x["text"] = interp.syn_aspect_blurb(x, na, nb)
interp.annotate_chart(chart_a)
interp.annotate_chart(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)