Ctrl K
ComunidadGuías…

frontend/src/types/index.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// Shared API types — mirror the backend Pydantic schemas.

// Mirrors the backend `LLMProvider` enum member-for-member, in the same order.
// `backend/tests/test_provider_frontend_parity.py` fails CI on any drift, here
// or in the PROVIDERS registry in `lib/providers.ts`.
export type LLMProvider =
  // --- AI brains ---
  | 'ollama'
  | 'openai'
  | 'anthropic'
  | 'gemini'
  | 'groq'
  | 'deepseek'
  | 'mistral'
  | 'xai'
  | 'openrouter'
  | 'together'
  | 'perplexity'
  | 'cerebras'
  | 'fireworks'
  | 'deepinfra'
  | 'sambanova'
  | 'nebius'
  | 'hyperbolic'
  | 'novita'
  | 'huggingface'
  | 'cohere'
  | 'moonshot'
  | 'zai'
  | 'qwen'
  | 'ai21'
  | 'custom'
  // --- Service integrations ---
  | 'x'
  | 'instagram'
  | 'reddit'
  | 'linkedin'
  | 'youtube'
  | 'tiktok'
  | 'bluesky'
  | 'pinterest'
  | 'discord'
  | 'slack'
  | 'telegram'
  | 'github'
  | 'gitlab'
  | 'vercel'
  | 'cloudflare'
  | 'sentry'
  | 'notion'
  | 'jira'
  | 'linear'
  | 'trello'
  | 'asana'
  | 'clickup'
  | 'airtable'
  | 'figma'
  | 'google_drive'
  | 'gmail'
  | 'resend'
  | 'sendgrid'
  | 'mailchimp'
  | 'twilio'
  | 'stripe'
  | 'shopify'
  | 'hubspot'
  | 'zendesk'
  | 'google_maps'
  | 'tavily'
  | 'exa'
  | 'serper'
  | 'firecrawl'
  | 'alpha_vantage'
  | 'coingecko'
  | 'openweather';

// Every account is provisioned with an active 'free' plan (unlimited tokens)
// at registration. The paid plans exist but are parked until a real payment
// processor is integrated.
export type SubscriptionPlan = 'free' | 'starter' | 'pro' | 'scale';

export type SubscriptionStatus =
  'active' | 'past_due' | 'canceled' | 'inactive';

export type CardBrand = 'visa' | 'mastercard';

export type TaskStatus =
  | 'pending'
  | 'running'
  | 'needs_review'
  | 'awaiting_answer'
  | 'completed'
  | 'completed_with_warnings'
  | 'failed'
  | 'cancelled'
  | 'timeout';

// The refresh half of the pair is never in a body — it arrives as an httpOnly
// cookie the browser stores on its own (CLAUDE.md §9 rule 14).
export interface AccessTokenResponse {
  access_token: string;
  token_type: string;
}

export interface UserPublic {
  id: string;
  email: string;
  display_name: string | undefined;
  // A display cache of the subscription row's plan. Registration provisions an
  // active 'free' plan, so a fresh account reads 'free', never null; null is
  // only a row that predates that provisioning.
  subscription_tier: SubscriptionPlan | null;
  // Default LLM "brain" for tasks; undefined/null means the local model (ollama).
  default_provider: LLMProvider | null | undefined;
  // Per-role model pins ({main: 'claude-opus-4-8', ...}); null/undefined = tier defaults.
  model_preferences?: Record<string, string> | null;
  // Profile personalization (client-rendered monogram avatar + short bio).
  bio?: string | null;
  avatar_color?: string | null;
  avatar_emoji?: string | null;
  // Account preferences.
  timezone?: string | null;
  default_reviewer_enabled?: boolean;
  default_tracing_enabled?: boolean;
  // Whether TOTP two-factor auth is active (never exposes the secret).
  two_factor_enabled?: boolean;
  // Whether the account's email address has been verified. While false, task
  // start, API-key creation and custom-API-tool writes are soft-gated
  // server-side (and only while EMAIL_VERIFICATION_REQUIRED is on).
  email_verified?: boolean;
  // ISO timestamp the account was created ("member since").
  created_at?: string | null;
  // ISO timestamp. Set means the account is locked and scheduled for purge.
  deletion_requested_at?: string | null;
  // Authorization role. The admin moderation surface is revealed only for 'admin'.
  role?: UserRole;
  // ISO timestamp. Set means a moderator suspended the account.
  suspended_at?: string | null;
}

