diff --git a/.dev_session/SESSION_INFO b/.dev_session/SESSION_INFO index 24ca2c552..9fcaf7037 100644 --- a/.dev_session/SESSION_INFO +++ b/.dev_session/SESSION_INFO @@ -1 +1 @@ -{"task_id": "32788f42-8544-80e1-a13a-c26114cf9b34", "token": "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", "readme_path": "readme.md", "session_start_time": "2026-04-07T18:10:43.355316"} \ No newline at end of file +{"task_id": "32788f42-8544-80e1-a13a-c26114cf9b34", "token": "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", "readme_path": "readme.md", "session_start_time": "2026-04-08T08:21:53.169017"} \ No newline at end of file diff --git a/fotograf-de-scraper/backend/main.py b/fotograf-de-scraper/backend/main.py index cdc69964b..62bff1bfd 100644 --- a/fotograf-de-scraper/backend/main.py +++ b/fotograf-de-scraper/backend/main.py @@ -1,6 +1,7 @@ import os import logging import datetime +from zoneinfo import ZoneInfo import base64 import re import pandas as pd @@ -36,6 +37,34 @@ logging.basicConfig( ) logger = logging.getLogger("fotograf-scraper") +# --- Global State for Last Generated File --- +# Simple and robust: persists as long as the container runs. +LATEST_FILE_STATE = { + "path": None, + "display_name": None, + "timestamp": None, + "type": None # 'pdf' or 'csv' +} + +def update_latest_file(file_path: str, display_name: str, file_type: str): + try: + # Copy file to a stable location inside the container (/app/data is persistent) + # but for simplicity, /tmp is also fine for "just the last one" + stable_path = os.path.join("/tmp", f"latest_result_{file_type}.{file_type}") + shutil.copy2(file_path, stable_path) + + now_berlin = datetime.datetime.now(ZoneInfo("Europe/Berlin")) + LATEST_FILE_STATE["path"] = stable_path + LATEST_FILE_STATE["display_name"] = display_name + LATEST_FILE_STATE["timestamp"] = now_berlin.strftime("%H:%M Uhr") + LATEST_FILE_STATE["type"] = file_type + logger.info(f"Updated latest file state: {display_name}") + except Exception as e: + logger.error(f"Failed to update latest file state: {e}") + +def get_berlin_now_str(): + return datetime.datetime.now(ZoneInfo("Europe/Berlin")).strftime("%d.%m.%Y %H:%M Uhr") + def format_job_date(date_str: str) -> str: import re import datetime @@ -171,7 +200,7 @@ def generate_pdf_from_csv(csv_path: str, institution: str, date_info: str, list_ env = Environment(loader=FileSystemLoader(template_dir)) template = env.get_template("school_list.html") - current_time = datetime.datetime.now().strftime("%d.%m.%Y %H:%M Uhr") + current_time = get_berlin_now_str() logo_base64 = get_logo_base64() render_context = { @@ -191,6 +220,7 @@ def generate_pdf_from_csv(csv_path: str, institution: str, date_info: str, list_ html_out = template.render(render_context) logger.info(f"Writing PDF to: {output_path}") HTML(string=html_out).write_pdf(output_path) + update_latest_file(output_path, f"Teilnehmerliste {institution}", "pdf") def generate_appointment_overview_pdf(raw_events: list, job_name: str, event_type_name: str, output_path: str): from collections import defaultdict @@ -315,7 +345,7 @@ def generate_appointment_overview_pdf(raw_events: list, job_name: str, event_typ env = Environment(loader=FileSystemLoader(template_dir)) template = env.get_template("appointment_list.html") - current_time = datetime.datetime.now().strftime("%d.%m.%Y %H:%M Uhr") + current_time = get_berlin_now_str() logo_base64 = get_logo_base64() render_context = { @@ -328,6 +358,7 @@ def generate_appointment_overview_pdf(raw_events: list, job_name: str, event_typ html_out = template.render(render_context) HTML(string=html_out).write_pdf(output_path) + update_latest_file(output_path, f"Terminübersicht {job_name}", "pdf") # --- Selenium Scraper Functions --- @@ -790,6 +821,9 @@ async def generate_qr_cards( # Cleanup uploaded file os.remove(base_pdf_path) + # Update latest file tracking + update_latest_file(output_path, f"QR-Karten ({event_type_name or 'Calendly'})", "pdf") + return FileResponse(path=output_path, filename=output_name, media_type="application/pdf") except Exception as e: @@ -853,6 +887,29 @@ async def generate_appointment_list(job_id: str, event_type_name: str, db: Sessi logger.error(f"Error generating appointment overview pdf: {e}") raise HTTPException(status_code=500, detail=str(e)) +@app.get("/api/jobs/latest-file") +async def get_latest_file_info(): + if not LATEST_FILE_STATE["path"] or not os.path.exists(LATEST_FILE_STATE["path"]): + return {"has_file": False} + return { + "has_file": True, + "display_name": LATEST_FILE_STATE["display_name"], + "timestamp": LATEST_FILE_STATE["timestamp"], + "type": LATEST_FILE_STATE["type"] + } + +@app.get("/api/jobs/download-latest") +async def download_latest_file(): + if not LATEST_FILE_STATE["path"] or not os.path.exists(LATEST_FILE_STATE["path"]): + raise HTTPException(status_code=404, detail="Keine Datei gefunden.") + + filename = f"Letzte_Datei_{LATEST_FILE_STATE['type']}.{LATEST_FILE_STATE['type']}" + return FileResponse( + path=LATEST_FILE_STATE["path"], + filename=filename, + media_type="application/pdf" if LATEST_FILE_STATE["type"] == "pdf" else "text/csv" + ) + @app.get("/health") async def health_check(): return {"status": "ok"} @@ -968,7 +1025,11 @@ async def download_task_csv(task_id: str): df = pd.DataFrame(result) temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".csv") df.to_csv(temp_file.name, index=False, encoding='utf-8-sig') - return FileResponse(path=temp_file.name, filename=f"Supermailer_Liste_{task_id[:8]}.csv", media_type="text/csv") + + filename = f"Supermailer_Liste_{task_id[:8]}.csv" + update_latest_file(temp_file.name, "Supermailer Liste", "csv") + + return FileResponse(path=temp_file.name, filename=filename, media_type="text/csv") except Exception as e: logger.error(f"Export error: {e}") raise HTTPException(status_code=500, detail="CSV Export fehlgeschlagen.") @@ -1054,7 +1115,7 @@ async def generate_pdf(job_id: str, account_type: str, db: Session = Depends(get if job_record and job_record.date: final_date_info = format_job_date(job_record.date) else: - final_date_info = datetime.datetime.now().strftime("%d.%m.%Y") + final_date_info = datetime.datetime.now(ZoneInfo("Europe/Berlin")).strftime("%d.%m.%Y") generate_pdf_from_csv( csv_path=csv_file, diff --git a/fotograf-de-scraper/backend/requirements.txt b/fotograf-de-scraper/backend/requirements.txt index 787359c19..f3893db76 100644 --- a/fotograf-de-scraper/backend/requirements.txt +++ b/fotograf-de-scraper/backend/requirements.txt @@ -11,3 +11,4 @@ sqlalchemy==2.0.31 requests==2.31.0 reportlab==4.0.9 PyPDF2==3.0.1 +tzdata diff --git a/fotograf-de-scraper/frontend/src/App.tsx b/fotograf-de-scraper/frontend/src/App.tsx index d18ecdf51..bd329d26c 100644 --- a/fotograf-de-scraper/frontend/src/App.tsx +++ b/fotograf-de-scraper/frontend/src/App.tsx @@ -41,9 +41,24 @@ function App() { const [reminderTaskId, setReminderTaskId] = useState(null); const [reminderProgress, setReminderProgress] = useState(''); const [isReminderRunning, setIsReminderRunning] = useState(false); + const [latestFile, setLatestFile] = useState(null); const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://192.168.178.6:8002'; + const fetchLatestFile = async () => { + try { + const response = await fetch(`${API_BASE_URL}/api/jobs/latest-file`); + if (response.ok) { + const data = await response.json(); + if (data.has_file) { + setLatestFile(data); + } + } + } catch (err) { + console.error("Failed to fetch latest file info"); + } + }; + const fetchJobs = async (account: AccountType, forceRefresh = false) => { setIsLoading(true); setError(null); @@ -67,6 +82,7 @@ function App() { if (jobsCache[activeTab] === null) { fetchJobs(activeTab, false); } + fetchLatestFile(); }, [activeTab]); const handleRefresh = () => fetchJobs(activeTab, true); @@ -179,6 +195,7 @@ function App() { // da wir bei window.open nicht wissen, wann der Download fertig ist. setTimeout(() => { setProcessingJobId(null); + fetchLatestFile(); }, 3000); } catch (err: any) { @@ -227,6 +244,7 @@ function App() { setTimeout(() => { document.body.removeChild(a); window.URL.revokeObjectURL(url); + fetchLatestFile(); }, 100); } catch (err: any) { setError(err.message); @@ -244,6 +262,7 @@ function App() { setTimeout(() => { setIsListGenerating(false); + fetchLatestFile(); }, 3000); } catch (err: any) { setError(`Listen-Fehler (${job.name}): ${err.message}`); @@ -272,6 +291,7 @@ function App() { const handleDownloadReminderCsv = async (taskId: string) => { try { window.open(`${API_BASE_URL}/api/tasks/${taskId}/download-csv`, '_blank'); + setTimeout(fetchLatestFile, 2000); } catch (err: any) { setError("Download fehlgeschlagen."); } @@ -296,6 +316,21 @@ function App() { Zum Dashboard + + {latestFile && ( +
+ Letzte Datei: + + + {latestFile.display_name} ({latestFile.timestamp}) + +
+ )} {/* Main Tabs */}