Teich / app.py
LarsDiedrich22
Deploy Teich Space (rooted from froschgruppe/latest_froschgruppe)
f00396e
import os, sys, json
from datetime import datetime, date
from pathlib import Path
import gradio as gr
# Make src importable
sys.path.append("src")
from src.latest_froschgruppe.tools.organizer_tool import OrganizerTool
from src.latest_froschgruppe.tools.payroll_tool import PayrollTool
from src.latest_froschgruppe.tools.roadkill_gbif_tool import RoadkillGBIFTool
from src.latest_froschgruppe.tools.calendar_tool import CalendarTool # ICS generator
from src.latest_froschgruppe.tools.gbif_occurrence_tool import GBIFOccurrenceTool
# Optional: if you have folium map tool
# from latest_froschgruppe.tools.map_tool import build_map
DATA_DIR = Path(os.getenv("DATA_DIR", "data"))
DATA_DIR.mkdir(parents=True, exist_ok=True)
EVENTS_FILE = DATA_DIR / "events.json"
organizer = OrganizerTool()
payroll = PayrollTool()
gbif = GBIFOccurrenceTool()
roadkill = RoadkillGBIFTool()
cal = CalendarTool()
def _load_events():
if not EVENTS_FILE.exists():
return []
return json.loads(EVENTS_FILE.read_text(encoding="utf-8"))
def _save_events(events):
EVENTS_FILE.write_text(json.dumps(events, ensure_ascii=False, indent=2), encoding="utf-8")
def add_event(title: str, start_date: str, start_time: str, end_time: str, location: str):
events = _load_events()
# store ISO-ish strings
start = f"{start_date}T{start_time}"
end = f"{start_date}T{end_time}"
ev = {
"id": f"E{len(events)+1:04d}",
"title": title.strip(),
"start": start,
"end": end,
"location": location.strip(),
"created_at": datetime.utcnow().isoformat() + "Z",
}
events.append(ev)
_save_events(events)
return list_events()
def delete_event(event_id: str):
events = _load_events()
events = [e for e in events if e["id"] != event_id.strip()]
_save_events(events)
return list_events()
def list_events():
events = _load_events()
events_sorted = sorted(events, key=lambda e: e.get("start", ""))
# dataframe-like list
rows = []
for e in events_sorted[:200]:
rows.append([e["id"], e["start"], e.get("end",""), e["title"], e.get("location","")])
return rows
def dashboard_snapshot():
# Todos
todos_md = organizer._run("list_todos", "{}")
vols_md = organizer._run("list_volunteers", "{}")
# Payroll report
payroll_md = payroll._run("report", "{}")
# Events
events_rows = list_events()
events_md = "## Nächste Events\n"
if not events_rows:
events_md += "Keine Events gespeichert."
else:
# show only next 10
events_md += "\n".join([f"- {r[1]} | {r[3]} @ {r[4]} (ID {r[0]})" for r in events_rows[:10]])
# Next migration window (simple heuristic)
today = date.today()
year = today.year
# Typical window for DE: Feb 15 – Apr 30 (configurable)
# If already past, show next year.
window_start = date(year, 2, 15)
window_end = date(year, 4, 30)
if today > window_end:
window_start = date(year + 1, 2, 15)
window_end = date(year + 1, 4, 30)
mig_md = (
"## Nächste Wanderphase (operatives Fenster)\n"
f"- Fenster: **{window_start.isoformat()} bis {window_end.isoformat()}**\n"
"- Trigger (praktisch): milde, feuchte Abende/Nächte + Regen → Einsatzwahrscheinlichkeit hoch.\n"
"- Tipp: täglich 17:00 Wettercheck + bei Trigger Nachtteam aktivieren."
)
return mig_md, events_md, todos_md, vols_md, payroll_md
def add_todo(title: str, priority: str):
payload = json.dumps({"title": title, "priority": priority}, ensure_ascii=False)
msg = organizer._run("add_todo", payload)
return msg, organizer._run("list_todos", "{}")
def add_volunteer(name: str, phone: str, availability: str):
payload = json.dumps({"name": name, "phone": phone, "availability": availability}, ensure_ascii=False)
msg = organizer._run("add_volunteer", payload)
return msg, organizer._run("list_volunteers", "{}")
def payroll_plan(hourly_rate: float, monthly_cap: float):
payload = json.dumps({"hourly_rate": hourly_rate, "monthly_cap": monthly_cap}, ensure_ascii=False)
return payroll._run("plan", payload)
def payroll_add_worker(name: str, hourly_rate: float, monthly_cap: float):
payload = json.dumps({"name": name, "hourly_rate": hourly_rate, "monthly_cap": monthly_cap}, ensure_ascii=False)
msg = payroll._run("add_worker", payload)
rep = payroll._run("report", "{}")
return msg, rep
def payroll_log_hours(name: str, date_str: str, hours: float, note: str):
payload = json.dumps({"name": name, "date": date_str, "hours": hours, "note": note}, ensure_ascii=False)
msg = payroll._run("log_hours", payload)
rep = payroll._run("report", "{}")
return msg, rep
def fetch_hotspots(species: str, country: str, year_from: int, year_to: int):
# Tools return markdown tables + google maps links
gbif_md = gbif._run(scientific_name=species, country=country, year_from=year_from, year_to=year_to, limit=300)
road_md = roadkill._run(country=country, scientific_name=species, year_from=year_from, year_to=year_to, limit=300)
return gbif_md, road_md
def generate_migration_ics(region: str, year: int, start_month: int, start_day: int, end_month: int, end_day: int):
# CalendarTool returns a path in text; we want a file output
txt = cal._run(region=region, year=year, start_month=start_month, start_day=start_day, end_month=end_month, end_day=end_day)
# Extract file path from tool output
# expected: "✅ ICS erstellt: <path>"
path = None
for token in txt.split():
if token.endswith(".ics"):
path = token
break
return txt, path
with gr.Blocks(title="Teich – Management Partner (Froschrettung)") as demo:
gr.Markdown("# 🐸 Teich – Management Partner (Froschrettung)\n**Dashboard • Kalender • Einsätze • Daten • Team • Budget**")
with gr.Tab("Dashboard"):
btn_refresh = gr.Button("🔄 Dashboard aktualisieren")
mig_md = gr.Markdown()
events_md = gr.Markdown()
todos_md = gr.Markdown()
vols_md = gr.Markdown()
payroll_md = gr.Markdown()
btn_refresh.click(fn=dashboard_snapshot, inputs=[], outputs=[mig_md, events_md, todos_md, vols_md, payroll_md])
demo.load(fn=dashboard_snapshot, inputs=[], outputs=[mig_md, events_md, todos_md, vols_md, payroll_md])
with gr.Tab("Kalender & Schichten"):
gr.Markdown("## Events\nEvents werden lokal in `data/events.json` gespeichert.")
events_table = gr.Dataframe(headers=["ID", "Start", "Ende", "Titel", "Ort"], interactive=False)
with gr.Row():
ev_title = gr.Textbox(label="Titel", placeholder="z.B. Zaunkontrolle / Nachtpatrouille")
ev_location = gr.Textbox(label="Ort", placeholder="z.B. Straße X / Teich Y")
with gr.Row():
ev_date = gr.Textbox(label="Datum (YYYY-MM-DD)", value=str(date.today()))
ev_start = gr.Textbox(label="Start (HH:MM)", value="20:00")
ev_end = gr.Textbox(label="Ende (HH:MM)", value="23:00")
with gr.Row():
btn_add_ev = gr.Button("➕ Event anlegen")
del_id = gr.Textbox(label="Event ID löschen", placeholder="E0001")
btn_del_ev = gr.Button("🗑️ Löschen")
btn_reload_ev = gr.Button("📋 Events neu laden")
btn_reload_ev.click(fn=list_events, inputs=[], outputs=[events_table])
demo.load(fn=list_events, inputs=[], outputs=[events_table])
btn_add_ev.click(fn=add_event, inputs=[ev_title, ev_date, ev_start, ev_end, ev_location], outputs=[events_table])
btn_del_ev.click(fn=delete_event, inputs=[del_id], outputs=[events_table])
gr.Markdown("## Wanderfenster (ICS)\nErzeugt einen Standard-ICS (Wettercheck, Zaunkontrolle, Patrouillenblock).")
with gr.Row():
region = gr.Textbox(label="Region", value="Deutschland")
year = gr.Number(label="Jahr", value=date.today().year, precision=0)
with gr.Row():
sm = gr.Number(label="Start Monat", value=2, precision=0)
sd = gr.Number(label="Start Tag", value=15, precision=0)
em = gr.Number(label="Ende Monat", value=4, precision=0)
ed = gr.Number(label="Ende Tag", value=30, precision=0)
btn_ics = gr.Button("📅 ICS generieren")
ics_status = gr.Markdown()
ics_file = gr.File(label="ICS Download")
btn_ics.click(fn=generate_migration_ics, inputs=[region, year, sm, sd, em, ed], outputs=[ics_status, ics_file])
with gr.Tab("Einsatz planen"):
gr.Markdown("## Einsatzplanung\nHier kannst du wie bisher einen Einsatz planen (später: Crew-Kickoff anbinden).")
user_prompt = gr.Textbox(label="Auftrag / Situation", lines=5, placeholder="z.B. Plane nächste Woche Froschrettung in Region X ...")
country = gr.Textbox(label="Land (GBIF Country Code)", value="DE")
region2 = gr.Textbox(label="Region (Freitext)", value="Deutschland")
species = gr.Dropdown(["Rana temporaria", "Bufo bufo", "Lissotriton vulgaris", "Triturus cristatus"], value="Rana temporaria", label="Zielart")
year_from = gr.Number(label="Jahr von", value=2020, precision=0)
year_to = gr.Number(label="Jahr bis", value=date.today().year, precision=0)
# Optional: Hier später Crew kickoff (run_manager) wieder einhängen.
gr.Markdown("➡️ (Nächster Schritt) Crew-Kickoff als Button: erzeugt Plan + Tasks + Kalender + Budget automatisch.")
with gr.Tab("Daten & Hotspots (GBIF/Roadkill)"):
gr.Markdown("## Amphibien-Sichtungen & Roadkill-Hotspots\nTop-Cluster inkl. Google-Maps Links.")
with gr.Row():
d_species = gr.Dropdown(["Rana temporaria", "Bufo bufo", "Lissotriton vulgaris", "Triturus cristatus"], value="Rana temporaria", label="Art")
d_country = gr.Textbox(label="Land", value="DE")
d_y1 = gr.Number(label="Jahr von", value=2020, precision=0)
d_y2 = gr.Number(label="Jahr bis", value=date.today().year, precision=0)
btn_data = gr.Button("📡 Daten laden (Hotspots)")
out_gbif = gr.Markdown()
out_road = gr.Markdown()
btn_data.click(fn=fetch_hotspots, inputs=[d_species, d_country, d_y1, d_y2], outputs=[out_gbif, out_road])
with gr.Tab("Team & Budget (450€)"):
gr.Markdown("## To-Dos / Volunteers / Payroll\nAlles wird lokal gespeichert (DATA_DIR).")
with gr.Accordion("To-Dos", open=True):
todo_title = gr.Textbox(label="Neue Aufgabe", placeholder="z.B. Materialliste erstellen")
todo_prio = gr.Dropdown(["L", "M", "H"], value="M", label="Priorität")
btn_todo = gr.Button("➕ To-Do anlegen")
todo_msg = gr.Markdown()
todo_list = gr.Markdown()
btn_todo.click(fn=add_todo, inputs=[todo_title, todo_prio], outputs=[todo_msg, todo_list])
demo.load(fn=lambda: organizer._run("list_todos", "{}"), inputs=[], outputs=[todo_list])
with gr.Accordion("Volunteers", open=False):
v_name = gr.Textbox(label="Name")
v_phone = gr.Textbox(label="Telefon")
v_avail = gr.Textbox(label="Verfügbarkeit", placeholder="z.B. Mo/Di ab 18 Uhr")
btn_vol = gr.Button("➕ Volunteer hinzufügen")
vol_msg = gr.Markdown()
vol_list = gr.Markdown()
btn_vol.click(fn=add_volunteer, inputs=[v_name, v_phone, v_avail], outputs=[vol_msg, vol_list])
demo.load(fn=lambda: organizer._run("list_volunteers", "{}"), inputs=[], outputs=[vol_list])
with gr.Accordion("450€ Plan & Payroll", open=True):
p_rate = gr.Number(label="Stundenlohn €", value=12.0, precision=2)
p_cap = gr.Number(label="Monatscap €", value=450.0, precision=2)
btn_plan = gr.Button("🧮 450€ Plan berechnen")
plan_md = gr.Markdown()
btn_plan.click(fn=payroll_plan, inputs=[p_rate, p_cap], outputs=[plan_md])
gr.Markdown("### Worker anlegen")
w_name = gr.Textbox(label="Name")
btn_add_w = gr.Button("➕ Worker hinzufügen")
w_msg = gr.Markdown()
rep_md = gr.Markdown()
btn_add_w.click(fn=payroll_add_worker, inputs=[w_name, p_rate, p_cap], outputs=[w_msg, rep_md])
demo.load(fn=lambda: payroll._run("report", "{}"), inputs=[], outputs=[rep_md])
gr.Markdown("### Stunden loggen")
l_name = gr.Textbox(label="Name (wie Worker)")
l_date = gr.Textbox(label="Datum", value=str(date.today()))
l_hours = gr.Number(label="Stunden", value=3.0, precision=2)
l_note = gr.Textbox(label="Notiz", placeholder="z.B. Nachtpatrouille")
btn_log = gr.Button("⏱️ log_hours")
log_msg = gr.Markdown()
btn_log.click(fn=payroll_log_hours, inputs=[l_name, l_date, l_hours, l_note], outputs=[log_msg, rep_md])
gr.Markdown("---\n**Hinweis:** In Hugging Face empfehle ich Persistent Storage aktivieren und `DATA_DIR=/data` setzen, dann bleiben Tasks/Events auch nach Restart erhalten.")
demo.queue()
demo.launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", 7860)))