diff --git a/apps/gittensory-ui/src/components/site/app-panels/maintainer-panel.tsx b/apps/gittensory-ui/src/components/site/app-panels/maintainer-panel.tsx index 58b77d8fc9..f40d6b9ea5 100644 --- a/apps/gittensory-ui/src/components/site/app-panels/maintainer-panel.tsx +++ b/apps/gittensory-ui/src/components/site/app-panels/maintainer-panel.tsx @@ -1,6 +1,14 @@ -import { useState } from "react"; -import { AnimatePresence, motion } from "motion/react"; -import { Eye, EyeOff } from "lucide-react"; +import { useEffect, useMemo, useState } from "react"; +import { + AlertTriangle, + Bot, + CheckCircle2, + CircleSlash, + Play, + RefreshCw, + ShieldCheck, + UserCheck, +} from "lucide-react"; import { DiffBlock, @@ -10,7 +18,17 @@ import { } from "@/components/site/control-primitives"; import { StatCard } from "@/components/site/primitives"; import { StateBoundary } from "@/components/site/state-views"; +import { apiFetch } from "@/lib/api/request"; +import { getApiOrigin } from "@/lib/api/origin"; import { useApiResource } from "@/lib/api/use-api-resource"; +import { + PREVIEW_SCENARIOS, + buildSettingsPreviewRequest, + extractPreviewRepoOptions, + splitRepoFullName, + type PreviewFormState, + type PreviewScenarioId, +} from "@/lib/maintainer-settings-preview"; import { cn } from "@/lib/utils"; const BUCKET_TONE: Record = { @@ -43,6 +61,47 @@ type MaintainerDashboard = { settingsPreview: { removed: string[]; added: string[] }; }; +type SettingsPreviewResponse = { + repoFullName: string; + generatedAt: string; + installation: { + installationId: number; + status: "healthy" | "needs_attention" | "broken"; + missingPermissions: string[]; + missingEvents: string[]; + permissionRemediation: Array<{ + permission: string; + requiredAccess: string; + currentAccess: string; + ok: boolean; + action: string; + }>; + } | null; + sample: { + authorLogin: string; + authorType: string; + authorAssociation: string; + minerStatus: string; + title: string; + labels: string[]; + linkedIssues: number[]; + }; + decision: { + willComment: boolean; + willLabel: boolean; + willCheckRun: boolean; + skipped: boolean; + skipReason: string | null; + actions: Array<"skip" | "comment" | "label" | "check_run" | "none">; + summary: string; + }; + previewComment: string | null; + appliedLabel: string | null; + checkRun: { willCreate: boolean; title: string; detailLevel: string } | null; + warnings: string[]; + summary: string; +}; + export function MaintainerPanel() { const dashboard = useApiResource( "/v1/app/maintainer-dashboard", @@ -184,90 +243,357 @@ export function MaintainerPanel() { - + ) : null} ); } -type Side = "public" | "private"; +function SurfacePreview({ + reviewability, +}: { + reviewability: MaintainerDashboard["reviewability"]; +}) { + const repoOptions = useMemo(() => extractPreviewRepoOptions(reviewability), [reviewability]); + const [form, setForm] = useState({ + repoFullName: repoOptions[0] ?? "", + scenarioId: "confirmed-miner", + title: "Sample pull request", + labels: "bug", + linkedIssues: "7", + body: "", + }); + const [preview, setPreview] = useState(null); + const [error, setError] = useState(null); + const [busy, setBusy] = useState(false); + const repoParts = splitRepoFullName(form.repoFullName); + + useEffect(() => { + if (!form.repoFullName && repoOptions[0]) { + setForm((current) => ({ ...current, repoFullName: repoOptions[0] })); + } + }, [form.repoFullName, repoOptions]); + + async function runPreview(nextForm = form) { + const target = splitRepoFullName(nextForm.repoFullName); + if (!target) { + setPreview(null); + setError("Enter a repository as owner/repo."); + return; + } + setBusy(true); + setError(null); + const result = await apiFetch( + `${getApiOrigin().replace(/\/$/, "")}/v1/repos/${encodeURIComponent(target.owner)}/${encodeURIComponent(target.repo)}/settings-preview`, + { + method: "POST", + label: "Settings preview", + credentials: "include", + headers: { Accept: "application/json", "Content-Type": "application/json" }, + body: JSON.stringify(buildSettingsPreviewRequest(nextForm)), + }, + ); + setBusy(false); + if (result.ok) { + setPreview(result.data); + return; + } + setPreview(null); + setError(result.message); + } + + function updateScenario(scenarioId: PreviewScenarioId) { + const next = { + ...form, + scenarioId, + title: + scenarioId === "bot-author" + ? "Automated dependency update" + : scenarioId === "maintainer-author" + ? "Maintainer follow-up" + : "Sample pull request", + }; + setForm(next); + setPreview(null); + setError(null); + } -function SurfacePreview() { - const [side, setSide] = useState("public"); return ( -
+
-

Surface preview

+

+ Public-safe preview simulator +

- Flip between what shows on GitHub publicly and what only you see in private MCP / API - context. + Dry-run the GitHub App decision for a sample PR against the same policy engine used in + production.

-
- {[ - { - id: "public" as const, - label: "Public on GitHub", - icon: , - }, - { - id: "private" as const, - label: "Private to you", - icon: , - }, - ].map((option) => ( - - ))} +
+ + {preview ? (preview.decision.skipped ? "skip" : "ready") : "preview"} + +
- - - {side === "public" ? ( -
-              {[
-                "Gittensory checked public metadata for this PR.",
-                "",
-                "- No private scorer weights are posted publicly.",
-                "- Maintainer-only context stays in MCP/API surfaces.",
-              ].join("\n")}
-            
- ) : ( -
-              {[
-                "Private maintainer context",
-                "",
-                "- Decision-pack blockers",
-                "- Reviewability score",
-                "- Cached contributor outcome history",
-              ].join("\n")}
-            
- )} -
-
+
+
+ + +
+
+ Scenario +
+
+ {PREVIEW_SCENARIOS.map((scenario) => ( + + ))} +
+
+ +
+ + +
+ + + +