export type UserRole = 'user' | 'admin';

/** One active login session (a refresh-token family). */
export interface SessionInfo {
  id: string;
  /** Human-readable device/browser summary parsed from the User-Agent. */
  device: string;
  ip: string | null;
  created_at: string;
  last_used_at: string | null;
  /** True for the session making this request. */
  current: boolean;
}

/** Response to a 2FA setup call: the secret to enroll + a ready-to-scan QR. */
export interface TwoFactorSetup {
  secret: string;
  otpauth_uri: string;
  /** Inline SVG markup for the QR code (no external image). */
  qr_svg: string;
}

/** One-time recovery codes shown right after enabling 2FA. */
export interface RecoveryCodes {
  recovery_codes: string[];
}

/**
 * A login response. Either a full token pair, or an MFA challenge that must be
 * completed via the TOTP verify endpoint before tokens are issued.
 */
export interface MfaChallenge {
  mfa_required: true;
  mfa_token: string;
}

/**
 * What a public form needs to render and submit, fetched at runtime.
 *
 * Deliberately not a build-time constant: a site key baked in as a
 * `NEXT_PUBLIC_*` value would tie the built image to one deployment, the same
 * reason `SITE_URL` is server-only. `provider === 'none'` means the CAPTCHA
 * layer is off and nothing should be loaded from a third party.
 */
export interface CaptchaChallenge {
  provider: 'none' | 'turnstile';
  site_key: string;
  nonce: string;
}

/**
 * The anti-automation fields a public form submits alongside its own data.
 *
 * `website_url` is a honeypot: it is hidden from users and must stay empty.
 * The backend answers a tripped submission with its normal success body, so
 * nothing here changes what the caller sees.
 */
export interface HumanCheckFields {
  website_url?: string;
  challenge?: string;
  captcha_token?: string;
}

/** When deletion was requested, and when it becomes irreversible. */
export interface AccountDeletionStatus {
  deletion_requested_at: string;
  purge_after: string;
}

// --- Billing ---

/** A card as typed by the user. Sent once, never stored client-side. */
export interface CardInput {
  number: string;
  exp_month: number;
  exp_year: number;
  cvc: string;
  holder: string;
}

/** The safe-to-display remnants of a card. */
export interface PaymentMethodPublic {
  brand: CardBrand;
  last4: string;
  exp_month: number;
  exp_year: number;
}

export interface PlanPublic {
  plan: SubscriptionPlan;
  price_cents: number;
  quota_tokens: number;
  currency: string;
}

/** A paid plan on the anonymous pricing page, at list price. */
export interface PlanPublicListing {
  plan: SubscriptionPlan;
  price_cents: number;
  quota_tokens: number;
  currency: string;
}

export interface SubscriptionPublic {
  plan: SubscriptionPlan;
  status: SubscriptionStatus;
  current_period_start: string;
  current_period_end: string;
  cancel_at_period_end: boolean;
  used_tokens: number;
  quota_tokens: number;
  payment_method: PaymentMethodPublic | undefined;
}

export interface ApiKeyPublic {
  id: string;
  provider: LLMProvider;
  label: string;
  key_hint: string;
  is_active: boolean;
  created_at: string;
  // Only set for the custom OpenAI-compatible provider.
  base_url?: string;
  model?: string;
}

export interface TaskCreated {
  task_id: string;
  status: TaskStatus;
}

