2025-09-11 16:01:32 +02:00
|
|
|
|
import os
|
|
|
|
|
|
import shutil
|
2025-10-07 13:02:29 +02:00
|
|
|
|
import csv
|
|
|
|
|
|
from pathlib import Path
|
2025-09-17 16:35:11 +02:00
|
|
|
|
from fastapi import FastAPI, UploadFile, File, Request, Form
|
2025-10-02 12:20:11 +02:00
|
|
|
|
from fastapi.responses import HTMLResponse
|
2025-09-11 16:01:32 +02:00
|
|
|
|
from fastapi.templating import Jinja2Templates
|
|
|
|
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
|
|
|
|
|
|
|
|
|
from cost_calculator import allowed_file, analyze_pdf, get_rate_black, get_rate_color, UPLOAD_FOLDER
|
2025-09-17 16:35:11 +02:00
|
|
|
|
from mailer import send_order_sync
|
|
|
|
|
|
|
|
|
|
|
|
from dotenv import load_dotenv
|
|
|
|
|
|
load_dotenv()
|
|
|
|
|
|
|
2025-10-07 13:02:29 +02:00
|
|
|
|
# Get server hostname from environment
|
|
|
|
|
|
SERVER_HOSTNAME = os.environ.get("SERVER_HOSTNAME", "einszwovier.local")
|
|
|
|
|
|
BOOKSTACK_PORT = os.environ.get("BOOKSTACK_PORT", "6875")
|
|
|
|
|
|
OPENWEBUI_PORT = os.environ.get("OPENWEBUI_PORT", "8080")
|
|
|
|
|
|
PORTAINER_PORT = os.environ.get("PORTAINER_PORT", "9000")
|
|
|
|
|
|
|
|
|
|
|
|
# Courses CSV path
|
|
|
|
|
|
COURSES_CSV = Path("data/courses.csv")
|
|
|
|
|
|
|
2025-09-11 16:01:32 +02:00
|
|
|
|
app = FastAPI()
|
|
|
|
|
|
templates = Jinja2Templates(directory="templates")
|
|
|
|
|
|
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
|
|
|
|
|
|
2025-10-02 12:20:11 +02:00
|
|
|
|
import asyncio
|
|
|
|
|
|
|
|
|
|
|
|
presence_state = {"present": False}
|
|
|
|
|
|
|
|
|
|
|
|
async def update_presence_periodically():
|
|
|
|
|
|
while True:
|
|
|
|
|
|
try:
|
|
|
|
|
|
# Here you can implement logic to update presence automatically
|
|
|
|
|
|
# For example, check some condition, a file, or even keep it False
|
|
|
|
|
|
print("Updating presence state…")
|
|
|
|
|
|
# Example: flip presence every 5 mins (just for demo)
|
|
|
|
|
|
# presence_state["present"] = not presence_state["present"]
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print("Error updating presence:", e)
|
|
|
|
|
|
|
|
|
|
|
|
await asyncio.sleep(300) # 5 minutes
|
|
|
|
|
|
|
|
|
|
|
|
@app.on_event("startup")
|
|
|
|
|
|
async def startup_event():
|
|
|
|
|
|
asyncio.create_task(update_presence_periodically())
|
2025-09-11 16:01:32 +02:00
|
|
|
|
|
2025-10-02 12:20:11 +02:00
|
|
|
|
|
|
|
|
|
|
# ---- Presence State ----
|
|
|
|
|
|
presence_state = {"present": False}
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/presence")
|
|
|
|
|
|
def get_presence():
|
|
|
|
|
|
"""Return current presence state"""
|
|
|
|
|
|
return presence_state
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/presence")
|
|
|
|
|
|
def set_presence(present: bool = Form(...)):
|
|
|
|
|
|
"""Set presence state explicitly"""
|
|
|
|
|
|
presence_state["present"] = present
|
|
|
|
|
|
return {"status": "ok", "present": presence_state["present"]}
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/presence/toggle")
|
|
|
|
|
|
def toggle_presence():
|
|
|
|
|
|
"""Toggle presence state"""
|
|
|
|
|
|
presence_state["present"] = not presence_state["present"]
|
|
|
|
|
|
return {"status": "ok", "present": presence_state["present"]}
|
|
|
|
|
|
|
|
|
|
|
|
# ---- Existing Endpoints ----
|
2025-09-11 16:01:32 +02:00
|
|
|
|
@app.get("/", response_class=HTMLResponse)
|
|
|
|
|
|
async def welcome(request: Request):
|
2025-10-02 12:20:11 +02:00
|
|
|
|
return templates.TemplateResponse(
|
|
|
|
|
|
"landing.html",
|
|
|
|
|
|
{
|
|
|
|
|
|
"request": request,
|
|
|
|
|
|
"studio_open": presence_state["present"],
|
2025-10-07 13:02:29 +02:00
|
|
|
|
"opening_hours": "Di-Do 11:00–16:00",
|
|
|
|
|
|
"server_hostname": SERVER_HOSTNAME,
|
|
|
|
|
|
"bookstack_port": BOOKSTACK_PORT,
|
|
|
|
|
|
"openwebui_port": OPENWEBUI_PORT,
|
|
|
|
|
|
"portainer_port": PORTAINER_PORT,
|
2025-10-02 12:20:11 +02:00
|
|
|
|
},
|
|
|
|
|
|
)
|
2025-09-11 16:01:32 +02:00
|
|
|
|
|
|
|
|
|
|
@app.get("/about", response_class=HTMLResponse)
|
|
|
|
|
|
async def about(request: Request):
|
2025-10-07 13:02:29 +02:00
|
|
|
|
# Load courses from CSV
|
|
|
|
|
|
courses = []
|
|
|
|
|
|
if COURSES_CSV.exists():
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(COURSES_CSV, 'r', encoding='utf-8') as f:
|
|
|
|
|
|
reader = csv.DictReader(f)
|
|
|
|
|
|
courses = list(reader)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"Error loading courses: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
return templates.TemplateResponse("about.html", {
|
|
|
|
|
|
"request": request,
|
|
|
|
|
|
"courses": courses
|
|
|
|
|
|
})
|
2025-09-11 16:01:32 +02:00
|
|
|
|
|
|
|
|
|
|
@app.get("/cost", response_class=HTMLResponse)
|
|
|
|
|
|
async def cost_dashboard(request: Request):
|
|
|
|
|
|
return templates.TemplateResponse(
|
|
|
|
|
|
"cost-calculator.html",
|
|
|
|
|
|
{
|
|
|
|
|
|
"request": request,
|
|
|
|
|
|
"rate_black": get_rate_black(),
|
2025-09-17 16:35:11 +02:00
|
|
|
|
"rate_color": get_rate_color(),
|
|
|
|
|
|
},
|
2025-09-11 16:01:32 +02:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/upload")
|
|
|
|
|
|
async def upload_file(request: Request, file: UploadFile = File(...)):
|
|
|
|
|
|
if not allowed_file(file.filename):
|
|
|
|
|
|
return templates.TemplateResponse(
|
|
|
|
|
|
"cost-calculator.html",
|
|
|
|
|
|
{"request": request, "error": "Unsupported file type. Only PDF allowed."},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
path = os.path.join(UPLOAD_FOLDER, file.filename)
|
|
|
|
|
|
with open(path, "wb") as buffer:
|
|
|
|
|
|
shutil.copyfileobj(file.file, buffer)
|
|
|
|
|
|
|
|
|
|
|
|
result = analyze_pdf(path)
|
|
|
|
|
|
|
|
|
|
|
|
return templates.TemplateResponse(
|
|
|
|
|
|
"result.html",
|
|
|
|
|
|
{
|
|
|
|
|
|
"request": request,
|
|
|
|
|
|
"result": result,
|
|
|
|
|
|
"rate_black": get_rate_black(),
|
2025-09-17 16:35:11 +02:00
|
|
|
|
"rate_color": get_rate_color(),
|
2025-09-11 16:01:32 +02:00
|
|
|
|
},
|
|
|
|
|
|
)
|
2025-09-17 16:35:11 +02:00
|
|
|
|
|
|
|
|
|
|
@app.post("/send-order")
|
|
|
|
|
|
def send_order_endpoint(
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
filename: str = Form(...),
|
2025-10-02 12:20:11 +02:00
|
|
|
|
name: str = Form(...),
|
2025-09-17 16:35:11 +02:00
|
|
|
|
comment: str = Form(""),
|
|
|
|
|
|
):
|
|
|
|
|
|
path = os.path.join(UPLOAD_FOLDER, filename)
|
|
|
|
|
|
if not os.path.exists(path):
|
|
|
|
|
|
return templates.TemplateResponse(
|
|
|
|
|
|
"cost-calculator.html",
|
2025-10-02 12:20:11 +02:00
|
|
|
|
{"request": request, "error": "Datei nicht gefunden. Bitte erneut hochladen."},
|
2025-09-17 16:35:11 +02:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
analysis = analyze_pdf(path)
|
|
|
|
|
|
|
2025-10-07 13:02:29 +02:00
|
|
|
|
# Get Matrix room ID from environment
|
|
|
|
|
|
matrix_room = os.environ.get("MATRIX_ROOM", "!eFWbWEnYsgeIKqyfjw:einszwovier.local")
|
|
|
|
|
|
|
2025-09-17 16:35:11 +02:00
|
|
|
|
try:
|
2025-10-02 12:20:11 +02:00
|
|
|
|
send_order_sync(
|
|
|
|
|
|
pdf_path=path,
|
|
|
|
|
|
analysis=analysis,
|
2025-10-07 13:02:29 +02:00
|
|
|
|
room_id=matrix_room,
|
2025-10-02 12:20:11 +02:00
|
|
|
|
name=name,
|
|
|
|
|
|
comment=comment,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-09-17 16:35:11 +02:00
|
|
|
|
return templates.TemplateResponse(
|
|
|
|
|
|
"result.html",
|
|
|
|
|
|
{
|
|
|
|
|
|
"request": request,
|
|
|
|
|
|
"result": analysis,
|
|
|
|
|
|
"rate_black": get_rate_black(),
|
|
|
|
|
|
"rate_color": get_rate_color(),
|
2025-10-02 12:20:11 +02:00
|
|
|
|
"success": "✅ Dein Auftrag wurde erfolgreich gesendet!",
|
|
|
|
|
|
"name": name,
|
|
|
|
|
|
"comment": comment,
|
2025-09-17 16:35:11 +02:00
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
return templates.TemplateResponse(
|
|
|
|
|
|
"result.html",
|
|
|
|
|
|
{
|
|
|
|
|
|
"request": request,
|
|
|
|
|
|
"result": analysis,
|
|
|
|
|
|
"rate_black": get_rate_black(),
|
|
|
|
|
|
"rate_color": get_rate_color(),
|
2025-10-02 12:20:11 +02:00
|
|
|
|
"error": f"Fehler beim Senden des Auftrags: {e}",
|
|
|
|
|
|
"name": name,
|
|
|
|
|
|
"comment": comment,
|
2025-09-17 16:35:11 +02:00
|
|
|
|
},
|
2025-10-02 12:20:11 +02:00
|
|
|
|
)
|