From 1d79f78426c00d252c4aa73de0f296d70eaa8773 Mon Sep 17 00:00:00 2001 From: Patrick Erichsen Date: Tue, 28 Apr 2026 22:30:43 -0700 Subject: [PATCH] feat: add plugin curation to management --- convex/packages.ts | 9 ++ src/routes/management.tsx | 186 ++++++++++++++++++++++++++++------- src/routes/plugins/$name.tsx | 31 +----- 3 files changed, 161 insertions(+), 65 deletions(-) diff --git a/convex/packages.ts b/convex/packages.ts index 9b2403af..865619d2 100644 --- a/convex/packages.ts +++ b/convex/packages.ts @@ -1112,9 +1112,18 @@ export const getByNameForStaff = query({ .query("packageBadges") .withIndex("by_package_kind", (q) => q.eq("packageId", pkg._id).eq("kind", "highlighted")) .unique(); + const latestRelease = pkg.latestReleaseId ? await ctx.db.get(pkg.latestReleaseId) : null; + const owner = toPublicPublisher( + await getOwnerPublisher(ctx, { + ownerPublisherId: pkg.ownerPublisherId, + ownerUserId: pkg.ownerUserId, + }), + ); return { package: pkg, + latestRelease: latestRelease && !latestRelease.softDeletedAt ? latestRelease : null, + owner, highlighted: highlighted ? { byUserId: highlighted.byUserId, diff --git a/src/routes/management.tsx b/src/routes/management.tsx index ce414da0..422bacce 100644 --- a/src/routes/management.tsx +++ b/src/routes/management.tsx @@ -1,4 +1,4 @@ -import { createFileRoute, Link } from "@tanstack/react-router"; +import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; import { useMutation, useQuery } from "convex/react"; import { useEffect, useState } from "react"; import { api } from "../../convex/_generated/api"; @@ -12,6 +12,8 @@ import { isSkillHighlighted, isSkillOfficial, } from "../lib/badges"; +import { familyLabel } from "../lib/packageLabels"; +import type { PublicPublisher } from "../lib/publicUser"; import { isAdmin, isModerator } from "../lib/roles"; import { useAuthStatus } from "../lib/useAuthStatus"; @@ -75,6 +77,13 @@ type SkillBySlugResult = { } | null; } | null; +type PluginByNameResult = { + package: Doc<"packages">; + latestRelease: Doc<"packageReleases"> | null; + owner: PublicPublisher | null; + highlighted: { byUserId: Id<"users">; at: number } | null; +} | null; + function resolveOwnerParam( handle: string | null | undefined, ownerId?: Id<"users"> | Id<"publishers">, @@ -99,6 +108,7 @@ function promptUnbanReason(label: string) { export const Route = createFileRoute("/management")({ validateSearch: (search) => ({ skill: typeof search.skill === "string" && search.skill.trim() ? search.skill : undefined, + plugin: typeof search.plugin === "string" && search.plugin.trim() ? search.plugin : undefined, }), component: Management, }); @@ -106,14 +116,20 @@ export const Route = createFileRoute("/management")({ function Management() { const { me } = useAuthStatus(); const search = Route.useSearch(); + const navigate = useNavigate(); const staff = isModerator(me); const admin = isAdmin(me); const selectedSlug = search.skill?.trim(); + const selectedPluginName = search.plugin?.trim(); const selectedSkill = useQuery( api.skills.getBySlugForStaff, staff && selectedSlug ? { slug: selectedSlug, auditLogLimit: SKILL_AUDIT_LOG_LIMIT } : "skip", ) as SkillBySlugResult | undefined; + const selectedPlugin = useQuery( + api.packages.getByNameForStaff, + staff && selectedPluginName ? { name: selectedPluginName } : "skip", + ) as PluginByNameResult | undefined; const selectedSkillId = selectedSkill?.skill?._id ?? null; const recentVersions = useQuery(api.skills.listRecentVersions, staff ? { limit: 20 } : "skip") as | RecentVersionEntry[] @@ -130,6 +146,7 @@ function Management() { const banUser = useMutation(api.users.banUser); const unbanUser = useMutation(api.users.unbanUser); const setBatch = useMutation(api.skills.setBatch); + const setPackageBatch = useMutation(api.packages.setBatch); const setSoftDeleted = useMutation(api.skills.setSoftDeleted); const hardDelete = useMutation(api.skills.hardDelete); const changeOwner = useMutation(api.skills.changeOwner); @@ -145,6 +162,7 @@ function Management() { const [reportSearchDebounced, setReportSearchDebounced] = useState(""); const [userSearch, setUserSearch] = useState(""); const [userSearchDebounced, setUserSearchDebounced] = useState(""); + const [pluginSearch, setPluginSearch] = useState(selectedPluginName ?? ""); const [skillOverrideNote, setSkillOverrideNote] = useState(""); const userQuery = userSearchDebounced.trim(); @@ -166,6 +184,10 @@ function Management() { setSkillOverrideNote(""); }, [selectedSkillId]); + useEffect(() => { + setPluginSearch(selectedPluginName ?? ""); + }, [selectedPluginName]); + useEffect(() => { const handle = setTimeout(() => setReportSearchDebounced(reportSearch), 250); return () => clearTimeout(handle); @@ -257,15 +279,22 @@ function Management() { .catch((error) => window.alert(formatMutationError(error))); }; + const managePlugin = () => { + const name = pluginSearch.trim(); + if (!name) return; + void navigate({ + to: "/management", + search: { skill: undefined, plugin: name }, + }); + }; + return (

Management console

Moderation, curation, and ownership tools.

-

- Reported skills -

+

Reported skills

Filter @@ -318,9 +347,7 @@ function Management() { ))}
) : ( -
- No report reasons yet. -
+
No report reasons yet.
)}
@@ -365,9 +392,7 @@ function Management() { -

- Skill tools -

+

Skill tools

{selectedSlug ? (
Managing "{selectedSlug}" ·{" "} @@ -417,16 +442,12 @@ function Management() { {skill.moderationFlags?.length ? (
{skill.moderationFlags.map((flag: string) => ( - - {flag} - + {flag} ))}
) : null}
-
- Manual overrides -
+
Manual overrides
Current override @@ -478,18 +499,14 @@ function Management() {
-
- Recent audit activity -
+
Recent audit activity
Window Last {SKILL_AUDIT_LOG_LIMIT} entries for this skill.
{auditLogs.length === 0 ? ( -
- No audit activity yet. -
+
No audit activity yet.
) : (
{auditLogs.map((entry) => { @@ -590,10 +607,7 @@ function Management() {
@@ -693,9 +707,115 @@ function Management() { -

- Duplicate candidates -

+

Plugin tools

+
+
+ Package + setPluginSearch(event.target.value)} + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + managePlugin(); + } + }} + /> +
+ +
+ {selectedPluginName ? ( +
+ Managing "{selectedPluginName}" ·{" "} + + Clear selection + +
+ ) : null} +
+ {!selectedPluginName ? ( +
Enter a plugin package name to open tooling here.
+ ) : selectedPlugin === undefined ? ( +
Loading plugin…
+ ) : !selectedPlugin?.package ? ( +
No plugin found for "{selectedPluginName}".
+ ) : ( + (() => { + const plugin = selectedPlugin.package; + const owner = selectedPlugin.owner; + const latestRelease = selectedPlugin.latestRelease; + const isHighlighted = Boolean(selectedPlugin.highlighted); + + return ( +
+
+ + {plugin.displayName} + +
+ {owner?.handle ? `@${owner.handle}` : "unknown owner"} ·{" "} + {familyLabel(plugin.family)} · v{latestRelease?.version ?? "—"} · updated{" "} + {formatTimestamp(plugin.updatedAt)} + {plugin.softDeletedAt ? " · hidden" : ""} + {isHighlighted ? " · highlighted" : ""} +
+
+ {plugin.channel} + {plugin.isOfficial ? official : null} + {plugin.executesCode ? executes code : null} + {plugin.runtimeId ? {plugin.runtimeId} : null} +
+
+
+ Package name + {plugin.name} +
+
+ Summary + {plugin.summary ?? "No summary provided."} +
+
+ Featured state + + {isHighlighted + ? `Highlighted ${formatTimestamp(selectedPlugin.highlighted?.at ?? 0)}` + : "Not highlighted"} + +
+
+
+
+ + +
+
+ ); + })() + )} +
+
+ + +

Duplicate candidates

{duplicateCandidates.length === 0 ? (
No duplicate candidates.
@@ -784,9 +904,7 @@ function Management() { -

- Recent pushes -

+

Recent pushes

{recentVersions.length === 0 ? (
No recent versions.
@@ -832,9 +950,7 @@ function Management() { {admin ? ( -

- Users -

+

Users

Filter diff --git a/src/routes/plugins/$name.tsx b/src/routes/plugins/$name.tsx index 13d932ce..da373c19 100644 --- a/src/routes/plugins/$name.tsx +++ b/src/routes/plugins/$name.tsx @@ -26,7 +26,6 @@ import { type PackageVersionDetail, } from "../../lib/packageApi"; import { familyLabel } from "../../lib/packageLabels"; -import { isModerator } from "../../lib/roles"; import { useAuthStatus } from "../../lib/useAuthStatus"; type PluginDetailRateLimitState = { @@ -185,14 +184,8 @@ function PluginDetailRoute() { const { name } = Route.useParams(); const { detail, version, readme, rateLimited } = Route.useLoaderData() as PluginDetailLoaderData; const pathname = useRouterState({ select: (state) => state.location.pathname }); - const { isAuthenticated, me } = useAuthStatus(); - const staff = isModerator(me); - const setPackageBatch = useMutation(api.packages.setBatch); + const { isAuthenticated } = useAuthStatus(); const requestPluginRescan = useMutation(api.packages.requestRescan); - const staffPackage = useQuery( - api.packages.getByNameForStaff, - staff && detail.package ? { name: detail.package.name } : "skip", - ) as { package: { _id: Id<"packages"> }; highlighted: { at: number } | null } | null | undefined; const rescanState = useQuery( api.packages.getOwnerRescanStateByName, isAuthenticated && detail.package ? { name: detail.package.name } : "skip", @@ -249,21 +242,6 @@ function PluginDetailRoute() { : pkg.family === "bundle-plugin" ? `openclaw bundles install clawhub:${pkg.name}` : `openclaw skills install ${pkg.name}`; - const isHighlighted = Boolean(staffPackage?.highlighted); - - const toggleHighlighted = async () => { - const packageId = staffPackage?.package._id; - if (!packageId) return; - try { - await setPackageBatch({ - packageId, - batch: isHighlighted ? undefined : "highlighted", - }); - toast.success(isHighlighted ? "Plugin unhighlighted." : "Plugin highlighted."); - } catch (error) { - toast.error(getUserFacingConvexError(error, "Could not update plugin highlight.")); - } - }; const capabilities = latestRelease?.capabilities ?? pkg.capabilities; const compatibility = latestRelease?.compatibility ?? pkg.compatibility; @@ -326,13 +304,6 @@ function PluginDetailRoute() { Download blocked
) : null} - {staffPackage ? ( -
- -
- ) : null}

{pkg.summary ?? "No summary provided."}