export interface AgentEvent {
  type: string;
  ts: string;
  role?: string;
  state?: string;
  index?: number;
  subtask?: string;
  domain?: string;
  // Routing stage one's answer. Carried on its own interim node_update, and
  // again on the terminal one alongside the domain, so the log shows routing as
  // the two narrowing steps it is.
  group?: string;
  reason?: string;
  // Routing decision origin: 'user' (manual selection) | 'orchestrator'.
  source?: string;
  subtasks?: string[];
  // Fixed-team member running this subtask (display name + id).
  member?: string;
  member_id?: string;
  // Main Agent's per-member briefs for the fixed team.
  assignments?: AssignmentBrief[];
  answer?: string;
  // Streamed synthesis chunk (agent_delta events carry ~80 chars each in `text`).
  text?: string;
  question?: string;
  error?: string;
  // agent_warning: 'degraded' (a fallback ran) | 'retry' (provider link is
  // failing). Non-fatal — the task keeps going, but the result is affected.
  kind?: string;
  message?: string;
  // The true terminal status carried on a task_failed event (cancelled |
  // timeout | failed), so the live view freezes with the correct look
  // regardless of which client cancelled and on snapshot replay.
  status?: TaskStatus;
  approved?: boolean;
  issues?: string[];
  retry_hints?: string[];
  // Subagent tool activity (agent_message with role 'subagent'). Each tool
  // call emits twice — `done: false` on start, `done: true` on completion — so
  // the canvas can show a call as in-flight rather than only after the fact.
  action?: string;
  done?: boolean;
  content?: string;
  query?: string;
  url?: string;
  // Connected-API tools only: the BYOK service the call authenticated against
  // ('x', 'github', 'google_maps', 'discord', 'slack', 'telegram'). Absent for
  // the keyless tools, which get no lane on the connected rail.
  provider?: string;
  repo?: string;
  channel?: string;
  [key: string]: unknown;
}

export interface TaskState {
  task_id: string;
  status: TaskStatus;
  prompt: string;
  domain: string | undefined;
  reviewer_enabled: boolean;
  result: TaskResult | undefined;
  error: string | undefined;
  events: AgentEvent[];
  metadata: Record<string, unknown>;
}

/** One row of the task history sidebar. Carries no events. */
export interface TaskSummary {
  task_id: string;
  status: TaskStatus;
  /** Truncated server-side. */
  prompt: string;
  domain: string | undefined;
  error: string | undefined;
  created_at: string;
  updated_at: string;
}

export interface TaskListResponse {
  items: TaskSummary[];
  total: number;
  limit: number;
  offset: number;
}

export interface TaskResult {
  domain: string;
  answer: string;
  subtasks: unknown[];
  metadata: Record<string, unknown>;
}

export interface ApiError {
  detail: string;
}

// --- Dashboard ---

/** Daily series over the trend window, oldest first (one entry per day). */
export interface DashboardTrends {
  window_days: number;
  /** ISO YYYY-MM-DD labels, oldest first. */
  days: string[];
  tasks: number[];
  completed: number[];
  failed: number[];
  tokens: number[];
  /** 0..1 per day; null on days with no terminal tasks. */
  success_rate: (number | null)[];
  /** null on days with no tasks. */
  avg_tokens_per_task: (number | null)[];
}

export interface DashboardMetrics {
  total_tasks: number;
  running_tasks: number;
  completed_tasks: number;
  failed_tasks: number;
  success_rate: number;
  total_tokens: number;
  avg_tokens_per_task: number;
  trends: DashboardTrends;
}

export interface ProviderUsage {
  tasks: number;
  tokens: number;
}

export interface TokenUsage {
  user_id: string;
  total_tokens: number;
  total_tasks: number;
  success_rate: number;
  by_provider: Record<string, ProviderUsage>;
}

export interface CostSummary {
  currency: string;
  total_cost: number;
  by_provider: Record<string, number>;
}

// --- Traces (execution spans) & cost breakdown ---

export type SpanKind = 'task' | 'step' | 'agent' | 'llm' | 'tool';
export type SpanStatus = 'ok' | 'error';
export type CostGroupBy = 'day' | 'model' | 'domain';

/**
 * A span's OTel-style attribute bag. Keys are dotted (`gen_ai.*`, `maestro.*`),
 * so the typed hints are optional and an index signature keeps the shape open.
 */
export interface SpanAttributes {
  'gen_ai.request.model'?: string;
  'gen_ai.response.model'?: string;
  'gen_ai.usage.input_tokens'?: number;
  'gen_ai.usage.output_tokens'?: number;
  'gen_ai.system'?: string;
  'gen_ai.response.finish_reason'?: string;
  'maestro.request.preview'?: string;
  'maestro.tokens.estimated'?: boolean;
  'maestro.role'?: string;
  'maestro.member_id'?: string;
  'maestro.domain'?: string;
  'maestro.index'?: number;
  'maestro.provider'?: string;
  'maestro.resumed'?: boolean;
  'maestro.tool.action'?: string;
  [key: string]: unknown;
}

