From 08d85a1576f10fd79b27aeb6eb8fca8feb7c2eb8 Mon Sep 17 00:00:00 2001 From: Power-Maverick Date: Tue, 25 Aug 2026 17:08:35 -0400 Subject: [PATCH] feat: implement tool maturity model with verification requests and admin workflows - Add API routes for admin verification requests and tool verification submissions. - Implement logic for handling verification request lifecycle including claiming, deciding, and sending notifications. - Create a VerifiedCheckmark component for UI representation of verified tools. - Document testing procedures for each phase of the maturity model. - Introduce utility functions for managing active verification requests and sending emails. --- .gitignore | 1 + .../admin/verification-requests/page.tsx | 334 ++++++++++++++++++ app/(authenticated)/dashboard/page.tsx | 134 ++++++- app/(public)/tools/[id]/page.tsx | 10 +- app/(public)/tools/page.tsx | 13 +- app/api/admin/verification-requests/route.ts | 258 ++++++++++++++ app/api/dashboard/route.ts | 36 ++ app/api/odata/$metadata/route.ts | 4 + app/api/odata/tools/route.ts | 58 +-- .../tools/[id]/request-verification/route.ts | 102 ++++++ app/api/tools/[id]/route.ts | 7 +- app/api/tools/route.ts | 23 +- app/api/update-tool/route.ts | 16 +- components/VerifiedCheckmark.tsx | 13 + docs/MATURITY_MODEL_PHASE_1_TESTING.md | 75 ++++ docs/MATURITY_MODEL_PHASE_2_TESTING.md | 31 ++ docs/MATURITY_MODEL_PHASE_3_TESTING.md | 40 +++ docs/MATURITY_MODEL_PHASE_4_TESTING.md | 24 ++ lib/maturity.ts | 53 +++ lib/mock-tools.ts | 1 + lib/resend.ts | 106 +++++- 21 files changed, 1281 insertions(+), 58 deletions(-) create mode 100644 app/(authenticated)/admin/verification-requests/page.tsx create mode 100644 app/api/admin/verification-requests/route.ts create mode 100644 app/api/tools/[id]/request-verification/route.ts create mode 100644 components/VerifiedCheckmark.tsx create mode 100644 docs/MATURITY_MODEL_PHASE_1_TESTING.md create mode 100644 docs/MATURITY_MODEL_PHASE_2_TESTING.md create mode 100644 docs/MATURITY_MODEL_PHASE_3_TESTING.md create mode 100644 docs/MATURITY_MODEL_PHASE_4_TESTING.md create mode 100644 lib/maturity.ts diff --git a/.gitignore b/.gitignore index 4875f3b..7bddbca 100644 --- a/.gitignore +++ b/.gitignore @@ -144,3 +144,4 @@ vite.config.ts.timestamp-* # Next JS Typescript Env file # see https://nextjs.org/docs/app/api-reference/config/typescript#next-envdts for additional reference next-env.d.ts +.playwright-mcp/page-2026-08-25T17-28-50-591Z.yml diff --git a/app/(authenticated)/admin/verification-requests/page.tsx b/app/(authenticated)/admin/verification-requests/page.tsx new file mode 100644 index 0000000..4e76d0c --- /dev/null +++ b/app/(authenticated)/admin/verification-requests/page.tsx @@ -0,0 +1,334 @@ +"use client"; + +import { Container } from "@/components/Container"; +import Link from "next/link"; +import { useEffect, useState } from "react"; + +interface Criterion { + key: string; + category: string; + label: string; + reviewer_guidance: string; + required: boolean; + sort_order: number; +} + +interface VerificationRequest { + id: string; + tool_id: string; + developer_id: string; + status: "queued" | "in_review"; + submitted_at: string; + reviewed_by: string | null; + tool: { id: string; name: string; version: string | null; repository: string | null } | null; + usageMetrics: { mau: number; downloads: number; qualifyingReviews: number }; + usageMetricsMet: number; +} + +interface ReviewResult { + passed?: boolean; + waived: boolean; + comment: string; +} + +export default function VerificationRequestsPage() { + const [requests, setRequests] = useState([]); + const [criteria, setCriteria] = useState([]); + const [selected, setSelected] = useState(null); + const [results, setResults] = useState>({}); + const [token] = useState(() => (typeof window === "undefined" ? "" : sessionStorage.getItem("supabaseToken") || "")); + const [loading, setLoading] = useState(true); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + const [notice, setNotice] = useState<{ kind: "success" | "warning"; message: string } | null>(null); + + useEffect(() => { + async function loadQueue() { + try { + setError(null); + const response = await fetch("/api/admin/verification-requests", { headers: { Authorization: `Bearer ${token}` } }); + const data = await response.json(); + if (!response.ok) throw new Error(data.error || "Failed to load verification queue"); + setRequests(data.requests || []); + setCriteria(data.criteria || []); + } catch (loadError) { + setError(loadError instanceof Error ? loadError.message : "Failed to load verification queue"); + } finally { + setLoading(false); + } + } + void loadQueue(); + }, [token]); + + async function openRequest(item: VerificationRequest) { + try { + setError(null); + const response = await fetch("/api/admin/verification-requests", { + method: "POST", + headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, + body: JSON.stringify({ action: "claim", requestId: item.id }), + }); + const data = await response.json(); + if (!response.ok) throw new Error(data.error || "Failed to open verification request"); + + const claimed = { ...item, status: "in_review" as const, reviewed_by: data.request.reviewed_by }; + setRequests((current) => current.map((request) => (request.id === item.id ? claimed : request))); + setSelected(claimed); + setResults(Object.fromEntries(criteria.map((criterion) => [criterion.key, { passed: undefined, waived: false, comment: "" }]))); + } catch (openError) { + setError(openError instanceof Error ? openError.message : "Failed to open verification request"); + } + } + + const allEvaluated = criteria.length === 13 && criteria.every((criterion) => typeof results[criterion.key]?.passed === "boolean"); + const failedRequired = criteria.filter((criterion) => criterion.required && results[criterion.key]?.passed === false && !(criterion.key === "usage_thresholds" && results[criterion.key]?.waived)); + const decision = allEvaluated ? (failedRequired.length === 0 ? "approve" : "reject") : null; + + async function decide(requestDecision: "approve" | "reject") { + if (!selected || requestDecision !== decision) return; + setSubmitting(true); + setError(null); + setNotice(null); + try { + const response = await fetch("/api/admin/verification-requests", { + method: "POST", + headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, + body: JSON.stringify({ + action: "decide", + requestId: selected.id, + decision: requestDecision, + results: criteria.map((criterion) => ({ + criterionKey: criterion.key, + passed: results[criterion.key].passed, + waived: results[criterion.key].waived, + comment: results[criterion.key].comment, + })), + }), + }); + const data = await response.json(); + if (!response.ok) throw new Error(data.error || "Failed to complete review"); + + const decisionLabel = requestDecision === "approve" ? "approved" : "rejected"; + setNotice( + data.notificationSent + ? { kind: "success", message: `Verification ${decisionLabel}. The decision email was sent to the developer.` } + : { kind: "warning", message: data.notificationError || `Verification ${decisionLabel}, but the decision email could not be sent.` }, + ); + + setRequests((current) => current.filter((request) => request.id !== selected.id)); + setSelected(null); + setResults({}); + } catch (decisionError) { + setError(decisionError instanceof Error ? decisionError.message : "Failed to complete review"); + } finally { + setSubmitting(false); + } + } + + function updateResult(key: string, patch: Partial) { + setResults((current) => ({ ...current, [key]: { ...current[key], ...patch } })); + } + + return ( +
+ +
+
+
+

