feat(tasks): offene Git-Issues als Aufgaben uebernehmen

Ein Klick im Git-Panel macht aus den offenen Issues Aufgaben - mit Titel,
Labels und einem Link zurueck ins Git-System.

Grundlage ist der zuletzt geholte Snapshot. Hier wird bewusst nichts
nachgeladen: ein Klick soll nicht ungefragt fremde APIs ansprechen und
womoeglich in ein Rate-Limit laufen. Wer den neuesten Stand will, gleicht
vorher ab; ohne Snapshot sagt der Server das statt still nichts zu tun.

Die Issue-Nummer bleibt an der Aufgabe und ist je Projekt eindeutig. Ein
zweiter Klick legt deshalb keine Dubletten an, auch wenn die Aufgabe
inzwischen verschoben oder umbenannt wurde. NULL gilt in Postgres als
verschieden, von Hand angelegte Aufgaben stoeren die Eindeutigkeit also
nicht.

Nachgewiesen an vercel/swr: 15 offene Issues uebernommen, beim zweiten
Aufruf 0 angelegt und 15 uebersprungen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-26 11:36:44 +02:00
co-authored by Claude Opus 5
parent 3bad4ace10
commit 75460c8cf4
4 changed files with 174 additions and 1 deletions
+53 -1
View File
@@ -12,6 +12,7 @@ import {
GitMerge,
GitPullRequest,
HardDrive,
ListPlus,
Loader2,
Lock,
Package,
@@ -48,8 +49,36 @@ export function GitPanel({ project }: { project: ProjectDetail }) {
const router = useRouter();
const [tab, setTab] = useState<TabKey>("pulls");
const [syncing, setSyncing] = useState(false);
const [importing, setImporting] = useState(false);
const [message, setMessage] = useState<{ ok: boolean; text: string } | null>(null);
/** Offene Issues als Aufgaben anlegen – ohne erneuten Abruf beim Provider. */
async function importIssues() {
setImporting(true);
setMessage(null);
try {
const response = await fetch(`/api/projects/${project.id}/tasks/from-issues`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: "{}",
});
const body = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(body.error ?? "Uebernahme fehlgeschlagen.");
setMessage({
ok: true,
text:
body.created === 0
? `Nichts Neues – alle ${body.skipped} Issues sind bereits als Aufgabe vorhanden.`
: `${body.created} Aufgabe(n) angelegt${body.skipped ? `, ${body.skipped} uebersprungen` : ""}.`,
});
router.refresh();
} catch (caught) {
setMessage({ ok: false, text: caught instanceof Error ? caught.message : String(caught) });
} finally {
setImporting(false);
}
}
const snapshot = project.snapshot;
const branches = asArray<BranchInfo>(snapshot?.branches);
const tags = asArray<TagInfo>(snapshot?.tags);
@@ -329,7 +358,30 @@ export function GitPanel({ project }: { project: ProjectDetail }) {
/>
) : null}
{tab === "issues" ? (
<TicketList tickets={issues} empty="Keine offenen Issues." icon={CircleDot} />
<>
{issues.length > 0 ? (
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-white/6 bg-white/2 px-4 py-2">
<p className="text-[11px] text-slate-500">
Aus offenen Issues Aufgaben machen. Bereits uebernommene werden
uebersprungen.
</p>
<button
type="button"
className="btn btn-ghost px-2.5 py-1 text-[11px]"
onClick={importIssues}
disabled={importing}
>
{importing ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
<ListPlus className="h-3 w-3" />
)}
Als Aufgaben uebernehmen
</button>
</div>
) : null}
<TicketList tickets={issues} empty="Keine offenen Issues." icon={CircleDot} />
</>
) : null}
{tab === "branches" ? <BranchList branches={branches} /> : null}
{tab === "tags" ? <TagList tags={tags} /> : null}