/** One execution span. Tree is encoded via `parent_span_id` -> `span_id`. */
export interface TraceSpan {
  trace_id: string;
  span_id: string;
  // null on the root "task" span; may reference an id absent under the 1000 cap.
  parent_span_id: string | null;
  name: string;
  kind: SpanKind;
  user_id: string;
  start_time: string; // ISO
  end_time: string; // ISO
  duration_ms: number;
  status: SpanStatus;
  error_message: string | null;
  attributes: SpanAttributes;
  // Nonzero only on llm.chat spans; all parents carry 0.
  cost_usd: number;
  created_at: string;
}

export interface TaskTrace {
  task_id: string;
  // Sorted by start_time ASC, capped at 1000 server-side. Empty when tracing off.
  spans: TraceSpan[];
}

/** One rollup row per distinct span name. */
export interface TraceSummaryNode {
  name: string;
  kind: SpanKind;
  count: number;
  input_tokens: number;
  output_tokens: number;
  cost_usd: number;
  duration_ms: number;
}

export interface TraceSummary {
  task_id: string;
  span_count: number;
  input_tokens: number;
  output_tokens: number;
  cost_usd: number;
  nodes: TraceSummaryNode[];
}

export interface CostBucket {
  // day -> "YYYY-MM-DD"; model -> model name; domain -> domain string ("unknown").
  key: string;
  cost_usd: number;
  input_tokens: number;
  output_tokens: number;
}

export interface CostBreakdown {
  days: number;
  group_by: CostGroupBy;
  total_cost_usd: number;
  buckets: CostBucket[];
}

// --- Agents (custom configurations) ---

// 'executable' tools have a real runtime behind the subagent directive loop;
// 'declarative' ones the model performs natively in its reasoning. Mirrors
// backend schemas/agent.py ToolCatalogEntry.
export type ToolKind = 'executable' | 'declarative' | 'custom_api';

export interface ToolCatalogItem {
  id: string;
  label: string;
  description: string;
  kind: ToolKind;
  // Which BYOK services this tool can authenticate against. Plural because
  // community_read picks one per call. Empty means nothing to connect.
  providers: LLMProvider[];
  // Still useful with no key at all (repo_intel: GitHub serves anonymous reads).
  keyless: boolean;
  // Whether *this* user holds an active key. Always true when providers is empty.
  connected: boolean;
  // The operator switch. Distinct from `connected`: the remedies differ.
  available: boolean;
}

export interface AgentConfig {
  id: string;
  name: string;
  domain: string;
  system_prompt: string;
  tools: string[];
  description: string;
  routing_hint: string;
  output_format: string;
  routable: boolean;
  custom_api_tool_ids: string[];
  skill_ids: string[];
  mcp_server_ids: string[];
  // Catalog tools an attached skill says it needs but this agent does not
  // enable. Computed by the backend on every read, never stored: the answer
  // changes when either side changes. Advisory — nothing at run time reads it,
  // because attached text must never widen an agent's tool set.
  missing_tools: string[];
  type: string;
  created_at: string;
  updated_at: string;
}

// A fixed team member of a built-in domain agent.
export interface TeamMember {
  id: string;
  name: string;
  description: string;
}

// One brief the Main Agent assigned to a fixed-team member.
export interface AssignmentBrief {
  member_id: string;
  member_name: string;
  brief: string;
  // Ids of earlier members whose output this member builds on.
  depends_on?: string[];
}

export interface BuiltinAgent {
  id: string;
  name: string;
  domain: string;
  /** The family this domain belongs to; drives the catalog tabs and its hue. */
  group: string;
  type: string;
  description: string;
  capabilities: string[];
  tools: string[];
  team: TeamMember[];
}

/** A family of related domains — one tab in the Architect catalog. */
export interface AgentGroup {
  id: string;
  name: string;
  description: string;
}

export interface AgentList {
  builtin: BuiltinAgent[];
  groups: AgentGroup[];
  custom: AgentConfig[];
}

