From eab212ea8da4523814a01dd922cbf7ee3163b115 Mon Sep 17 00:00:00 2001 From: Rephl3x Date: Fri, 23 Jan 2026 19:24:45 +1300 Subject: [PATCH] Add feedback form and webhook --- backend/app/config.py | 5 ++ backend/app/main.py | 2 + backend/app/routers/feedback.py | 38 +++++++++ frontend/app/feedback/page.tsx | 120 +++++++++++++++++++++++++++++ frontend/app/globals.css | 19 +++++ frontend/app/how-it-works/page.tsx | 35 +++++---- frontend/app/ui/HeaderActions.tsx | 1 + 7 files changed, 204 insertions(+), 16 deletions(-) create mode 100644 backend/app/routers/feedback.py create mode 100644 frontend/app/feedback/page.tsx diff --git a/backend/app/config.py b/backend/app/config.py index c501fc8..902d658 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -101,5 +101,10 @@ class Settings(BaseSettings): default=None, validation_alias=AliasChoices("QBIT_PASSWORD", "QBITTORRENT_PASSWORD") ) + discord_webhook_url: Optional[str] = Field( + default="https://discord.com/api/webhooks/1464141924775629033/O_rvCAmIKowR04tyAN54IuMPcQFEiT-ustU3udDaMTlF62PmoI6w4-52H3ZQcjgHQOgt", + validation_alias=AliasChoices("DISCORD_WEBHOOK_URL"), + ) + settings = Settings() diff --git a/backend/app/main.py b/backend/app/main.py index 8c33255..4d9a876 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -17,6 +17,7 @@ from .routers.admin import router as admin_router from .routers.images import router as images_router from .routers.branding import router as branding_router from .routers.status import router as status_router +from .routers.feedback import router as feedback_router from .services.jellyfin_sync import run_daily_jellyfin_sync from .logging_config import configure_logging from .runtime import get_runtime_settings @@ -54,3 +55,4 @@ app.include_router(admin_router) app.include_router(images_router) app.include_router(branding_router) app.include_router(status_router) +app.include_router(feedback_router) diff --git a/backend/app/routers/feedback.py b/backend/app/routers/feedback.py new file mode 100644 index 0000000..d383724 --- /dev/null +++ b/backend/app/routers/feedback.py @@ -0,0 +1,38 @@ +from typing import Any, Dict +import httpx +from fastapi import APIRouter, Depends, HTTPException + +from ..auth import get_current_user +from ..runtime import get_runtime_settings + +router = APIRouter(prefix="/feedback", tags=["feedback"], dependencies=[Depends(get_current_user)]) + + +@router.post("") +async def send_feedback(payload: Dict[str, Any], user: Dict[str, str] = Depends(get_current_user)) -> dict: + runtime = get_runtime_settings() + webhook_url = runtime.discord_webhook_url + if not webhook_url: + raise HTTPException(status_code=400, detail="Discord webhook not configured") + + feedback_type = str(payload.get("type") or "").strip().lower() + if feedback_type not in {"bug", "feature"}: + raise HTTPException(status_code=400, detail="Invalid feedback type") + + message = str(payload.get("message") or "").strip() + if not message: + raise HTTPException(status_code=400, detail="Message is required") + if len(message) > 2000: + raise HTTPException(status_code=400, detail="Message is too long") + + username = user.get("username") or "unknown" + content = f"**{feedback_type.title()}** from **{username}**\n{message}" + + try: + async with httpx.AsyncClient(timeout=10.0) as client: + response = await client.post(webhook_url, json={"content": content}) + response.raise_for_status() + except httpx.HTTPStatusError as exc: + raise HTTPException(status_code=502, detail=str(exc)) from exc + + return {"status": "ok"} diff --git a/frontend/app/feedback/page.tsx b/frontend/app/feedback/page.tsx new file mode 100644 index 0000000..ddb3936 --- /dev/null +++ b/frontend/app/feedback/page.tsx @@ -0,0 +1,120 @@ +'use client' + +import { useEffect, useState } from 'react' +import { useRouter } from 'next/navigation' +import { authFetch, clearToken, getApiBase, getToken } from '../lib/auth' + +type Profile = { + username?: string +} + +export default function FeedbackPage() { + const router = useRouter() + const [profile, setProfile] = useState(null) + const [category, setCategory] = useState('bug') + const [message, setMessage] = useState('') + const [status, setStatus] = useState(null) + const [submitting, setSubmitting] = useState(false) + + useEffect(() => { + if (!getToken()) { + router.push('/login') + return + } + const load = async () => { + try { + const baseUrl = getApiBase() + const response = await authFetch(`${baseUrl}/auth/me`) + if (!response.ok) { + clearToken() + router.push('/login') + return + } + const data = await response.json() + setProfile({ username: data?.username }) + } catch (error) { + console.error(error) + } + } + void load() + }, [router]) + + const submit = async (event: React.FormEvent) => { + event.preventDefault() + setStatus(null) + if (!message.trim()) { + setStatus('Please write a short message before sending.') + return + } + setSubmitting(true) + try { + const baseUrl = getApiBase() + const response = await authFetch(`${baseUrl}/feedback`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + type: category, + message: message.trim(), + }), + }) + if (!response.ok) { + if (response.status === 401) { + clearToken() + router.push('/login') + return + } + const text = await response.text() + throw new Error(text || `Request failed: ${response.status}`) + } + setMessage('') + setStatus('Thanks! Your message has been sent.') + } catch (error) { + console.error(error) + setStatus('That did not send. Please try again.') + } finally { + setSubmitting(false) + } + } + + return ( +
+
+

Send feedback

+

Help us improve Magent

+

+ Found a problem or have an idea? Send it here and we will see it right away. +

+
+ +
+ + + + + + + +