[32788f42] Keine Zusammenfassung angegeben.

Keine Zusammenfassung angegeben.
This commit is contained in:
2026-04-08 08:21:54 +00:00
parent 5d28a34f02
commit 4baece46bb
4 changed files with 102 additions and 5 deletions

View File

@@ -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"} {"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"}

View File

@@ -1,6 +1,7 @@
import os import os
import logging import logging
import datetime import datetime
from zoneinfo import ZoneInfo
import base64 import base64
import re import re
import pandas as pd import pandas as pd
@@ -36,6 +37,34 @@ logging.basicConfig(
) )
logger = logging.getLogger("fotograf-scraper") 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: def format_job_date(date_str: str) -> str:
import re import re
import datetime 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)) env = Environment(loader=FileSystemLoader(template_dir))
template = env.get_template("school_list.html") 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() logo_base64 = get_logo_base64()
render_context = { 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) html_out = template.render(render_context)
logger.info(f"Writing PDF to: {output_path}") logger.info(f"Writing PDF to: {output_path}")
HTML(string=html_out).write_pdf(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): def generate_appointment_overview_pdf(raw_events: list, job_name: str, event_type_name: str, output_path: str):
from collections import defaultdict 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)) env = Environment(loader=FileSystemLoader(template_dir))
template = env.get_template("appointment_list.html") 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() logo_base64 = get_logo_base64()
render_context = { 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_out = template.render(render_context)
HTML(string=html_out).write_pdf(output_path) HTML(string=html_out).write_pdf(output_path)
update_latest_file(output_path, f"Terminübersicht {job_name}", "pdf")
# --- Selenium Scraper Functions --- # --- Selenium Scraper Functions ---
@@ -790,6 +821,9 @@ async def generate_qr_cards(
# Cleanup uploaded file # Cleanup uploaded file
os.remove(base_pdf_path) 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") return FileResponse(path=output_path, filename=output_name, media_type="application/pdf")
except Exception as e: 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}") logger.error(f"Error generating appointment overview pdf: {e}")
raise HTTPException(status_code=500, detail=str(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") @app.get("/health")
async def health_check(): async def health_check():
return {"status": "ok"} return {"status": "ok"}
@@ -968,7 +1025,11 @@ async def download_task_csv(task_id: str):
df = pd.DataFrame(result) df = pd.DataFrame(result)
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".csv") temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".csv")
df.to_csv(temp_file.name, index=False, encoding='utf-8-sig') 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: except Exception as e:
logger.error(f"Export error: {e}") logger.error(f"Export error: {e}")
raise HTTPException(status_code=500, detail="CSV Export fehlgeschlagen.") 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: if job_record and job_record.date:
final_date_info = format_job_date(job_record.date) final_date_info = format_job_date(job_record.date)
else: 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( generate_pdf_from_csv(
csv_path=csv_file, csv_path=csv_file,

View File

@@ -11,3 +11,4 @@ sqlalchemy==2.0.31
requests==2.31.0 requests==2.31.0
reportlab==4.0.9 reportlab==4.0.9
PyPDF2==3.0.1 PyPDF2==3.0.1
tzdata

View File

@@ -41,9 +41,24 @@ function App() {
const [reminderTaskId, setReminderTaskId] = useState<string | null>(null); const [reminderTaskId, setReminderTaskId] = useState<string | null>(null);
const [reminderProgress, setReminderProgress] = useState<string>(''); const [reminderProgress, setReminderProgress] = useState<string>('');
const [isReminderRunning, setIsReminderRunning] = useState(false); const [isReminderRunning, setIsReminderRunning] = useState(false);
const [latestFile, setLatestFile] = useState<any>(null);
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://192.168.178.6:8002'; 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) => { const fetchJobs = async (account: AccountType, forceRefresh = false) => {
setIsLoading(true); setIsLoading(true);
setError(null); setError(null);
@@ -67,6 +82,7 @@ function App() {
if (jobsCache[activeTab] === null) { if (jobsCache[activeTab] === null) {
fetchJobs(activeTab, false); fetchJobs(activeTab, false);
} }
fetchLatestFile();
}, [activeTab]); }, [activeTab]);
const handleRefresh = () => fetchJobs(activeTab, true); const handleRefresh = () => fetchJobs(activeTab, true);
@@ -179,6 +195,7 @@ function App() {
// da wir bei window.open nicht wissen, wann der Download fertig ist. // da wir bei window.open nicht wissen, wann der Download fertig ist.
setTimeout(() => { setTimeout(() => {
setProcessingJobId(null); setProcessingJobId(null);
fetchLatestFile();
}, 3000); }, 3000);
} catch (err: any) { } catch (err: any) {
@@ -227,6 +244,7 @@ function App() {
setTimeout(() => { setTimeout(() => {
document.body.removeChild(a); document.body.removeChild(a);
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
fetchLatestFile();
}, 100); }, 100);
} catch (err: any) { } catch (err: any) {
setError(err.message); setError(err.message);
@@ -244,6 +262,7 @@ function App() {
setTimeout(() => { setTimeout(() => {
setIsListGenerating(false); setIsListGenerating(false);
fetchLatestFile();
}, 3000); }, 3000);
} catch (err: any) { } catch (err: any) {
setError(`Listen-Fehler (${job.name}): ${err.message}`); setError(`Listen-Fehler (${job.name}): ${err.message}`);
@@ -272,6 +291,7 @@ function App() {
const handleDownloadReminderCsv = async (taskId: string) => { const handleDownloadReminderCsv = async (taskId: string) => {
try { try {
window.open(`${API_BASE_URL}/api/tasks/${taskId}/download-csv`, '_blank'); window.open(`${API_BASE_URL}/api/tasks/${taskId}/download-csv`, '_blank');
setTimeout(fetchLatestFile, 2000);
} catch (err: any) { } catch (err: any) {
setError("Download fehlgeschlagen."); setError("Download fehlgeschlagen.");
} }
@@ -296,6 +316,21 @@ function App() {
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" /></svg> <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" /></svg>
Zum Dashboard Zum Dashboard
</a> </a>
{latestFile && (
<div className="hidden lg:flex items-center gap-2">
<span className="text-xs text-gray-400">Letzte Datei:</span>
<a
href={`${API_BASE_URL}/api/jobs/download-latest`}
target="_blank"
rel="noopener noreferrer"
className="text-xs font-bold text-emerald-600 hover:text-emerald-700 underline flex items-center gap-1"
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a2 2 0 002 2h12a2 2 0 002-2v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
{latestFile.display_name} ({latestFile.timestamp})
</a>
</div>
)}
</div> </div>
{/* Main Tabs */} {/* Main Tabs */}