export interface AgentConfigInput {
  name: string;
  domain: string;
  system_prompt: string;
  tools: string[];
  description: string;
  routing_hint: string;
  output_format: string;
  // Lets the orchestrator auto-route matching prompts to this agent, using
  // routing_hint as the description it classifies against.
  routable: boolean;
  // Ids of the user's own registered endpoints. Deliberately separate from
  // `tools`: those are catalog ids shared by everyone, these are per-user
  // records, and the backend rejects one appearing in the other's list.
  custom_api_tool_ids: string[];
  // Ids of the user's own skills, separate from `tools` for the same reason.
  skill_ids: string[];
  // And of their registered MCP servers. Same rule a third time.
  mcp_server_ids: string[];
}

// --- Agent skills (reusable instruction bundles) ---

export interface Skill {
  id: string;
  slug: string;
  name: string;
  description: string;
  instructions: string;
  output_format: string;
  // Advisory: catalog tool ids this skill expects an attaching agent to enable.
  required_tools: string[];
  version: number;
  source: string;
  marketplace_item_id?: string | null;
  plugin_id?: string | null;
  // False when the bundle trips the current injection scanner. Such a skill is
  // withheld from every run, so the UI has to be able to say why.
  security_scan_passed: boolean;
  created_at: string;
  updated_at: string;
}

export interface SkillInput {
  slug: string;
  name: string;
  description: string;
  instructions: string;
  output_format: string;
  required_tools: string[];
}

// --- Plugins (bundles of skills + MCP servers + agents) ---

// A manifest declares members; it never carries a credential and never ships
// tool schemas. Both are absences the backend enforces with `extra="forbid"`,
// so naming either is a 422 rather than a silent drop.
export interface PluginManifestSkill {
  slug: string;
  name: string;
  description: string;
  instructions: string;
  output_format: string;
  required_tools: string[];
}

export interface PluginManifestMcpServer {
  slug: string;
  name: string;
  description: string;
  url: string;
  transport: McpTransport;
  auth_mode: McpAuthMode;
  auth_name: string;
  timeout_seconds: number;
}

export interface PluginManifestAgent {
  slug: string;
  name: string;
  domain: string;
  system_prompt: string;
  tools: string[];
  description: string;
  routing_hint: string;
  output_format: string;
  routable: boolean;
  // Manifest-local slugs, resolved to freshly created ids at install.
  skill_slugs: string[];
  mcp_server_slugs: string[];
}

export interface PluginManifest {
  manifest_version: number;
  id: string;
  name: string;
  version: string;
  description: string;
  homepage: string;
  // A display label only — a manifest cannot assert who wrote it.
  author: string;
  skills: PluginManifestSkill[];
  mcp_servers: PluginManifestMcpServer[];
  agents: PluginManifestAgent[];
}

export interface Plugin {
  id: string;
  plugin_id: string;
  name: string;
  description: string;
  version: string;
  author_label: string;
  installs: number;
  featured: boolean;
  created_at: string;
}

export interface PluginDetail extends Plugin {
  manifest: PluginManifest;
}

export interface PluginInstall {
  id: string;
  plugin_id: string;
  name: string;
  version: string;
  source: 'catalog' | 'url';
  origin_url?: string | null;
  created_skill_ids: string[];
  created_mcp_server_ids: string[];
  created_agent_ids: string[];
  // Servers that landed disabled because the manifest could not carry their
  // credential. The one thing the user must do after installing.
  needs_credentials: string[];
  installed_at: string;
  updated_at: string;
}

export interface PluginInstallMember {
  kind: 'skill' | 'mcp_server' | 'agent';
  id: string;
  name: string;
  // True when the user changed it after installing.
  edited: boolean;
}

export interface PluginUpgradeChange {
  kind: 'skill' | 'mcp_server' | 'agent';
  slug: string;
  name: string;
  action: 'created' | 'updated' | 'unchanged' | 'removed' | 'conflict';
  // Fields the new version changed and the upgrade applied.
  applied: string[];
  // Fields the new version changed that you had also changed, so they were
  // left alone. This is why the upgrade is a three-way merge, not an overwrite.
  conflicted: string[];
  note: string;
}

export interface PluginUpgradeResult {
  dry_run: boolean;
  from_version: string;
  to_version: string;
  changed: boolean;
  changes: PluginUpgradeChange[];
}

export interface PluginUninstallPreview {
  plugin_id: string;
  name: string;
  members: PluginInstallMember[];
}

