From e9d2515a7fdb6864d00a97d90e3cd55c18fc1e4b Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 25 Feb 2026 18:05:16 +0700 Subject: [PATCH] feat(ui): add updates center with support matrix - add data-driven catalog for support notices and CLI compatibility - introduce Updates Center page with announcements, filtering, and search - render reusable support cards and status badges for future expansions --- .../components/updates/support-entry-card.tsx | 96 ++++++++ .../updates/support-status-badge.tsx | 31 +++ ui/src/lib/support-updates-catalog.ts | 222 ++++++++++++++++++ ui/src/pages/updates.tsx | 214 +++++++++++++++++ 4 files changed, 563 insertions(+) create mode 100644 ui/src/components/updates/support-entry-card.tsx create mode 100644 ui/src/components/updates/support-status-badge.tsx create mode 100644 ui/src/lib/support-updates-catalog.ts create mode 100644 ui/src/pages/updates.tsx diff --git a/ui/src/components/updates/support-entry-card.tsx b/ui/src/components/updates/support-entry-card.tsx new file mode 100644 index 00000000..8d8e64b4 --- /dev/null +++ b/ui/src/components/updates/support-entry-card.tsx @@ -0,0 +1,96 @@ +import { Link } from 'react-router-dom'; +import { ArrowUpRight } from 'lucide-react'; +import { Badge } from '@/components/ui/badge'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { + SUPPORT_SCOPE_LABELS, + type CliSupportEntry, + type SupportScope, +} from '@/lib/support-updates-catalog'; +import { SupportStatusBadge } from './support-status-badge'; + +const PILLAR_LABELS: { key: keyof CliSupportEntry['pillars']; label: string }[] = [ + { key: 'baseUrl', label: 'Base URL' }, + { key: 'auth', label: 'Auth' }, + { key: 'model', label: 'Model' }, +]; + +const SCOPE_STYLES: Record = { + target: + 'border-violet-200 bg-violet-50 text-violet-700 dark:border-violet-900/50 dark:bg-violet-900/20 dark:text-violet-300', + cliproxy: + 'border-sky-200 bg-sky-50 text-sky-700 dark:border-sky-900/50 dark:bg-sky-900/20 dark:text-sky-300', + 'api-profiles': + 'border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-900/50 dark:bg-emerald-900/20 dark:text-emerald-300', + websearch: + 'border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-900/50 dark:bg-amber-900/20 dark:text-amber-300', +}; + +export function SupportEntryCard({ entry }: { entry: CliSupportEntry }) { + return ( + + +
+
+ {entry.name} + {entry.summary} +
+ +
+ + + {SUPPORT_SCOPE_LABELS[entry.scope]} + +
+ + +
+ {PILLAR_LABELS.map((pillar) => ( +
+

+ {pillar.label} +

+

{entry.pillars[pillar.key]}

+
+ ))} +
+ +
+

+ Dashboard +

+
+ {entry.routes.map((route) => ( + + {route.label} + + + ))} +
+
+ +
+

+ CLI Usage +

+
+ {entry.commands.slice(0, 2).map((command) => ( + + {command} + + ))} +
+
+ + {entry.notes &&

{entry.notes}

} +
+
+ ); +} diff --git a/ui/src/components/updates/support-status-badge.tsx b/ui/src/components/updates/support-status-badge.tsx new file mode 100644 index 00000000..2ab10cd8 --- /dev/null +++ b/ui/src/components/updates/support-status-badge.tsx @@ -0,0 +1,31 @@ +import { Badge } from '@/components/ui/badge'; +import { cn } from '@/lib/utils'; +import type { SupportStatus } from '@/lib/support-updates-catalog'; + +const STATUS_LABELS: Record = { + new: 'New', + stable: 'Stable', + planned: 'Planned', +}; + +const STATUS_STYLES: Record = { + new: 'border-blue-200 bg-blue-50 text-blue-700 dark:border-blue-900/50 dark:bg-blue-900/20 dark:text-blue-300', + stable: + 'border-green-200 bg-green-50 text-green-700 dark:border-green-900/50 dark:bg-green-900/20 dark:text-green-300', + planned: + 'border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-900/50 dark:bg-amber-900/20 dark:text-amber-300', +}; + +export function SupportStatusBadge({ + status, + className, +}: { + status: SupportStatus; + className?: string; +}) { + return ( + + {STATUS_LABELS[status]} + + ); +} diff --git a/ui/src/lib/support-updates-catalog.ts b/ui/src/lib/support-updates-catalog.ts new file mode 100644 index 00000000..195bc992 --- /dev/null +++ b/ui/src/lib/support-updates-catalog.ts @@ -0,0 +1,222 @@ +export type SupportStatus = 'new' | 'stable' | 'planned'; + +export type SupportScope = 'target' | 'cliproxy' | 'api-profiles' | 'websearch'; + +export interface SupportRouteHint { + label: string; + path: string; +} + +export interface SupportNotice { + id: string; + title: string; + summary: string; + publishedAt: string; + status: SupportStatus; + highlights: string[]; + routes: SupportRouteHint[]; + commands: string[]; +} + +export interface CliSupportEntry { + id: string; + name: string; + scope: SupportScope; + status: SupportStatus; + summary: string; + pillars: { + baseUrl: string; + auth: string; + model: string; + }; + routes: SupportRouteHint[]; + commands: string[]; + notes?: string; +} + +export const SUPPORT_SCOPE_LABELS: Record = { + target: 'Target CLI', + cliproxy: 'CLIProxy Provider', + 'api-profiles': 'API Profile', + websearch: 'WebSearch', +}; + +export const SUPPORT_NOTICES: SupportNotice[] = [ + { + id: 'droid-target-support', + title: 'Factory Droid support is live', + summary: + 'API Profiles and CLIProxy variants now support Droid as a first-class execution target.', + publishedAt: '2026-02-25', + status: 'new', + highlights: [ + 'Set default target to Droid when creating or editing API Profiles.', + 'Set default target to Droid for CLIProxy variants, including Codex and Antigravity flows.', + 'Use ccsd alias or --target droid for one-off target overrides.', + ], + routes: [ + { label: 'API Profiles', path: '/providers' }, + { label: 'CLIProxy', path: '/cliproxy' }, + ], + commands: [ + 'ccsd glm', + 'ccs codex --target droid "your prompt"', + 'ccs cliproxy create mycodex --provider codex --target droid', + ], + }, + { + id: 'updates-center-launch', + title: 'Updates Center added to dashboard navigation', + summary: + 'CCS now has a dedicated updates route so support announcements are visible and reusable.', + publishedAt: '2026-02-25', + status: 'new', + highlights: [ + 'Single data source powers Home spotlight and Updates Center page.', + 'New support entries can be added without touching multiple pages.', + 'Catalog includes targets, CLIProxy providers, and WebSearch integrations.', + ], + routes: [{ label: 'Updates Center', path: '/updates' }], + commands: ['ccs config'], + }, +]; + +export const CLI_SUPPORT_ENTRIES: CliSupportEntry[] = [ + { + id: 'claude-target', + name: 'Claude Code', + scope: 'target', + status: 'stable', + summary: 'Default runtime target for all CCS profile types.', + pillars: { + baseUrl: 'From profile settings (ANTHROPIC_BASE_URL)', + auth: 'From profile settings (ANTHROPIC_AUTH_TOKEN)', + model: 'From profile settings (ANTHROPIC_MODEL)', + }, + routes: [ + { label: 'API Profiles', path: '/providers' }, + { label: 'CLIProxy', path: '/cliproxy' }, + ], + commands: ['ccs', 'ccs glm "your prompt"'], + }, + { + id: 'droid-target', + name: 'Factory Droid', + scope: 'target', + status: 'new', + summary: 'First-class target for API Profiles and CLIProxy variants.', + pillars: { + baseUrl: 'From profile or variant settings', + auth: 'From profile or variant settings', + model: 'From profile or variant settings', + }, + routes: [ + { label: 'API Profiles', path: '/providers' }, + { label: 'CLIProxy', path: '/cliproxy' }, + ], + commands: ['ccsd glm', 'ccs km --target droid', 'ccs codex --target droid'], + notes: 'Use ccsd alias for automatic Droid target selection.', + }, + { + id: 'codex-cliproxy', + name: 'Codex via CLIProxy', + scope: 'cliproxy', + status: 'stable', + summary: 'OAuth-backed provider with configurable variant model and target.', + pillars: { + baseUrl: 'Managed by CLIProxy backend', + auth: 'OAuth account via CLIProxy auth flow', + model: 'Selectable per provider or variant', + }, + routes: [ + { label: 'CLIProxy', path: '/cliproxy' }, + { label: 'Control Panel', path: '/cliproxy/control-panel' }, + ], + commands: ['ccs codex', 'ccs cliproxy create mycodex --provider codex'], + }, + { + id: 'gemini-cliproxy', + name: 'Gemini via CLIProxy', + scope: 'cliproxy', + status: 'stable', + summary: 'OAuth-backed Gemini provider with multi-account management.', + pillars: { + baseUrl: 'Managed by CLIProxy backend', + auth: 'OAuth account via CLIProxy auth flow', + model: 'Selectable per provider or variant', + }, + routes: [ + { label: 'CLIProxy', path: '/cliproxy' }, + { label: 'Control Panel', path: '/cliproxy/control-panel' }, + ], + commands: ['ccs gemini', 'ccs cliproxy create mygem --provider gemini'], + }, + { + id: 'agy-cliproxy', + name: 'Antigravity via CLIProxy', + scope: 'cliproxy', + status: 'stable', + summary: 'OAuth-backed Antigravity provider with variant target controls.', + pillars: { + baseUrl: 'Managed by CLIProxy backend', + auth: 'OAuth account via CLIProxy auth flow', + model: 'Selectable per provider or variant', + }, + routes: [ + { label: 'CLIProxy', path: '/cliproxy' }, + { label: 'Control Panel', path: '/cliproxy/control-panel' }, + ], + commands: ['ccs agy', 'ccs cliproxy create myagy --provider agy --target droid'], + }, + { + id: 'custom-api-profiles', + name: 'Custom API Profiles', + scope: 'api-profiles', + status: 'stable', + summary: 'Any Anthropic-compatible endpoint with per-profile target and model mapping.', + pillars: { + baseUrl: 'User-defined endpoint', + auth: 'User-defined token/key', + model: 'User-defined model identifier', + }, + routes: [{ label: 'API Profiles', path: '/providers' }], + commands: ['ccs api create myprofile', 'ccs myprofile "your prompt"'], + }, + { + id: 'opencode-websearch', + name: 'OpenCode WebSearch', + scope: 'websearch', + status: 'stable', + summary: 'WebSearch provider surfaced in Settings for third-party profile workflows.', + pillars: { + baseUrl: 'Managed by OpenCode CLI integration', + auth: 'Provider-specific (managed externally)', + model: 'Configurable in WebSearch settings', + }, + routes: [{ label: 'Settings', path: '/settings' }], + commands: ['ccs config', 'ccs codex "your prompt"'], + notes: 'Enable OpenCode in Settings > WebSearch to activate fallback search.', + }, +]; + +export function getLatestSupportNotice(): SupportNotice | null { + if (SUPPORT_NOTICES.length === 0) { + return null; + } + + return [...SUPPORT_NOTICES].sort((a, b) => b.publishedAt.localeCompare(a.publishedAt))[0]; +} + +export function formatCatalogDate(value: string): string { + const parsed = new Date(`${value}T00:00:00Z`); + if (Number.isNaN(parsed.getTime())) { + return value; + } + + return new Intl.DateTimeFormat(undefined, { + year: 'numeric', + month: 'short', + day: 'numeric', + timeZone: 'UTC', + }).format(parsed); +} diff --git a/ui/src/pages/updates.tsx b/ui/src/pages/updates.tsx new file mode 100644 index 00000000..24f7cd2b --- /dev/null +++ b/ui/src/pages/updates.tsx @@ -0,0 +1,214 @@ +import { useMemo, useState } from 'react'; +import { Link } from 'react-router-dom'; +import { BellRing, Filter, Megaphone, Search } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { SupportEntryCard } from '@/components/updates/support-entry-card'; +import { SupportStatusBadge } from '@/components/updates/support-status-badge'; +import { + CLI_SUPPORT_ENTRIES, + SUPPORT_NOTICES, + SUPPORT_SCOPE_LABELS, + formatCatalogDate, + type SupportScope, +} from '@/lib/support-updates-catalog'; + +type ScopeFilter = 'all' | SupportScope; + +const SCOPE_FILTERS: { id: ScopeFilter; label: string }[] = [ + { id: 'all', label: 'All' }, + { id: 'target', label: SUPPORT_SCOPE_LABELS.target }, + { id: 'cliproxy', label: SUPPORT_SCOPE_LABELS.cliproxy }, + { id: 'api-profiles', label: SUPPORT_SCOPE_LABELS['api-profiles'] }, + { id: 'websearch', label: SUPPORT_SCOPE_LABELS.websearch }, +]; + +export function UpdatesPage() { + const [scope, setScope] = useState('all'); + const [query, setQuery] = useState(''); + + const filteredEntries = useMemo(() => { + const queryValue = query.trim().toLowerCase(); + + return CLI_SUPPORT_ENTRIES.filter((entry) => { + if (scope !== 'all' && entry.scope !== scope) { + return false; + } + + if (!queryValue) { + return true; + } + + const haystack = [ + entry.name, + entry.summary, + entry.notes || '', + ...entry.commands, + ...entry.routes.map((route) => route.label), + ] + .join(' ') + .toLowerCase(); + + return haystack.includes(queryValue); + }); + }, [scope, query]); + + return ( +
+ + + + + CCS Updates Center + + + Release visibility for runtime support, CLIProxy providers, and integration readiness. + + + + + + + This page is data-driven. Update one catalog file to publish new support notices + across dashboard surfaces. + + + +
+ + ccsd glm + + + ccs codex --target droid "your prompt" + + + ccs cliproxy create mycodex --provider codex --target droid + +
+
+
+ +
+
+

Announcements

+ {SUPPORT_NOTICES.length} published +
+ +
+ {SUPPORT_NOTICES.map((notice) => ( + + +
+
+ {notice.title} + {notice.summary} +
+ +
+

+ {formatCatalogDate(notice.publishedAt)} +

+
+ +
    + {notice.highlights.map((highlight) => ( +
  • {highlight}
  • + ))} +
+ +
+ {notice.routes.map((route) => ( + + {route.label} + + ))} +
+
+
+ ))} +
+
+ +
+
+
+

Support Matrix

+

+ Search by CLI/provider and filter by support surface. +

+
+ +
+ + setQuery(event.target.value)} + placeholder="Search by command, provider, or note" + className="pl-8" + /> +
+
+ +
+ + + Scope: + + {SCOPE_FILTERS.map((filter) => ( + + ))} + {filteredEntries.length} entries +
+ + {filteredEntries.length === 0 ? ( + + + No support entries match this filter. + + + ) : ( +
+ {filteredEntries.map((entry) => ( + + ))} +
+ )} +
+ + + + Maintainer Notes + + Keep update messaging in one place for future CLI expansions. + + + +

+ Edit{' '} + + ui/src/lib/support-updates-catalog.ts + {' '} + to add new notices or support entries. +

+

+ Home spotlight and this page consume the same catalog, so announcements stay consistent + without repeated UI edits. +

+
+
+
+ ); +}