2 Commits

Author SHA1 Message Date
4baece46bb [32788f42] Keine Zusammenfassung angegeben.
Keine Zusammenfassung angegeben.
2026-04-08 08:21:54 +00:00
5d28a34f02 Docs: Aktualisierung der Dokumentation für Task [32788f42] 2026-04-08 08:21:53 +00:00
5 changed files with 111 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 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,

View File

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

View File

@@ -41,9 +41,24 @@ function App() {
const [reminderTaskId, setReminderTaskId] = useState<string | null>(null);
const [reminderProgress, setReminderProgress] = useState<string>('');
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 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() {
<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
</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>
{/* Main Tabs */}

View File

@@ -241,3 +241,12 @@ Investierte Zeit in dieser Session: 01:12
Arbeitszusammenfassung:
Keine Zusammenfassung angegeben.
```
## 🤖 Status-Update (2026-04-08 10:21 Berlin Time)
```yaml
Investierte Zeit in dieser Session: 14:11
Arbeitszusammenfassung:
Keine Zusammenfassung angegeben.
```