// --- Remote MCP servers ---

// Only Streamable HTTP is supported. Local stdio is not a transport and never
// will be: Maestro is hosted, and a stdio server means a process on the host.
export type McpTransport = 'streamable_http';
// No `query` mode, unlike a custom API tool: an MCP endpoint is called
// repeatedly, and a credential in the URL would land in every intermediary log.
export type McpAuthMode = 'none' | 'bearer' | 'header';

export interface McpTool {
  // The sanitized id the model names, `mcp__{server}__{tool}`.
  action: string;
  // The server's own spelling, which is what `tools/call` sends.
  remote_name: string;
  display_name: string;
  description: string;
  // Rebuilt from the server's inputSchema, never passed through.
  parameters: Record<string, unknown>;
  enabled: boolean;
  // Set when the tool was withheld: a failed injection scan, an unusable name,
  // or a schema that could not be rebuilt within the caps.
  blocked_reason?: string | null;
}

export interface McpServer {
  id: string;
  slug: string;
  name: string;
  description: string;
  url: string;
  transport: McpTransport;
  headers: Record<string, string>;
  auth_mode: McpAuthMode;
  auth_name: string;
  // Masked tail only — the stored credential is never returned.
  secret_hint?: string;
  timeout_seconds: number;
  enabled: boolean;
  tool_allowlist: string[];
  tools: McpTool[];
  tools_fetched_at?: string | null;
  // True once the cache has aged past its TTL, or once a call named a tool the
  // server no longer has. Drives the "rediscover" prompt.
  tools_stale: boolean;
  protocol_version: string;
  server_name: string;
  server_version: string;
  source: string;
  created_at: string;
  updated_at: string;
}

export interface McpServerInput {
  slug: string;
  name: string;
  description: string;
  url: string;
  transport: McpTransport;
  headers: Record<string, string>;
  auth_mode: McpAuthMode;
  auth_name: string;
  // Write-only. Omit on update to leave the stored credential untouched.
  secret?: string;
  timeout_seconds: number;
  enabled: boolean;
  tool_allowlist?: string[];
}

export interface McpDiscoverResult {
  ok: boolean;
  server_name: string;
  server_version: string;
  protocol_version: string;
  tools: McpTool[];
  // How many tools the server advertised but Maestro refused to expose.
  withheld: number;
  duration_ms: number;
  error?: string | null;
}

// --- Custom API tools (user-registered HTTP endpoints) ---

export type CustomApiAuthMode = 'none' | 'bearer' | 'header' | 'query';
export type CustomApiMethod = 'GET' | 'POST';
export type CustomApiParameterType = 'string' | 'integer' | 'number' | 'boolean';

export interface CustomApiParameter {
  name: string;
  type: CustomApiParameterType;
  description: string;
  required: boolean;
  enum?: string[];
}

export interface CustomApiTool {
  id: string;
  slug: string;
  name: string;
  description: string;
  method: CustomApiMethod;
  base_url: string;
  path_template: string;
  query_template: Record<string, string>;
  headers: Record<string, string>;
  auth_mode: CustomApiAuthMode;
  auth_name: string;
  // Masked tail only — the stored credential is never returned.
  secret_hint?: string;
  parameters: CustomApiParameter[];
  response_json_path: string;
  response_max_chars: number;
  timeout_seconds: number;
  enabled: boolean;
  created_at: string;
  updated_at: string;
}

export interface CustomApiToolInput {
  slug: string;
  name: string;
  description: string;
  method: CustomApiMethod;
  base_url: string;
  path_template: string;
  auth_mode: CustomApiAuthMode;
  auth_name: string;
  // Write-only. Omit on update to leave the stored credential untouched.
  secret?: string;
  parameters: CustomApiParameter[];
  enabled: boolean;
}

export interface CustomApiToolTestResult {
  ok: boolean;
  status: number;
  duration_ms: number;
  preview: string;
  error?: string;
}

// --- Marketplace ---

export interface MarketplaceItem {
  id: string;
  name: string;
  description: string;
  domain: string;
  system_prompt: string;
  tools: string[];
  installs: number;
  security_scan: Record<string, unknown>;
  created_at: string;
  /** Daily install counts over the trend window (oldest first); null until events exist. */
  install_history: number[] | null;
  /** Denormalized review aggregates; null/0 until the first review lands. */
  rating_avg: number | null;
  rating_count: number;
}