Administration

+

Verification Queue

+

Oldest submissions appear first. Opening a queued request claims it.

+
+ + Dashboard + +
+ + {error && ( +
+ {error} +
+ )} + {notice && ( +
+ {notice.message} +
+ )} + + {loading ? ( +

Loading verification queue...

+ ) : ( +
+ + +
+ {!selected ? ( +
Open the oldest queued request to begin its review.
+ ) : ( +
+
+
+
+

{selected.tool?.name}

+

Version {selected.tool?.version || "unknown"}

+
+ {selected.tool?.repository && ( + + Repository + + )} +
+
+ +
+ {criteria.map((criterion) => { + const result = results[criterion.key] || { waived: false, comment: "" }; + return ( +
+ + {criterion.sort_order}. {criterion.label}{" "} + {criterion.required ? "Required" : "Optional"} + +

{criterion.reviewer_guidance}

+ + {criterion.key === "bug_health" && ( +
+

+ Pass: fewer than 5 open bugs and every first maintainer response within 10 days. +

+

+ Flag: 5+ open bugs or any response later than 10 days; use reviewer judgment. +

+

+ Blocker: any bug with no maintainer response after 30 days. +

+

Measure issue creation to first maintainer response, not time to close.

+
+ )} + + {criterion.key === "usage_thresholds" && ( +
+ = 10} threshold="≥ 10" /> + = 50} threshold="≥ 50" /> + = 1} + threshold="≥ 1" + /> +

{selected.usageMetricsMet}/3 metrics met; 2 are required.

+
+ )} + +
+ + + {criterion.key === "usage_thresholds" && result.passed === false && ( + + )} +
+