feat(marketplace): UI fuer Plugin-Marketplace — Modul 5/16 des UI-Backlogs
Check Cross-Plugin Imports / check (push) Has been cancelled

ERSTES Modul ueber die Phase-Q-Plugin-Architektur: Registrierung komplett
ueber das Plugin-Manifest (page_routes + menu_items) — routes/index.tsx und
Sidebar.tsx wurden NICHT angefasst. Der Komponenten-Map-Generator wired den
lazy import automatisch (38 Komponenten).

Backend existierte vollstaendig (listings mit search/tags/pagination,
listing-detail, install mit Ed25519-Signatur-Verify, verify, categories;
marketplace:read/admin + require_admin fuer Install), Frontend hatte 0%
Abdeckung.

- api/marketplace.ts: TanStack-Hooks (useMarketplaceListings mit
  search/tags/pagination, useMarketplaceListing, useMarketplaceCategories,
  useInstallFromMarketplace, useVerifyMarketplacePlugin)
- pages/Marketplace.tsx: Suche, Tag-Filter-Chips, Listing-Karten (Name,
  Version, Author, Beschreibung, Tags, Download-Counter, Verified-Badge,
  Preis/Kostenlos), Install mit Confirm (Admin-only via is_system_admin),
  Signatur-Verify, Ergebnis-Banner, Pagination — No-Access-Card ohne
  marketplace:read
- Manifest: page_route /marketplace + menu_item (Store-Icon, order 85,
  permission marketplace:read)
- i18n marketplace.* + nav.marketplace de/en

Verifikation: Vitest 10/10 (Rendering, No-Access, Empty/Error, Karten,
  Search+Tags, Admin-Gating, Install-Flow mit+ohne Confirm, Verify-Flow) ·
  tsc exit 0 · production build exit 0 · Backend-Regressionen
  (route-order, m5-miniapps) 10/10 · compileall sauber · Manifest-Check:
  page_route + menu_item korrekt.
This commit is contained in:
Agent Zero
2026-09-13 09:44:28 +02:00
parent a3201ae221
commit 289dfc8230
7 changed files with 741 additions and 5 deletions
+136
View File
@@ -0,0 +1,136 @@
/**
* Marketplace API client — browse, search, verify and install plugins.
*
* Backend: /api/v1/marketplace (listings, listing detail, install, verify,
* categories). Permissions: marketplace:read (browse/verify), admin
* (install).
*/
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiGet, apiPost } from '@/api/client';
export interface MarketplaceListing {
id: string;
name: string;
display_name: string;
description: string;
version: string;
author: string;
homepage: string;
download_url: string;
icon: string;
screenshots: string[];
tags: string[];
price: number;
is_verified: boolean;
download_count: number;
min_app_version: string;
license: string;
created_at: string | null;
updated_at: string | null;
}
export interface MarketplaceListResponse {
listings: MarketplaceListing[];
total: number;
page: number;
page_size: number;
}
export interface MarketplaceCategoriesResponse {
categories: string[];
total: number;
}
export interface MarketplaceInstallResponse {
success: boolean;
name: string;
display_name: string;
version: string;
installed: boolean;
activated: boolean;
message: string;
error: string | null;
}
export interface MarketplaceVerifyResponse {
name: string;
version: string;
signature_valid: boolean;
message: string;
}
export interface MarketplaceListParams {
search?: string;
tags?: string[];
page?: number;
pageSize?: number;
}
// ─── Query hooks ─────────────────────────────────────────────
export function useMarketplaceListings(params: MarketplaceListParams = {}) {
const searchParams = new URLSearchParams();
if (params.search) searchParams.set('search', params.search);
if (params.tags && params.tags.length > 0) searchParams.set('tags', params.tags.join(','));
if (params.page) searchParams.set('page', String(params.page));
if (params.pageSize) searchParams.set('page_size', String(params.pageSize));
const qs = searchParams.toString();
return useQuery<MarketplaceListResponse>({
queryKey: ['marketplace-listings', params.search, params.tags, params.page, params.pageSize],
queryFn: () => apiGet<MarketplaceListResponse>(`/marketplace/listings${qs ? `?${qs}` : ''}`),
});
}
export function useMarketplaceListing(name: string | null) {
return useQuery<MarketplaceListing>({
queryKey: ['marketplace-listings', name],
queryFn: () => apiGet<MarketplaceListing>(`/marketplace/listings/${name}`),
enabled: !!name,
});
}
export function useMarketplaceCategories() {
return useQuery<MarketplaceCategoriesResponse>({
queryKey: ['marketplace-categories'],
queryFn: () => apiGet<MarketplaceCategoriesResponse>('/marketplace/categories'),
});
}
// ─── Mutation hooks ──────────────────────────────────────────
export function useInstallFromMarketplace() {
const qc = useQueryClient();
return useMutation<
MarketplaceInstallResponse,
Error,
{ name: string; signature?: string; publicKey?: string; activate?: boolean }
>({
mutationFn: ({ name, signature, publicKey, activate }) =>
apiPost<MarketplaceInstallResponse>(`/marketplace/install/${name}`, {
name,
signature: signature ?? null,
public_key: publicKey ?? null,
activate: activate ?? false,
}),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['marketplace-listings'] });
},
});
}
export function useVerifyMarketplacePlugin() {
return useMutation<
MarketplaceVerifyResponse,
Error,
{ name: string; signature?: string; publicKey?: string }
>({
mutationFn: ({ name, signature, publicKey }) =>
apiPost<MarketplaceVerifyResponse>(`/marketplace/verify/${name}`, {
name,
signature: signature ?? null,
public_key: publicKey ?? null,
}),
});
}