[32788f42] Keine Zusammenfassung angegeben.
Keine Zusammenfassung angegeben.
This commit is contained in:
@@ -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"}
|
||||
@@ -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,
|
||||
|
||||
@@ -11,3 +11,4 @@ sqlalchemy==2.0.31
|
||||
requests==2.31.0
|
||||
reportlab==4.0.9
|
||||
PyPDF2==3.0.1
|
||||
tzdata
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user