pelagia-portal/App/components/layout/report-issue-button.tsx
Hardik 8b6d4e8ea6 feat(automation): issue-to-deploy pipeline — Report Issue button, Claude watcher, tag-triggered deploy
- Report Issue button in portal header files a Forgejo issue (portal + claude-queue labels)
- Windows scheduled watcher runs headless Claude Code on queued issues and opens a PR
- .forgejo/workflows/deploy.yml deploys v* release tags via the pms1 host runner (pm2 restart ppms)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 16:39:43 +05:30

130 lines
4.5 KiB
TypeScript

"use client";
import { useState, useTransition } from "react";
import { usePathname } from "next/navigation";
import { Bug } from "lucide-react";
import { AdminDialog } from "@/components/ui/admin-dialog";
import { reportIssue } from "./report-issue-actions";
const PRIORITIES = [
"P0 — Critical (broken / blocking)",
"P1 — High",
"P2 — Medium",
"P3 — Low",
] as const;
export function ReportIssueButton() {
const pathname = usePathname();
const [open, setOpen] = useState(false);
const [pending, startTransition] = useTransition();
const [error, setError] = useState<string | null>(null);
const [filedIssue, setFiledIssue] = useState<{ number: number; url: string } | null>(null);
function close() {
setOpen(false);
setError(null);
setFiledIssue(null);
}
function onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setError(null);
const formData = new FormData(e.currentTarget);
formData.set("page", pathname);
startTransition(async () => {
const result = await reportIssue(formData);
if ("error" in result) {
setError(result.error);
} else {
setFiledIssue({ number: result.issueNumber, url: result.issueUrl });
}
});
}
return (
<>
<button
onClick={() => setOpen(true)}
className="flex items-center gap-1.5 rounded-lg p-2 text-neutral-500 hover:bg-neutral-100 hover:text-neutral-700 transition-colors"
title="Report an issue"
>
<Bug className="h-4 w-4" />
</button>
<AdminDialog title="Report an issue" open={open} onClose={close}>
{filedIssue ? (
<div className="space-y-4">
<p className="text-sm text-neutral-700">
Thanks issue <span className="font-semibold">#{filedIssue.number}</span> has been
filed and queued for an automated fix. You&apos;ll see the change in an upcoming
release.
</p>
<button
onClick={close}
className="w-full rounded-lg bg-neutral-900 px-4 py-2 text-sm font-medium text-white hover:bg-neutral-700 transition-colors"
>
Done
</button>
</div>
) : (
<form onSubmit={onSubmit} className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-neutral-700" htmlFor="ri-title">
Title
</label>
<input
id="ri-title"
name="title"
required
minLength={5}
maxLength={150}
placeholder="Short summary of the problem"
className="w-full rounded-lg border border-neutral-300 px-3 py-2 text-sm focus:border-neutral-500 focus:outline-none"
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-neutral-700" htmlFor="ri-description">
Description
</label>
<textarea
id="ri-description"
name="description"
required
minLength={10}
maxLength={5000}
rows={5}
placeholder="What happened? What did you expect? Steps to reproduce if it's a bug."
className="w-full rounded-lg border border-neutral-300 px-3 py-2 text-sm focus:border-neutral-500 focus:outline-none"
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-neutral-700" htmlFor="ri-priority">
Priority
</label>
<select
id="ri-priority"
name="priority"
defaultValue={PRIORITIES[2]}
className="w-full rounded-lg border border-neutral-300 px-3 py-2 text-sm focus:border-neutral-500 focus:outline-none"
>
{PRIORITIES.map((p) => (
<option key={p} value={p}>
{p}
</option>
))}
</select>
</div>
{error && <p className="text-sm text-red-600">{error}</p>}
<button
type="submit"
disabled={pending}
className="w-full rounded-lg bg-neutral-900 px-4 py-2 text-sm font-medium text-white hover:bg-neutral-700 transition-colors disabled:opacity-50"
>
{pending ? "Filing issue…" : "Report issue"}
</button>
</form>
)}
</AdminDialog>
</>
);
}