/**
 * A published item as served to anonymous visitors. Deliberately narrower than
 * `MarketplaceItem`: the author's system prompt and the raw security-scan
 * findings never leave the authenticated surface.
 */
export interface MarketplaceItemPreview {
  id: string;
  name: string;
  description: string;
  domain: string;
  tools: string[];
  installs: number;
  /** True only for the first-party teams Maestro publishes. */
  featured: boolean;
  author_label: string;
  security_scan_status: string;
  created_at: string;
  rating_avg: number | null;
  rating_count: number;
}

/**
 * A review as served to clients. Carries no author identity (reviewer display
 * names are never public); `is_own` marks the caller's review server-side.
 */
export interface MarketplaceReview {
  /** The review's own id (a random uuid, not identity) — used to report it. */
  id: string;
  rating: number;
  comment: string | null;
  created_at: string;
  updated_at: string;
  is_own: boolean;
}

export interface MarketplaceReviewList {
  items: MarketplaceReview[];
  total: number;
  /** The caller's review regardless of pagination, for prefilling the form. */
  my_review: MarketplaceReview | null;
}

export interface MarketplaceReviewInput {
  rating: number;
  comment?: string;
}

export interface MarketplacePublishInput {
  name: string;
  description: string;
  domain: string;
  system_prompt: string;
  tools: string[];
}

// --- Documents (RAG knowledge base) ---

export interface DocumentMeta {
  id: string;
  filename: string;
  chunk_count: number;
  size_bytes: number;
  created_at: string;
}

/**
 * Knowledge-base usage against the account's plan allowance.
 *
 * `max_documents` / `max_bytes` are `null` for an unmetered (admin) account,
 * which means "no ceiling" — never zero. Branch on it before comparing.
 */
export interface DocumentStorage {
  documents: number;
  bytes: number;
  max_documents: number | null;
  max_bytes: number | null;
}

// --- Moderation / content reports ---

export type ReportReason = 'spam' | 'abuse' | 'malicious' | 'other';
export type ReportStatus = 'open' | 'resolved' | 'dismissed';
export type MarketplaceStatus = 'published' | 'hidden' | 'removed';

export interface ReportInput {
  reason: ReportReason;
  note?: string;
}

// --- Admin / moderation surface ---

export interface AdminRecentItem {
  id: string;
  name: string;
  domain: string;
  author_id: string | null;
  status: string;
  created_at: string | null;
}

export interface AdminOverview {
  users_total: number;
  admins_total: number;
  suspended_total: number;
  items_total: number;
  items_hidden: number;
  items_removed: number;
  reviews_total: number;
  open_reports: number;
  recent_items: AdminRecentItem[];
}

export interface AdminUserRow {
  id: string;
  email: string;
  display_name: string | null;
  role: UserRole;
  subscription_tier: SubscriptionPlan | null;
  email_verified: boolean;
  suspended_at: string | null;
  deletion_requested_at: string | null;
  created_at: string | null;
}

export interface AdminUserDetail {
  user: AdminUserRow;
  counts: { items: number; agents: number; reviews: number };
}

export interface AdminMarketplaceItem {
  id: string;
  name: string;
  description: string;
  domain: string;
  author_id: string | null;
  installs: number;
  status: MarketplaceStatus;
  rating_avg: number | null;
  rating_count: number;
  security_scan: Record<string, unknown> | null;
  moderation: Record<string, unknown> | null;
  created_at: string | null;
}

export interface AdminReview {
  id: string;
  item_id: string;
  user_id: string;
  rating: number;
  comment: string | null;
  hidden: boolean;
  created_at: string | null;
  updated_at: string | null;
}

export interface AdminReport {
  id: string;
  target_type: 'item' | 'review';
  target_id: string;
  reporter_id: string;
  reason: ReportReason;
  note: string | null;
  status: ReportStatus;
  created_at: string | null;
  resolved_by: string | null;
  resolved_at: string | null;
}

export interface AdminAuditEntry {
  id: string;
  actor_id: string;
  action: string;
  target_type: string;
  target_id: string;
  reason: string | null;
  metadata: Record<string, unknown>;
  created_at: string | null;
}