[34588f42] Feat: Versandzeit-Steuerung für Freigabe-Anfragen hinzugefügt

- Backend unterstützt nun zeitgesteuerten Versand (scheduled_time) via BackgroundTasks.
- Frontend um ein Zeitauswahl-Feld erweitert.
This commit is contained in:
2026-04-17 20:21:44 +00:00
parent 929d92afeb
commit 1f5805e64c
2 changed files with 74 additions and 5 deletions

View File

@@ -1,4 +1,4 @@
from fastapi import APIRouter, Depends, HTTPException, Request from fastapi import APIRouter, Depends, HTTPException, Request, BackgroundTasks
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from database import get_db, DiscountCode from database import get_db, DiscountCode
@@ -6,6 +6,9 @@ import datetime
import logging import logging
from gmail_service import GmailService from gmail_service import GmailService
import re import re
import time
import asyncio
from typing import List, Dict, Optional
router = APIRouter(prefix="/api/publish-request", tags=["publish-request"]) router = APIRouter(prefix="/api/publish-request", tags=["publish-request"])
logger = logging.getLogger("publish-request") logger = logging.getLogger("publish-request")
@@ -13,6 +16,54 @@ logger = logging.getLogger("publish-request")
class CodesUpload(BaseModel): class CodesUpload(BaseModel):
codes: str # comma separated codes: str # comma separated
class SendReleaseRequest(BaseModel):
emails: List[Dict[str, str]]
scheduled_time: Optional[str] = None # e.g. "10:00"
async def delayed_send(emails: List[Dict[str, str]], scheduled_time: str, db: Session):
try:
# Calculate delay
now = datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=2))) # Berlin Time Approx
target_h, target_m = map(int, scheduled_time.split(":"))
target_time = now.replace(hour=target_h, minute=target_m, second=0, microsecond=0)
if target_time < now:
target_time += datetime.timedelta(days=1)
delay_seconds = (target_time - now).total_seconds()
logger.info(f"Scheduling {len(emails)} emails for {scheduled_time} (in {delay_seconds} seconds)")
await asyncio.sleep(delay_seconds)
service = GmailService(db)
success_count = 0
for email_data in emails:
if service.send_email(email_data["to"], email_data["subject"], email_data["body"]):
success_count += 1
await asyncio.sleep(1) # Rate limiting
logger.info(f"Scheduled send complete: {success_count}/{len(emails)} success.")
except Exception as e:
logger.exception("Error in delayed_send background task")
@router.post("/send")
async def send_requests(data: SendReleaseRequest, background_tasks: BackgroundTasks, db: Session = Depends(get_db)):
if data.scheduled_time:
background_tasks.add_task(delayed_send, data.emails, data.scheduled_time, db)
return {"status": "scheduled", "message": f"Versand für {data.scheduled_time} geplant."}
# Immediate send
service = GmailService(db)
success = 0
failed = []
for email_data in data.emails:
if service.send_email(email_data["to"], email_data["subject"], email_data["body"]):
success += 1
else:
failed.append(email_data["to"])
return {"status": "success", "success": success, "failed": failed}
@router.get("/stats") @router.get("/stats")
def get_stats(db: Session = Depends(get_db)): def get_stats(db: Session = Depends(get_db)):
total = db.query(DiscountCode).count() total = db.query(DiscountCode).count()

View File

@@ -61,6 +61,7 @@ function App() {
const [uploadMessage, setUploadMessage] = useState(""); const [uploadMessage, setUploadMessage] = useState("");
const [isSendingRelease, setIsSendingRelease] = useState(false); const [isSendingRelease, setIsSendingRelease] = useState(false);
const [releaseMessage, setReleaseMessage] = useState(""); const [releaseMessage, setReleaseMessage] = useState("");
const [scheduledTime, setScheduledTime] = useState(""); // New state
const fetchReleaseStats = async () => { const fetchReleaseStats = async () => {
try { try {
@@ -136,10 +137,13 @@ function App() {
setReleaseMessage(`Sende ${emailsToSend.length} Mails...`); setReleaseMessage(`Sende ${emailsToSend.length} Mails...`);
try { try {
const response = await fetch(`${API_BASE_URL}/api/gmail/send-bulk`, { const response = await fetch(`${API_BASE_URL}/api/publish-request/send`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ emails: emailsToSend }) body: JSON.stringify({
emails: emailsToSend,
scheduled_time: scheduledTime || null
})
}); });
const data = await response.json(); const data = await response.json();
if (response.ok) { if (response.ok) {
@@ -553,10 +557,13 @@ function App() {
}); });
try { try {
const response = await fetch(`${API_BASE_URL}/api/gmail/send-bulk`, { const response = await fetch(`${API_BASE_URL}/api/publish-request/send`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ emails: emailsToSend }) body: JSON.stringify({
emails: emailsToSend,
scheduled_time: scheduledTime || null
})
}); });
if (response.ok) { if (response.ok) {
@@ -1021,6 +1028,17 @@ function App() {
onChange={(e) => setReleaseEmails(e.target.value)} onChange={(e) => setReleaseEmails(e.target.value)}
className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:ring-2 focus:ring-indigo-500 mb-2 h-20" className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:ring-2 focus:ring-indigo-500 mb-2 h-20"
/> />
<div className="flex items-center gap-2 mb-3 bg-white p-2 rounded-lg border border-gray-100">
<span className="text-[10px] font-bold text-gray-400 uppercase">Versandzeit (Optional)</span>
<input
type="time"
value={scheduledTime}
onChange={(e) => setScheduledTime(e.target.value)}
className="text-xs border border-gray-200 rounded px-2 py-1 focus:ring-1 focus:ring-indigo-500"
/>
<span className="text-[10px] text-gray-400 italic">Leer = sofort</span>
</div>
<button <button
onClick={handleSendRelease} onClick={handleSendRelease}
disabled={isSendingRelease || !releaseEmails.trim() || !reminderResult || !isGmailAuthenticated} disabled={isSendingRelease || !releaseEmails.trim() || !reminderResult || !isGmailAuthenticated}