Add wildlife_bp with sightings, stats, frequency, and CSV export endpoints; Bootstrap 5 dark journal template with live-updating summary cards, species bars, time-of-day frequency chart, and paginated/filterable sightings table. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
73 lines
2.5 KiB
Python
73 lines
2.5 KiB
Python
"""Wildlife journal blueprint — sighting log, stats, export."""
|
|
|
|
import csv
|
|
import io
|
|
|
|
from flask import Blueprint, Response, current_app, jsonify, render_template, request
|
|
|
|
wildlife_bp = Blueprint("wildlife", __name__, url_prefix="/wildlife")
|
|
|
|
|
|
def _engine():
|
|
return current_app.config.get("DB_ENGINE")
|
|
|
|
|
|
@wildlife_bp.route("/")
|
|
def journal():
|
|
return render_template("wildlife/journal.html")
|
|
|
|
|
|
@wildlife_bp.route("/api/sightings")
|
|
def sightings_api():
|
|
engine = _engine()
|
|
if engine is None:
|
|
return jsonify({"sightings": []})
|
|
from vigilar.storage.queries import get_wildlife_sightings_paginated
|
|
sightings = get_wildlife_sightings_paginated(
|
|
engine,
|
|
species=request.args.get("species"),
|
|
threat_level=request.args.get("threat_level"),
|
|
camera_id=request.args.get("camera_id"),
|
|
since_ts=request.args.get("since", type=float),
|
|
until_ts=request.args.get("until", type=float),
|
|
limit=min(request.args.get("limit", 50, type=int), 500),
|
|
offset=request.args.get("offset", 0, type=int),
|
|
)
|
|
return jsonify({"sightings": sightings})
|
|
|
|
|
|
@wildlife_bp.route("/api/stats")
|
|
def stats_api():
|
|
engine = _engine()
|
|
if engine is None:
|
|
return jsonify({"total": 0, "species_count": 0, "per_species": {}})
|
|
from vigilar.storage.queries import get_wildlife_stats
|
|
return jsonify(get_wildlife_stats(engine))
|
|
|
|
|
|
@wildlife_bp.route("/api/frequency")
|
|
def frequency_api():
|
|
engine = _engine()
|
|
if engine is None:
|
|
return jsonify({})
|
|
from vigilar.storage.queries import get_wildlife_frequency
|
|
return jsonify(get_wildlife_frequency(engine))
|
|
|
|
|
|
@wildlife_bp.route("/api/export")
|
|
def export_csv():
|
|
engine = _engine()
|
|
if engine is None:
|
|
return Response("No data", mimetype="text/csv")
|
|
from vigilar.storage.queries import get_wildlife_sightings_paginated
|
|
sightings = get_wildlife_sightings_paginated(engine, limit=10000, offset=0)
|
|
output = io.StringIO()
|
|
writer = csv.writer(output)
|
|
writer.writerow(["id", "timestamp", "species", "threat_level", "camera_id",
|
|
"confidence", "temperature_c", "conditions"])
|
|
for s in sightings:
|
|
writer.writerow([s["id"], s["ts"], s["species"], s["threat_level"],
|
|
s["camera_id"], s.get("confidence"), s.get("temperature_c"), s.get("conditions")])
|
|
return Response(output.getvalue(), mimetype="text/csv",
|
|
headers={"Content-Disposition": "attachment; filename=wildlife_sightings.csv"})
|