diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 29c9628..f519184 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -185,4 +185,12 @@ export const api = { resetFans: (gpuIndex: number) => post("/fans/reset", undefined, gpuIndex), setFanSpeed: (fanPct: number, gpuIndex: number, fan?: number | null) => post("/fans/speed", { fan_pct: fanPct, fan: fan ?? null }, gpuIndex), + + /** Report Bug (Gitea) */ + reportBugStatus: () => get<{ configured: boolean }>("/report-bug/status"), + reportBug: (title: string, body: string) => + post<{ ok: boolean; issue_number: number | null; issue_url: string | null }>( + "/report-bug", + { title, body }, + ), }; diff --git a/frontend/src/components/Monitor/StatusBar.tsx b/frontend/src/components/Monitor/StatusBar.tsx index e603d2f..d4d3e6c 100644 --- a/frontend/src/components/Monitor/StatusBar.tsx +++ b/frontend/src/components/Monitor/StatusBar.tsx @@ -7,10 +7,13 @@ import { ChevronDown, LogOut, User, + Bug, } from "lucide-react"; import type { GpuInfo, MonitoringSample } from "../../types.js"; import { fmt } from "../../utils/units.js"; import { useCurveStore } from "../../store/curveStore.js"; +import { api } from "../../api/client.js"; +import { ReportBugDialog } from "../common/ReportBugDialog.js"; import { useState, useRef, useEffect } from "react"; interface Props { @@ -43,8 +46,17 @@ export function StatusBar({ const { availableGpus, selectedGpuIndex, setSelectedGpuIndex } = useCurveStore(); const [isOpen, setIsOpen] = useState(false); + const [bugReportConfigured, setBugReportConfigured] = useState(false); + const [reportBugOpen, setReportBugOpen] = useState(false); const dropdownRef = useRef(null); + useEffect(() => { + api + .reportBugStatus() + .then((s) => setBugReportConfigured(s.configured)) + .catch(() => {}); + }, []); + useEffect(() => { function handleClickOutside(event: MouseEvent) { if ( @@ -169,8 +181,18 @@ export function StatusBar({ /> - {/* Right: user + connection status */} + {/* Right: report bug + user + connection status */}
+ {bugReportConfigured && ( + + )} {user && (
@@ -192,6 +214,9 @@ export function StatusBar({
+ {reportBugOpen && ( + setReportBugOpen(false)} /> + )} ); } diff --git a/frontend/src/components/common/ReportBugDialog.tsx b/frontend/src/components/common/ReportBugDialog.tsx new file mode 100644 index 0000000..067e1a5 --- /dev/null +++ b/frontend/src/components/common/ReportBugDialog.tsx @@ -0,0 +1,120 @@ +import { useState } from "react"; +import { Bug, Loader } from "lucide-react"; +import { toast } from "sonner"; +import { api } from "../../api/client.js"; + +interface Props { + onClose: () => void; +} + +export function ReportBugDialog({ onClose }: Props) { + const [title, setTitle] = useState(""); + const [body, setBody] = useState(""); + const [submitting, setSubmitting] = useState(false); + const [done, setDone] = useState<{ + number: number | null; + url: string | null; + } | null>(null); + + async function handleSubmit() { + if (!title.trim() || submitting) return; + setSubmitting(true); + try { + const res = await api.reportBug(title.trim(), body.trim()); + setDone({ number: res.issue_number, url: res.issue_url }); + toast.success( + res.issue_number != null ? `Issue #${res.issue_number} created` : "Issue created", + ); + } catch (err) { + toast.error(err instanceof Error ? err.message : "Failed to create issue"); + } finally { + setSubmitting(false); + } + } + + return ( +
{ + if (e.target === e.currentTarget) onClose(); + }} + > +
+ {done ? ( + <> +

+ Issue created +

+

+ {done.number != null ? `Issue #${done.number} ` : "The issue "} + was filed on Gitea with the auto-collected system context.{" "} + {done.url ? ( + + Open it → + + ) : ( + "." + )} +

+
+ +
+ + ) : ( + <> +

+ Report a bug +

+

+ Creates a Gitea issue. System info (version, GPU, active profile, + recent log) is attached automatically. +

+ setTitle(e.target.value)} + placeholder="Title / Subject (required)" + maxLength={200} + className="w-full bg-zinc-800 border border-zinc-700 rounded px-3 py-2 text-sm text-zinc-100 placeholder:text-zinc-500 focus:outline-none focus:border-pink-500 mb-3" + /> +