Compare commits
2 Commits
831ec7e71c
...
4baece46bb
| Author | SHA1 | Date | |
|---|---|---|---|
| 4baece46bb | |||
| 5d28a34f02 |
@@ -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 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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
@@ -241,3 +241,12 @@ Investierte Zeit in dieser Session: 01:12
|
|||||||
Arbeitszusammenfassung:
|
Arbeitszusammenfassung:
|
||||||
Keine Zusammenfassung angegeben.
|
Keine Zusammenfassung angegeben.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 🤖 Status-Update (2026-04-08 10:21 Berlin Time)
|
||||||
|
```yaml
|
||||||
|
Investierte Zeit in dieser Session: 14:11
|
||||||
|
|
||||||
|
Arbeitszusammenfassung:
|
||||||
|
Keine Zusammenfassung angegeben.
|
||||||
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user