Files
foodlinkk-command-center/cockpit/app/routes/marketing_api.py
T
Aissa 5d60d33db1 Platform bundle: marketing publish, IT ops, packaging, agents mesh.
Volledige Foodlinkk Command Center uitbreiding met social automatisering,
reclamefolder filters, Proxmox monitoring en documentatie.
2026-06-09 00:41:27 +00:00

120 lines
3.8 KiB
Python

from __future__ import annotations
import json
from pathlib import Path
from uuid import uuid4
from fastapi import APIRouter, BackgroundTasks, File, HTTPException, UploadFile
from pydantic import BaseModel, Field
from app.db import fetch_all, fetch_one
from app.services.social_publish import PLATFORMS, get_configured_channels, run_publish_job
router = APIRouter(prefix="/api/marketing", tags=["marketing-api"])
BASE_DIR = Path(__file__).resolve().parent.parent.parent
UPLOAD_DIR = BASE_DIR / "static" / "uploads" / "marketing"
def _serialize_row(row: dict | None) -> dict | None:
if not row:
return None
out = dict(row)
for key, value in list(out.items()):
if hasattr(value, "isoformat"):
out[key] = value.isoformat()
return out
def _serialize_rows(rows: list[dict]) -> list[dict]:
return [_serialize_row(row) for row in rows]
class PublishRequest(BaseModel):
text: str = Field(..., min_length=1, max_length=5000)
image_url: str | None = None
media_ids: list[int] = Field(default_factory=list)
channels: list[str] = Field(default_factory=list)
@router.post("/upload")
async def upload_marketing_media(file: UploadFile = File(...)) -> dict:
data = await file.read()
if not data:
raise HTTPException(status_code=400, detail="empty file")
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
ext = Path(file.filename or "upload.bin").suffix or ".bin"
filename = f"{uuid4().hex}{ext.lower()}"
path = UPLOAD_DIR / filename
path.write_bytes(data)
media_url = f"/static/uploads/marketing/{filename}"
row = fetch_one(
"""
INSERT INTO marketing_media (filename, original_name, file_path, media_url, mime_type, size_bytes, created_at)
VALUES (%s, %s, %s, %s, %s, %s, NOW())
RETURNING id
""",
(
filename,
file.filename or filename,
str(path),
media_url,
file.content_type or "application/octet-stream",
len(data),
),
)
return {"media_id": row["id"], "url": media_url}
@router.post("/publish", status_code=202)
def create_publish_job(body: PublishRequest, background_tasks: BackgroundTasks) -> dict:
channels = [c.strip().lower() for c in body.channels if c.strip()]
invalid = [c for c in channels if c not in PLATFORMS]
if invalid:
raise HTTPException(status_code=400, detail=f"Unsupported channels: {', '.join(invalid)}")
if not channels:
channels = list(PLATFORMS)
row = fetch_one(
"""
INSERT INTO social_publish_jobs (text, image_url, media_ids, channels, status, created_at, updated_at)
VALUES (%s, %s, %s::jsonb, %s::jsonb, %s, NOW(), NOW())
RETURNING id
""",
(
body.text,
body.image_url,
json.dumps(body.media_ids),
json.dumps(channels),
"queued",
),
)
job_id = int(row["id"])
background_tasks.add_task(run_publish_job, job_id, body.text, channels, body.image_url, body.media_ids)
return {"job_id": job_id, "status": "queued", "channels": channels}
@router.get("/publish/{job_id}")
def get_publish_job(job_id: int) -> dict:
row = fetch_one("SELECT * FROM social_publish_jobs WHERE id = %s", (job_id,))
if not row:
raise HTTPException(status_code=404, detail="Job not found")
return {"job": _serialize_row(row)}
@router.get("/publish/history")
def list_publish_history(limit: int = 50) -> dict:
safe_limit = max(1, min(limit, 200))
rows = fetch_all(
"SELECT * FROM social_publish_jobs ORDER BY created_at DESC LIMIT %s",
(safe_limit,),
)
return {"items": _serialize_rows(rows), "count": len(rows)}
@router.get("/channels")
def list_channels() -> dict:
items = get_configured_channels()
return {"items": items, "platforms": list(PLATFORMS)}