@burdenoff/microfe-vibecontrols 2026.623.1 → 2026.623.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/actions/ActionCard.js +2 -2
- package/dist/components/actions/ActionCard.js.map +1 -1
- package/dist/components/actions/ActionTypeBadge.js +1 -1
- package/dist/components/actions/ActionTypeBadge.js.map +1 -1
- package/dist/components/actions/ExecutionHistory.js +1 -1
- package/dist/components/actions/ExecutionHistory.js.map +1 -1
- package/dist/components/agent-manager/AgentManagerAdminBar.js +1 -1
- package/dist/components/agent-manager/AgentManagerAdminBar.js.map +1 -1
- package/dist/components/agent-manager/AgentManagerChatPane.js +2 -2
- package/dist/components/agent-manager/AgentManagerChatPane.js.map +1 -1
- package/dist/components/agent-manager/AgentManagerInputArea.js +4 -4
- package/dist/components/agent-manager/AgentManagerInputArea.js.map +1 -1
- package/dist/components/agent-manager/AgentManagerLayout.js +1 -1
- package/dist/components/agent-manager/AgentManagerLayout.js.map +1 -1
- package/dist/components/agent-manager/AgentManagerMessageItem.js +1 -1
- package/dist/components/agent-manager/AgentManagerMessageItem.js.map +1 -1
- package/dist/components/agent-manager/AgentManagerSidebar.js +5 -5
- package/dist/components/agent-manager/AgentManagerSidebar.js.map +1 -1
- package/dist/components/agent-manager/AgentManagerTab.js +1 -1
- package/dist/components/agent-manager/AgentManagerTab.js.map +1 -1
- package/dist/components/agent-manager/AgentManagerTabBar.js +1 -1
- package/dist/components/agent-manager/AgentManagerTabBar.js.map +1 -1
- package/dist/components/agent-manager/BridleConfigPanel.js +24 -24
- package/dist/components/agent-manager/BridleConfigPanel.js.map +1 -1
- package/dist/components/agent-manager/PluginInstallModal.js +1 -1
- package/dist/components/agent-manager/PluginInstallModal.js.map +1 -1
- package/dist/components/agent-manager/ProfilesPanel.js +5 -5
- package/dist/components/agent-manager/ProfilesPanel.js.map +1 -1
- package/dist/components/agent-manager/controls/AutonomySelector.js +1 -1
- package/dist/components/agent-manager/controls/AutonomySelector.js.map +1 -1
- package/dist/components/agent-manager/controls/SdkSelector.js +1 -1
- package/dist/components/agent-manager/controls/SdkSelector.js.map +1 -1
- package/dist/components/agent-manager/controls/VoiceToggle.js +1 -1
- package/dist/components/agent-manager/controls/VoiceToggle.js.map +1 -1
- package/dist/components/agent-manager/detail/SessionConfigPanel.js +6 -6
- package/dist/components/agent-manager/detail/SessionConfigPanel.js.map +1 -1
- package/dist/components/agent-manager/detail/SessionLogsPanel.js +5 -5
- package/dist/components/agent-manager/detail/SessionLogsPanel.js.map +1 -1
- package/dist/components/agent-manager/detail/SessionToolsPanel.js +1 -1
- package/dist/components/agent-manager/detail/SessionToolsPanel.js.map +1 -1
- package/dist/components/agent-manager/dialogs/NewSessionDialog.js +2 -2
- package/dist/components/agent-manager/dialogs/NewSessionDialog.js.map +1 -1
- package/dist/components/agent-manager/dialogs/RenameSessionDialog.js +1 -1
- package/dist/components/agent-manager/dialogs/RenameSessionDialog.js.map +1 -1
- package/dist/components/agents/AgentCard.js +14 -11
- package/dist/components/agents/AgentCard.js.map +1 -1
- package/dist/components/ai/AIContextsTab.js +1 -1
- package/dist/components/ai/AIContextsTab.js.map +1 -1
- package/dist/components/ai/AIOverviewTab.js +4 -4
- package/dist/components/ai/AIOverviewTab.js.map +1 -1
- package/dist/components/ai/AIPlanPanel.js +4 -4
- package/dist/components/ai/AIPlanPanel.js.map +1 -1
- package/dist/components/ai/AIPlaygroundTab.js +1 -1
- package/dist/components/ai/AIPlaygroundTab.js.map +1 -1
- package/dist/components/ai/AIPromptsTab.js +2 -2
- package/dist/components/ai/AIPromptsTab.js.map +1 -1
- package/dist/components/ai/AIProviderCredentialsCard.js +4 -4
- package/dist/components/ai/AIProviderCredentialsCard.js.map +1 -1
- package/dist/components/ai/AIQueueTab.js +4 -4
- package/dist/components/ai/AIQueueTab.js.map +1 -1
- package/dist/components/ai/AISessionsTab.js +4 -4
- package/dist/components/ai/AISessionsTab.js.map +1 -1
- package/dist/components/ai/AITasksTab.js +1 -1
- package/dist/components/ai/AITasksTab.js.map +1 -1
- package/dist/components/ai/ClaudeCredentialsCard.js +3 -3
- package/dist/components/ai/ClaudeCredentialsCard.js.map +1 -1
- package/dist/components/ai/PluginHarnessPicker.js +3 -3
- package/dist/components/ai/PluginHarnessPicker.js.map +1 -1
- package/dist/components/analytics/AnalyticsBreakdownChart.js +8 -8
- package/dist/components/analytics/AnalyticsBreakdownChart.js.map +1 -1
- package/dist/components/analytics/AnalyticsTimeSeriesChart.js +5 -5
- package/dist/components/analytics/AnalyticsTimeSeriesChart.js.map +1 -1
- package/dist/components/audit/AuditDeleteDialog.js +2 -2
- package/dist/components/audit/AuditDeleteDialog.js.map +1 -1
- package/dist/components/audit/AuditExportButton.js +5 -5
- package/dist/components/audit/AuditExportButton.js.map +1 -1
- package/dist/components/audit/AuditLogDetail.js +9 -9
- package/dist/components/audit/AuditLogDetail.js.map +1 -1
- package/dist/components/audit/AuditLogFilters.js +1 -1
- package/dist/components/audit/AuditLogFilters.js.map +1 -1
- package/dist/components/audit/AuditLogStats.js +1 -1
- package/dist/components/audit/AuditLogStats.js.map +1 -1
- package/dist/components/audit/AuditLogTable.js +28 -28
- package/dist/components/audit/AuditLogTable.js.map +1 -1
- package/dist/components/catalog/CatalogCard.js +3 -3
- package/dist/components/catalog/CatalogCard.js.map +1 -1
- package/dist/components/catalog/CatalogForm.js +1 -1
- package/dist/components/catalog/CatalogForm.js.map +1 -1
- package/dist/components/common/ActionAgentTargetNotice.js +6 -6
- package/dist/components/common/ActionAgentTargetNotice.js.map +1 -1
- package/dist/components/common/CommandEditor.js +1 -1
- package/dist/components/common/CommandEditor.js.map +1 -1
- package/dist/components/docs/DocsPageTree.js +1 -1
- package/dist/components/docs/DocsPageTree.js.map +1 -1
- package/dist/components/docs/DocsPreviewPanel.js +1 -1
- package/dist/components/docs/DocsPreviewPanel.js.map +1 -1
- package/dist/components/docs/DocsRevisionHistory.js +4 -4
- package/dist/components/docs/DocsRevisionHistory.js.map +1 -1
- package/dist/components/docs/DocsSettingsPanel.js +7 -7
- package/dist/components/docs/DocsSettingsPanel.js.map +1 -1
- package/dist/components/docs/DocsTabPanel.js +1 -1
- package/dist/components/docs/DocsTabPanel.js.map +1 -1
- package/dist/components/graph/ConnectionPermissionsDialog.js +2 -2
- package/dist/components/graph/ConnectionPermissionsDialog.js.map +1 -1
- package/dist/components/graph/MeshDeployWizard.js +3 -3
- package/dist/components/graph/MeshDeployWizard.js.map +1 -1
- package/dist/components/graph/MeshRequestPanel.js +2 -2
- package/dist/components/graph/MeshRequestPanel.js.map +1 -1
- package/dist/components/idp-components/ComponentCard.js +5 -5
- package/dist/components/idp-components/ComponentCard.js.map +1 -1
- package/dist/components/idp-components/ComponentForm.js +1 -1
- package/dist/components/idp-components/ComponentForm.js.map +1 -1
- package/dist/components/idp-components/DependencyGraphTab.js +7 -7
- package/dist/components/idp-components/DependencyGraphTab.js.map +1 -1
- package/dist/components/logs/LogViewer.js +14 -14
- package/dist/components/logs/LogViewer.js.map +1 -1
- package/dist/components/notes/NoteCard.js +1 -1
- package/dist/components/notes/NoteCard.js.map +1 -1
- package/dist/components/notes/NoteEditor.js +2 -2
- package/dist/components/notes/NoteEditor.js.map +1 -1
- package/dist/components/notes/NoteList.js +1 -1
- package/dist/components/notes/NoteList.js.map +1 -1
- package/dist/components/notes/NotesTabPanel.js +1 -1
- package/dist/components/notes/NotesTabPanel.js.map +1 -1
- package/dist/components/scorecard/ScorecardEmptyState.js +1 -1
- package/dist/components/scorecard/ScorecardEmptyState.js.map +1 -1
- package/dist/components/scorecard/ScorecardPanel.js +3 -3
- package/dist/components/scorecard/ScorecardPanel.js.map +1 -1
- package/dist/components/security/SecurityScanList.js +11 -11
- package/dist/components/security/SecurityScanList.js.map +1 -1
- package/dist/components/sessions/SessionCard.js +26 -19
- package/dist/components/sessions/SessionCard.js.map +1 -1
- package/dist/components/sessions/SessionStatusBadge.js +7 -7
- package/dist/components/sessions/SessionStatusBadge.js.map +1 -1
- package/dist/components/shared/AgentSelector.js +4 -4
- package/dist/components/shared/AgentSelector.js.map +1 -1
- package/dist/components/shared/AgentSetupBanner.js +4 -4
- package/dist/components/shared/AgentSetupBanner.js.map +1 -1
- package/dist/components/shared/Card.js +4 -4
- package/dist/components/shared/Card.js.map +1 -1
- package/dist/components/shared/ErrorState.js +15 -9
- package/dist/components/shared/ErrorState.js.map +1 -1
- package/dist/components/shared/FilterDropdown.js +2 -2
- package/dist/components/shared/FilterDropdown.js.map +1 -1
- package/dist/components/shared/PageSkeleton.js +6 -6
- package/dist/components/shared/PageSkeleton.js.map +1 -1
- package/dist/components/shared/SearchInput.js +1 -1
- package/dist/components/shared/SearchInput.js.map +1 -1
- package/dist/components/shared/StatCard.js +7 -7
- package/dist/components/shared/StatCard.js.map +1 -1
- package/dist/components/templates/PublishTemplateForm.js +2 -2
- package/dist/components/templates/PublishTemplateForm.js.map +1 -1
- package/dist/components/templates/TemplateCard.js +5 -5
- package/dist/components/templates/TemplateCard.js.map +1 -1
- package/dist/components/templates/TemplateWizard.js +2 -2
- package/dist/components/templates/TemplateWizard.js.map +1 -1
- package/dist/components/tunnels/DomainVerificationBadge.js +4 -4
- package/dist/components/tunnels/DomainVerificationBadge.js.map +1 -1
- package/dist/components/tunnels/ProviderBadge.js +1 -1
- package/dist/components/tunnels/ProviderBadge.js.map +1 -1
- package/dist/components/tunnels/TunnelCard.js +16 -16
- package/dist/components/tunnels/TunnelCard.js.map +1 -1
- package/dist/components/tunnels/TunnelCreateWizard.js +1 -1
- package/dist/components/tunnels/TunnelCreateWizard.js.map +1 -1
- package/dist/components/tunnels/TunnelPublicAccessPanel.js +1 -1
- package/dist/components/tunnels/TunnelPublicAccessPanel.js.map +1 -1
- package/dist/components/tunnels/TunnelStatusBadge.js +4 -4
- package/dist/components/tunnels/TunnelStatusBadge.js.map +1 -1
- package/dist/components/vibecalendar/LogsDrawer.js +2 -2
- package/dist/components/vibecalendar/LogsDrawer.js.map +1 -1
- package/dist/components/vibecalendar/ScheduleTaskModal.js +5 -5
- package/dist/components/vibecalendar/ScheduleTaskModal.js.map +1 -1
- package/dist/components/vibecalendar/VibeCalAgendaView.js +2 -2
- package/dist/components/vibecalendar/VibeCalAgendaView.js.map +1 -1
- package/dist/components/vibecalendar/VibeCalDayView.js +3 -3
- package/dist/components/vibecalendar/VibeCalDayView.js.map +1 -1
- package/dist/components/vibecalendar/VibeCalMonthView.js +1 -1
- package/dist/components/vibecalendar/VibeCalMonthView.js.map +1 -1
- package/dist/components/vibecalendar/VibeCalWeekView.js +4 -4
- package/dist/components/vibecalendar/VibeCalWeekView.js.map +1 -1
- package/dist/components/vibecalendar/VibeCalendarHeader.js +4 -4
- package/dist/components/vibecalendar/VibeCalendarHeader.js.map +1 -1
- package/dist/components/vibecalendar/VibeTaskBlock.js +2 -2
- package/dist/components/vibecalendar/VibeTaskBlock.js.map +1 -1
- package/dist/components/vibecalendar/types.js +7 -7
- package/dist/components/vibecalendar/types.js.map +1 -1
- package/dist/components/vibedeck/VibeDeckButtonForm.js +9 -9
- package/dist/components/vibedeck/VibeDeckButtonForm.js.map +1 -1
- package/dist/components/vibedeck/VibeDeckButtonTile.js +6 -6
- package/dist/components/vibedeck/VibeDeckButtonTile.js.map +1 -1
- package/dist/components/vibedeck/VibeDeckExecutionLog.js +10 -10
- package/dist/components/vibedeck/VibeDeckExecutionLog.js.map +1 -1
- package/dist/components/vibedeck/VibeDeckForm.js +8 -8
- package/dist/components/vibedeck/VibeDeckForm.js.map +1 -1
- package/dist/components/vibedeck/VibeDeckGrid.js +2 -2
- package/dist/components/vibedeck/VibeDeckGrid.js.map +1 -1
- package/dist/components/vibedeck/VibeDeckPublicLinkPanel.js +2 -2
- package/dist/components/vibedeck/VibeDeckPublicLinkPanel.js.map +1 -1
- package/dist/components/vibedeck/VibeDeckSwitcher.js +8 -8
- package/dist/components/vibedeck/VibeDeckSwitcher.js.map +1 -1
- package/dist/components/vibes/VibeCard.js +22 -18
- package/dist/components/vibes/VibeCard.js.map +1 -1
- package/dist/components/vibes/VibeList.js +16 -12
- package/dist/components/vibes/VibeList.js.map +1 -1
- package/dist/pages/ActionsPage.js +4 -3
- package/dist/pages/ActionsPage.js.map +1 -1
- package/dist/pages/AgentDetailsPage.js +9 -9
- package/dist/pages/AgentDetailsPage.js.map +1 -1
- package/dist/pages/AgentsLandingPage.js +4 -4
- package/dist/pages/AgentsLandingPage.js.map +1 -1
- package/dist/pages/AgentsPage.js +14 -14
- package/dist/pages/AgentsPage.js.map +1 -1
- package/dist/pages/AnalyticsPage.js +8 -8
- package/dist/pages/AnalyticsPage.js.map +1 -1
- package/dist/pages/CatalogDetailPage.js +11 -11
- package/dist/pages/CatalogDetailPage.js.map +1 -1
- package/dist/pages/CatalogPage.js +12 -9
- package/dist/pages/CatalogPage.js.map +1 -1
- package/dist/pages/ComponentDetailPage.js +7 -7
- package/dist/pages/ComponentDetailPage.js.map +1 -1
- package/dist/pages/ComponentsPage.js +12 -9
- package/dist/pages/ComponentsPage.js.map +1 -1
- package/dist/pages/EnvTopologyPage.js +2 -2
- package/dist/pages/EnvTopologyPage.js.map +1 -1
- package/dist/pages/GettingStartedPage.js +12 -12
- package/dist/pages/GettingStartedPage.js.map +1 -1
- package/dist/pages/HealthPage.js +12 -12
- package/dist/pages/HealthPage.js.map +1 -1
- package/dist/pages/LogsPage.js +4 -4
- package/dist/pages/LogsPage.js.map +1 -1
- package/dist/pages/NoteDetailsPage.js +5 -5
- package/dist/pages/NoteDetailsPage.js.map +1 -1
- package/dist/pages/OverviewPage.js +76 -43
- package/dist/pages/OverviewPage.js.map +1 -1
- package/dist/pages/PluginsCatalogPage.js +7 -4
- package/dist/pages/PluginsCatalogPage.js.map +1 -1
- package/dist/pages/ScheduledJobDetailPage.js +7 -7
- package/dist/pages/ScheduledJobDetailPage.js.map +1 -1
- package/dist/pages/SessionDetailsPage.js +21 -21
- package/dist/pages/SessionDetailsPage.js.map +1 -1
- package/dist/pages/SessionsPage.js +20 -20
- package/dist/pages/SessionsPage.js.map +1 -1
- package/dist/pages/SettingsPage.js +64 -52
- package/dist/pages/SettingsPage.js.map +1 -1
- package/dist/pages/SharedSessionPage.js +21 -21
- package/dist/pages/SharedSessionPage.js.map +1 -1
- package/dist/pages/SharedVibeDeckPage.js +5 -5
- package/dist/pages/SharedVibeDeckPage.js.map +1 -1
- package/dist/pages/TargetDetailsPage.js +56 -65
- package/dist/pages/TargetDetailsPage.js.map +1 -1
- package/dist/pages/TargetsPage.js +4 -4
- package/dist/pages/TargetsPage.js.map +1 -1
- package/dist/pages/TemplateDetailPage.js +10 -10
- package/dist/pages/TemplateDetailPage.js.map +1 -1
- package/dist/pages/TemplatesPage.js +6 -5
- package/dist/pages/TemplatesPage.js.map +1 -1
- package/dist/pages/VibeAuditPage.js +3 -3
- package/dist/pages/VibeAuditPage.js.map +1 -1
- package/dist/pages/VibeCalendarPage.js +3 -3
- package/dist/pages/VibeCalendarPage.js.map +1 -1
- package/dist/pages/VibeDeckPage.js +3 -3
- package/dist/pages/VibeDeckPage.js.map +1 -1
- package/dist/pages/VibeDetailsPage.js +15 -15
- package/dist/pages/VibeDetailsPage.js.map +1 -1
- package/dist/pages/VibeTaskDetailPage.js +5 -5
- package/dist/pages/VibeTaskDetailPage.js.map +1 -1
- package/dist/pages/VibesPage.js +18 -18
- package/dist/pages/VibesPage.js.map +1 -1
- package/dist/pages/WebhooksPage.js +11 -11
- package/dist/pages/WebhooksPage.js.map +1 -1
- package/dist/pages/ai/AIBookmarksPage.js +1 -1
- package/dist/pages/ai/AIBookmarksPage.js.map +1 -1
- package/dist/pages/ai/AISessionDetailPage.js +6 -6
- package/dist/pages/ai/AISessionDetailPage.js.map +1 -1
- package/dist/pages/tunnels/AuditTab.js +1 -1
- package/dist/pages/tunnels/AuditTab.js.map +1 -1
- package/dist/pages/tunnels/DomainsTab.js +1 -1
- package/dist/pages/tunnels/DomainsTab.js.map +1 -1
- package/dist/pages/tunnels/ProvidersTab.js +1 -1
- package/dist/pages/tunnels/ProvidersTab.js.map +1 -1
- package/dist/pages/tunnels/SessionsTab.js +1 -1
- package/dist/pages/tunnels/SessionsTab.js.map +1 -1
- package/dist/pages/tunnels/TunnelDetailsPage.js +3 -3
- package/dist/pages/tunnels/TunnelDetailsPage.js.map +1 -1
- package/dist/pages/tunnels/UsageTab.js +1 -1
- package/dist/pages/tunnels/UsageTab.js.map +1 -1
- package/dist/pages/tunnels/tabs/TunnelDomainsTab.js +16 -16
- package/dist/pages/tunnels/tabs/TunnelDomainsTab.js.map +1 -1
- package/dist/pages/tunnels/tabs/TunnelOverviewTab.js +1 -1
- package/dist/pages/tunnels/tabs/TunnelOverviewTab.js.map +1 -1
- package/dist/pages/tunnels/tabs/TunnelSessionsTab.js +52 -49
- package/dist/pages/tunnels/tabs/TunnelSessionsTab.js.map +1 -1
- package/dist/pages/tunnels/tabs/TunnelSettingsTab.js +9 -9
- package/dist/pages/tunnels/tabs/TunnelSettingsTab.js.map +1 -1
- package/package.json +1 -1
|
@@ -196,7 +196,7 @@ var b = {
|
|
|
196
196
|
className: "font-medium",
|
|
197
197
|
children: k.name
|
|
198
198
|
}), j && /* @__PURE__ */ v("p", {
|
|
199
|
-
className: "mt-3 rounded-md border border-status-error-border bg-status-error-bg px-3 py-2 text-sm text-status-error-text",
|
|
199
|
+
className: "mt-3 rounded-md border border-status-error-border bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text",
|
|
200
200
|
children: j
|
|
201
201
|
})]
|
|
202
202
|
}),
|
|
@@ -259,7 +259,7 @@ var b = {
|
|
|
259
259
|
className: "space-y-4 overflow-y-auto px-5 py-4",
|
|
260
260
|
children: [
|
|
261
261
|
F && /* @__PURE__ */ v("div", {
|
|
262
|
-
className: "rounded-md border border-status-error-border bg-status-error-bg px-3 py-2 text-sm text-status-error-text",
|
|
262
|
+
className: "rounded-md border border-status-error-border bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text",
|
|
263
263
|
children: F
|
|
264
264
|
}),
|
|
265
265
|
/* @__PURE__ */ y("label", {
|
|
@@ -629,7 +629,7 @@ var b = {
|
|
|
629
629
|
})]
|
|
630
630
|
}),
|
|
631
631
|
s && /* @__PURE__ */ v("p", {
|
|
632
|
-
className: "mt-2 rounded-md border border-status-warning-border bg-status-warning-bg px-2 py-1.5 text-xs text-status-warning-text",
|
|
632
|
+
className: "mt-2 rounded-md border border-status-warning-border bg-status-warning-bg-subtle px-2 py-1.5 text-xs text-status-warning-text",
|
|
633
633
|
children: e.remediation
|
|
634
634
|
})
|
|
635
635
|
]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ScorecardPanel.js","names":[],"sources":["../../../src/components/scorecard/ScorecardPanel.tsx"],"sourcesContent":["import type { FC, FormEvent, MouseEvent } from 'react';\nimport { useState, useCallback, useEffect } from 'react';\nimport { motion, AnimatePresence } from 'framer-motion';\nimport { Plus, TrendingUp, RefreshCw, Pencil, Trash2, Info, ShieldCheck } from 'lucide-react';\nimport { LoadingState, ErrorState, Card } from '@/components/shared';\nimport { EntityShareButton } from '@/components/sharing';\nimport { ScorecardTier } from '@/generated/wspace-types';\nimport { MotionList, MotionItem } from '@burdenoff/fe-libs/shared/motion';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useActiveContext } from '@burdenoff/fe-libs/shared/providers/shell';\nimport {\n useGetScorecardQuery,\n useUpsertScorecardMutation,\n useAddScorecardMetricMutation,\n useUpdateScorecardMetricMutation,\n useRemoveScorecardMetricMutation,\n useApplyScorecardTemplateMutation,\n} from '@/generated/wspace-operations';\nimport {\n type ScorecardMetricType,\n type ScorecardMetricStatus,\n type ScorecardMetricDirection,\n METRIC_TYPE_OPTIONS,\n DIRECTION_OPTIONS,\n STATUS_OPTIONS,\n statusConfig,\n deriveStatus,\n} from './scorecardConstants';\nimport { ScorecardEmptyState } from './ScorecardEmptyState';\nimport { ScorecardsOverviewPanel } from './ScorecardsOverviewPanel';\nimport { MetricTrend } from './MetricTrend';\n\ninterface ScorecardMetric {\n id: string;\n name: string;\n description: string | null;\n type: ScorecardMetricType;\n value: number | null;\n threshold: number | null;\n unit: string | null;\n direction: ScorecardMetricDirection;\n weight: number;\n status: ScorecardMetricStatus;\n statusOverride: ScorecardMetricStatus | null;\n remediation: string | null;\n iframeUrl: string | null;\n iframeHeight: number | null;\n}\n\nexport interface ScorecardPanelProps {\n ownerType: 'CATALOG' | 'COMPONENT' | 'VIBE';\n ownerId: string;\n onAddMetric?: () => void;\n}\n\ntype StatusMode = 'AUTO' | ScorecardMetricStatus;\n\ninterface MetricFormState {\n name: string;\n description: string;\n type: ScorecardMetricType;\n value: string;\n threshold: string;\n unit: string;\n direction: ScorecardMetricDirection;\n weight: string;\n statusMode: StatusMode;\n remediation: string;\n iframeUrl: string;\n iframeHeight: string;\n}\n\nconst DEFAULT_METRIC_FORM: MetricFormState = {\n name: '',\n description: '',\n type: 'CUSTOM',\n value: '',\n threshold: '',\n unit: '',\n direction: 'HIGHER_IS_BETTER',\n weight: '1',\n statusMode: 'AUTO',\n remediation: '',\n iframeUrl: '',\n iframeHeight: '',\n};\n\nconst inputClass =\n 'w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary outline-none focus:border-action-primary-bg';\n\nexport const ScorecardPanel: FC<ScorecardPanelProps> = ({ ownerType, ownerId, onAddMetric }) => {\n const { t } = useI18n();\n const tr = useCallback(\n (key: string, fallback: string): string => {\n const v = t(key);\n return v === key ? fallback : v;\n },\n [t]\n );\n\n const { workspaceId } = useActiveContext();\n\n const [expandedMetric, setExpandedMetric] = useState<string | null>(null);\n const [isAddMetricOpen, setIsAddMetricOpen] = useState(false);\n const [editingMetricId, setEditingMetricId] = useState<string | null>(null);\n const [deletingMetric, setDeletingMetric] = useState<{ id: string; name: string } | null>(null);\n const [deleteError, setDeleteError] = useState<string | null>(null);\n const [metricForm, setMetricForm] = useState<MetricFormState>(DEFAULT_METRIC_FORM);\n const [formError, setFormError] = useState<string | null>(null);\n const [applyingTemplateId, setApplyingTemplateId] = useState<string | null>(null);\n\n const { data, loading, error, refetch } = useGetScorecardQuery({\n variables: { ownerType, ownerId },\n skip: !ownerId,\n fetchPolicy: 'cache-and-network',\n });\n\n const [upsertScorecard, { loading: upsertingScorecard }] = useUpsertScorecardMutation();\n const [addScorecardMetric, { loading: addingMetric }] = useAddScorecardMetricMutation();\n const [updateScorecardMetric, { loading: updatingMetric }] = useUpdateScorecardMetricMutation();\n const [removeScorecardMetric, { loading: removingMetric }] = useRemoveScorecardMetricMutation();\n const [applyTemplate] = useApplyScorecardTemplateMutation();\n\n const scorecard = data?.vibecontrolsScorecard ?? null;\n const isSavingMetric = upsertingScorecard || addingMetric || updatingMetric;\n const isEditing = editingMetricId !== null;\n\n const resetMetricForm = (): void => {\n setMetricForm(DEFAULT_METRIC_FORM);\n setFormError(null);\n setEditingMetricId(null);\n };\n\n const handleAddMetricClick = (): void => {\n if (onAddMetric) {\n onAddMetric();\n return;\n }\n resetMetricForm();\n setIsAddMetricOpen(true);\n };\n\n const handleEditMetricClick = (event: MouseEvent, metric: ScorecardMetric): void => {\n event.stopPropagation();\n setMetricForm({\n name: metric.name,\n description: metric.description ?? '',\n type: metric.type,\n value: metric.value !== null && metric.value !== undefined ? String(metric.value) : '',\n threshold:\n metric.threshold !== null && metric.threshold !== undefined ? String(metric.threshold) : '',\n unit: metric.unit ?? '',\n direction: metric.direction ?? 'HIGHER_IS_BETTER',\n weight: metric.weight !== null && metric.weight !== undefined ? String(metric.weight) : '1',\n statusMode: metric.statusOverride ?? 'AUTO',\n remediation: metric.remediation ?? '',\n iframeUrl: metric.iframeUrl ?? '',\n iframeHeight:\n metric.iframeHeight !== null && metric.iframeHeight !== undefined\n ? String(metric.iframeHeight)\n : '',\n });\n setFormError(null);\n setEditingMetricId(metric.id);\n setIsAddMetricOpen(true);\n };\n\n const handleRemoveMetricClick = (event: MouseEvent, metric: ScorecardMetric): void => {\n event.stopPropagation();\n setDeleteError(null);\n setDeletingMetric({ id: metric.id, name: metric.name });\n };\n\n const closeAddMetricDialog = (): void => {\n if (isSavingMetric) return;\n setIsAddMetricOpen(false);\n resetMetricForm();\n };\n\n const closeDeleteDialog = useCallback((): void => {\n if (removingMetric) return;\n setDeletingMetric(null);\n setDeleteError(null);\n }, [removingMetric]);\n\n useEffect(() => {\n if (!deletingMetric) return undefined;\n const handler = (event: KeyboardEvent): void => {\n if (event.key === 'Escape') closeDeleteDialog();\n };\n window.addEventListener('keydown', handler);\n return () => window.removeEventListener('keydown', handler);\n }, [deletingMetric, closeDeleteDialog]);\n\n const confirmRemoveMetric = async (): Promise<void> => {\n if (!deletingMetric) return;\n setDeleteError(null);\n try {\n await removeScorecardMetric({ variables: { id: deletingMetric.id } });\n await refetch();\n setDeletingMetric(null);\n } catch (caughtError) {\n setDeleteError(\n caughtError instanceof Error ? caughtError.message : 'Failed to remove metric'\n );\n }\n };\n\n const updateMetricForm = (field: keyof MetricFormState, value: string): void => {\n setMetricForm((current) => ({ ...current, [field]: value }));\n };\n\n const parseOptionalNumber = (value: string, label: string): number | undefined => {\n const trimmed = value.trim();\n if (!trimmed) return undefined;\n const parsed = Number(trimmed);\n if (!Number.isFinite(parsed)) throw new Error(`${label} must be a valid number`);\n return parsed;\n };\n\n const ensureScorecardId = async (): Promise<string> => {\n if (scorecard?.id) return scorecard.id;\n const created = await upsertScorecard({ variables: { input: { ownerType, ownerId } } });\n const id = created.data?.upsertVibecontrolsScorecard.id;\n if (!id) throw new Error(tr('scorecard.scorecardCreateFailed', 'Unable to create scorecard'));\n return id;\n };\n\n const handleApplyTemplate = async (templateId: string): Promise<void> => {\n setApplyingTemplateId(templateId);\n try {\n await applyTemplate({ variables: { templateId, ownerType, ownerId } });\n await refetch();\n } finally {\n setApplyingTemplateId(null);\n }\n };\n\n const handleAddMetricSubmit = async (event: FormEvent<HTMLFormElement>): Promise<void> => {\n event.preventDefault();\n setFormError(null);\n\n const name = metricForm.name.trim();\n if (!name) {\n setFormError(tr('scorecard.metricNameRequired', 'Metric name is required'));\n return;\n }\n\n // 'AUTO' → derive status (send no explicit status). Otherwise pin it.\n const statusOverride =\n metricForm.statusMode === 'AUTO'\n ? undefined\n : (metricForm.statusMode as ScorecardMetricStatus);\n\n try {\n const sharedInput = {\n name,\n description: metricForm.description.trim() || null,\n type: metricForm.type,\n value: parseOptionalNumber(metricForm.value, tr('scorecard.value', 'Value')) ?? null,\n threshold:\n parseOptionalNumber(metricForm.threshold, tr('scorecard.threshold', 'Threshold')) ?? null,\n unit: metricForm.unit.trim() || null,\n direction: metricForm.direction,\n weight: parseOptionalNumber(metricForm.weight, tr('scorecard.weight', 'Weight')) ?? 1,\n status: statusOverride ?? null,\n remediation: metricForm.remediation.trim() || null,\n iframeUrl: metricForm.iframeUrl.trim() || null,\n iframeHeight:\n parseOptionalNumber(\n metricForm.iframeHeight,\n tr('scorecard.iframeHeight', 'Iframe height')\n ) ?? null,\n };\n\n if (editingMetricId) {\n await updateScorecardMetric({ variables: { id: editingMetricId, input: sharedInput } });\n } else {\n const scorecardId = await ensureScorecardId();\n await addScorecardMetric({ variables: { scorecardId, input: sharedInput } });\n }\n\n await refetch();\n setIsAddMetricOpen(false);\n resetMetricForm();\n } catch (caughtError) {\n setFormError(\n caughtError instanceof Error\n ? caughtError.message\n : editingMetricId\n ? 'Failed to update metric'\n : 'Failed to add metric'\n );\n }\n };\n\n // Live status preview for the form.\n const previewStatus =\n metricForm.statusMode === 'AUTO'\n ? deriveStatus(\n metricForm.value.trim() ? Number(metricForm.value) : null,\n metricForm.threshold.trim() ? Number(metricForm.threshold) : null,\n metricForm.direction\n )\n : (metricForm.statusMode as ScorecardMetricStatus);\n\n const renderDeleteMetricDialog = () => (\n <AnimatePresence>\n {deletingMetric && (\n <motion.div\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n className=\"fixed inset-0 z-50 flex items-center justify-center bg-bg-overlay p-4\"\n onClick={closeDeleteDialog}\n >\n <motion.div\n initial={{ scale: 0.96, y: 12 }}\n animate={{ scale: 1, y: 0 }}\n exit={{ scale: 0.96, y: 12 }}\n className=\"w-full max-w-md rounded-xl border border-border-default bg-bg-surface shadow-xl\"\n onClick={(event) => event.stopPropagation()}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"scorecard-delete-title\"\n >\n <div className=\"border-b border-border-default px-5 py-4\">\n <h3 id=\"scorecard-delete-title\" className=\"text-lg font-semibold text-text-primary\">\n {tr('scorecard.removeMetricTitle', 'Remove metric')}\n </h3>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {tr(\n 'scorecard.removeMetricConfirm',\n 'Are you sure you want to remove this metric? This cannot be undone.'\n )}\n </p>\n </div>\n <div className=\"px-5 py-4 text-sm text-text-primary\">\n <p className=\"font-medium\">{deletingMetric.name}</p>\n {deleteError && (\n <p className=\"mt-3 rounded-md border border-status-error-border bg-status-error-bg px-3 py-2 text-sm text-status-error-text\">\n {deleteError}\n </p>\n )}\n </div>\n <div className=\"flex items-center justify-end gap-3 border-t border-border-default px-5 py-4\">\n <button\n type=\"button\"\n onClick={closeDeleteDialog}\n disabled={removingMetric}\n className=\"rounded-md border border-border-default px-4 py-2 text-sm text-text-primary hover:bg-bg-sunken disabled:cursor-not-allowed disabled:opacity-60\"\n >\n {tr('common.cancel', 'Cancel')}\n </button>\n <button\n type=\"button\"\n onClick={confirmRemoveMetric}\n disabled={removingMetric}\n className=\"rounded-md bg-status-error-bg px-4 py-2 text-sm text-status-error-text hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-60\"\n >\n {removingMetric\n ? tr('scorecard.removingMetric', 'Removing...')\n : tr('scorecard.removeMetric', 'Remove metric')}\n </button>\n </div>\n </motion.div>\n </motion.div>\n )}\n </AnimatePresence>\n );\n\n const renderAddMetricDialog = () => {\n const previewConf = statusConfig[previewStatus] ?? statusConfig.UNKNOWN;\n return (\n <AnimatePresence>\n {isAddMetricOpen && (\n <motion.div\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n className=\"fixed inset-0 z-50 flex items-center justify-center bg-bg-overlay p-4\"\n onClick={closeAddMetricDialog}\n >\n <motion.div\n initial={{ scale: 0.96, y: 12 }}\n animate={{ scale: 1, y: 0 }}\n exit={{ scale: 0.96, y: 12 }}\n className=\"w-full max-w-2xl rounded-xl border border-border-default bg-bg-surface shadow-xl\"\n onClick={(event) => event.stopPropagation()}\n >\n <form onSubmit={handleAddMetricSubmit} className=\"flex max-h-[85vh] flex-col\">\n <div className=\"border-b border-border-default px-5 py-4\">\n <h3 className=\"text-lg font-semibold text-text-primary\">\n {isEditing\n ? tr('scorecard.editMetricTitle', 'Edit scorecard metric')\n : tr('scorecard.addMetricTitle', 'Add scorecard metric')}\n </h3>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {tr(\n 'scorecard.addMetricDescription',\n 'Define a metric. Status is derived from value vs threshold unless you pin it.'\n )}\n </p>\n </div>\n\n <div className=\"space-y-4 overflow-y-auto px-5 py-4\">\n {formError && (\n <div className=\"rounded-md border border-status-error-border bg-status-error-bg px-3 py-2 text-sm text-status-error-text\">\n {formError}\n </div>\n )}\n\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.metricName', 'Metric name')}\n </span>\n <input\n aria-label=\"control\"\n value={metricForm.name}\n onChange={(event) => updateMetricForm('name', event.target.value)}\n className={inputClass}\n placeholder={tr('scorecard.metricNamePlaceholder', 'Test coverage')}\n />\n </label>\n\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.metricDescription', 'Description')}\n </span>\n <textarea\n aria-label=\"control\"\n value={metricForm.description}\n onChange={(event) => updateMetricForm('description', event.target.value)}\n className={`min-h-16 ${inputClass}`}\n placeholder={tr(\n 'scorecard.metricDescriptionPlaceholder',\n 'Explain what this metric measures'\n )}\n />\n </label>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.metricType', 'Type')}\n </span>\n <select\n value={metricForm.type}\n onChange={(event) =>\n updateMetricForm('type', event.target.value as ScorecardMetricType)\n }\n className={inputClass}\n >\n {METRIC_TYPE_OPTIONS.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n </label>\n\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.metricDirection', 'Direction')}\n </span>\n <select\n value={metricForm.direction}\n onChange={(event) =>\n updateMetricForm(\n 'direction',\n event.target.value as ScorecardMetricDirection\n )\n }\n className={inputClass}\n >\n {DIRECTION_OPTIONS.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n </label>\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-4\">\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.value', 'Value')}\n </span>\n <input\n aria-label=\"value\"\n value={metricForm.value}\n onChange={(event) => updateMetricForm('value', event.target.value)}\n inputMode=\"decimal\"\n className={inputClass}\n placeholder=\"86\"\n />\n </label>\n\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.threshold', 'Threshold')}\n </span>\n <input\n aria-label=\"threshold\"\n value={metricForm.threshold}\n onChange={(event) => updateMetricForm('threshold', event.target.value)}\n inputMode=\"decimal\"\n className={inputClass}\n placeholder=\"80\"\n />\n </label>\n\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.unit', 'Unit')}\n </span>\n <input\n aria-label=\"unit\"\n value={metricForm.unit}\n onChange={(event) => updateMetricForm('unit', event.target.value)}\n className={inputClass}\n placeholder=\"%\"\n />\n </label>\n\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.weight', 'Weight')}\n </span>\n <input\n aria-label=\"weight\"\n value={metricForm.weight}\n onChange={(event) => updateMetricForm('weight', event.target.value)}\n inputMode=\"decimal\"\n className={inputClass}\n placeholder=\"1\"\n />\n </label>\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.metricStatus', 'Status')}\n </span>\n <select\n value={metricForm.statusMode}\n onChange={(event) =>\n updateMetricForm('statusMode', event.target.value as StatusMode)\n }\n className={inputClass}\n >\n {STATUS_OPTIONS.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n </label>\n\n <div className=\"flex items-end\">\n <div\n className={`flex items-center gap-2 rounded-md px-3 py-2 text-sm ${previewConf.bg} ${previewConf.text}`}\n >\n <previewConf.icon className=\"size-4\" />\n <span>\n {metricForm.statusMode === 'AUTO' ? 'Derived: ' : 'Pinned: '}\n {previewConf.label}\n </span>\n </div>\n </div>\n </div>\n\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.remediation', 'Remediation guidance')}\n </span>\n <textarea\n aria-label=\"control\"\n value={metricForm.remediation}\n onChange={(event) => updateMetricForm('remediation', event.target.value)}\n className={`min-h-16 ${inputClass}`}\n placeholder={tr(\n 'scorecard.remediationPlaceholder',\n 'What should someone do when this metric is failing?'\n )}\n />\n </label>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-[1fr_10rem]\">\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.iframeUrl', 'Embed URL')}\n </span>\n <input\n aria-label=\"control\"\n value={metricForm.iframeUrl}\n onChange={(event) => updateMetricForm('iframeUrl', event.target.value)}\n className={inputClass}\n placeholder={tr('scorecard.iframeUrlPlaceholder', 'Optional dashboard URL')}\n />\n </label>\n\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.iframeHeight', 'Iframe height')}\n </span>\n <input\n aria-label=\"iframe height\"\n value={metricForm.iframeHeight}\n onChange={(event) => updateMetricForm('iframeHeight', event.target.value)}\n inputMode=\"numeric\"\n className={inputClass}\n placeholder=\"320\"\n />\n </label>\n </div>\n </div>\n\n <div className=\"flex items-center justify-end gap-3 border-t border-border-default px-5 py-4\">\n <button\n type=\"button\"\n onClick={closeAddMetricDialog}\n disabled={isSavingMetric}\n className=\"rounded-md border border-border-default px-4 py-2 text-sm text-text-primary hover:bg-bg-sunken disabled:cursor-not-allowed disabled:opacity-60\"\n >\n {tr('common.cancel', 'Cancel')}\n </button>\n <button\n type=\"submit\"\n disabled={isSavingMetric}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm text-action-primary-text hover:bg-action-primary-bg-hover disabled:cursor-not-allowed disabled:opacity-60\"\n >\n {isSavingMetric\n ? tr('scorecard.savingMetric', 'Saving...')\n : isEditing\n ? tr('scorecard.updateMetric', 'Update Metric')\n : tr('scorecard.saveMetric', 'Save Metric')}\n </button>\n </div>\n </form>\n </motion.div>\n </motion.div>\n )}\n </AnimatePresence>\n );\n };\n\n if (loading && !scorecard) {\n return <LoadingState message={tr('scorecard.loading', 'Loading scorecard...')} />;\n }\n\n if (error && !scorecard) {\n return (\n <ErrorState\n title={tr('scorecard.failedLoad', 'Failed to load scorecard')}\n message={error.message}\n onRetry={() => refetch()}\n />\n );\n }\n\n if (!scorecard || scorecard.metrics.length === 0) {\n return (\n <>\n <ScorecardEmptyState\n ownerType={ownerType}\n ownerId={ownerId}\n workspaceId={workspaceId}\n applyingTemplateId={applyingTemplateId}\n onApplyTemplate={handleApplyTemplate}\n onAddManual={handleAddMetricClick}\n />\n {renderAddMetricDialog()}\n {renderDeleteMetricDialog()}\n </>\n );\n }\n\n const overallScore = scorecard.overallScore ?? 0;\n const scoreColor =\n overallScore >= 80\n ? 'text-status-success-text'\n : overallScore >= 50\n ? 'text-status-warning-text'\n : 'text-status-error-text';\n\n return (\n <div className=\"space-y-6\">\n {/* Overall Score */}\n <Card padding=\"lg\">\n <div className=\"flex items-center justify-between\">\n <div>\n <p className=\"flex items-center gap-1.5 text-sm font-medium text-text-secondary\">\n {tr('scorecard.overallScore', 'Overall Score')}\n <span title=\"Weighted average of each metric's score (0–100). Status is derived from value vs threshold.\">\n <Info className=\"size-3.5 text-text-muted\" />\n </span>\n </p>\n <p className={`text-4xl font-bold ${scoreColor}`}>\n {overallScore}\n <span className=\"text-lg text-text-muted\">/100</span>\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n {scorecard.securityTier && <TierBadge tier={scorecard.securityTier} />}\n <TrendingUp className={`size-6 ${scoreColor}`} />\n <EntityShareButton\n entity=\"scorecard\"\n resource={{\n id: scorecard.id,\n // Scorecard has no display-name field; use its id as the title.\n name: scorecard.id,\n owner: { ownerId, ownerType },\n }}\n label={tr('scorecard.share', 'Share')}\n />\n <button\n type=\"button\"\n onClick={() => refetch()}\n className=\"rounded-md p-2 text-text-secondary transition-colors hover:bg-bg-sunken\"\n title={tr('common.refresh', 'Refresh')}\n >\n <RefreshCw className=\"size-4\" />\n </button>\n <button\n type=\"button\"\n onClick={handleAddMetricClick}\n className=\"flex items-center gap-2 rounded-md bg-action-primary-bg px-3 py-2 text-sm text-action-primary-text transition-colors hover:bg-action-primary-bg-hover\"\n >\n <Plus className=\"size-4\" />\n {tr('scorecard.addMetric', 'Add Metric')}\n </button>\n </div>\n </div>\n </Card>\n\n {/* Metrics Grid */}\n <MotionList stagger={0.04} className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {scorecard.metrics.map((metric: ScorecardMetric) => {\n const config = statusConfig[metric.status] || statusConfig.UNKNOWN;\n const StatusIcon = config.icon;\n const value = metric.value ?? 0;\n const threshold = metric.threshold ?? 100;\n const percentage =\n threshold > 0 ? Math.min(100, Math.max(0, (value / threshold) * 100)) : 0;\n const isExpanded = expandedMetric === metric.id;\n const showRemediation =\n metric.remediation && (metric.status === 'WARNING' || metric.status === 'FAILING');\n\n return (\n <MotionItem key={metric.id}>\n <Card hoverable padding=\"none\">\n <div\n role=\"button\"\n tabIndex={0}\n onClick={() => setExpandedMetric(isExpanded ? null : metric.id)}\n onKeyDown={(event) => {\n if (event.target !== event.currentTarget) return;\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n setExpandedMetric(isExpanded ? null : metric.id);\n }\n }}\n className=\"cursor-pointer rounded-lg p-4 focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n aria-expanded={isExpanded}\n aria-label={`${metric.name} metric details`}\n >\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {metric.name}\n </p>\n <p className=\"mt-0.5 flex items-center gap-1.5 text-xs text-text-muted\">\n <span>\n {metric.direction === 'LOWER_IS_BETTER'\n ? '↓ lower better'\n : '↑ higher better'}\n </span>\n {metric.weight && metric.weight !== 1 ? (\n <span>· ×{metric.weight}</span>\n ) : null}\n {metric.statusOverride ? <span>· pinned</span> : null}\n </p>\n </div>\n <div className=\"flex flex-shrink-0 items-center gap-1\">\n <button\n type=\"button\"\n onClick={(event) => handleEditMetricClick(event, metric)}\n className=\"rounded-md p-1 text-text-secondary hover:bg-bg-sunken hover:text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n aria-label={tr('scorecard.editMetric', 'Edit metric')}\n title={tr('scorecard.editMetric', 'Edit metric')}\n >\n <Pencil className=\"size-4\" />\n </button>\n <button\n type=\"button\"\n onClick={(event) => handleRemoveMetricClick(event, metric)}\n className=\"rounded-md p-1 text-text-secondary hover:bg-status-error-bg hover:text-status-error-text focus:outline-none focus:ring-2 focus:ring-status-error-text\"\n aria-label={tr('scorecard.removeMetric', 'Remove metric')}\n title={tr('scorecard.removeMetric', 'Remove metric')}\n >\n <Trash2 className=\"size-4\" />\n </button>\n <div\n className={`flex size-7 items-center justify-center rounded-full ${config.bg}`}\n >\n <StatusIcon className={`size-4 ${config.text}`} />\n </div>\n </div>\n </div>\n\n {/* Value and progress */}\n <div className=\"mt-3\">\n <div className=\"flex items-baseline justify-between\">\n <span className=\"text-lg font-bold text-text-primary\">\n {value}\n {metric.unit ? ` ${metric.unit}` : ''}\n </span>\n <span className=\"text-xs text-text-muted\">\n / {threshold}\n {metric.unit ? ` ${metric.unit}` : ''}\n </span>\n </div>\n <div className=\"mt-1.5 h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <motion.div\n initial={{ width: 0 }}\n animate={{ width: `${percentage}%` }}\n transition={{ duration: 0.5, ease: 'easeOut' }}\n className={`h-full rounded-full ${config.bar}`}\n />\n </div>\n </div>\n\n {showRemediation && (\n <p className=\"mt-2 rounded-md border border-status-warning-border bg-status-warning-bg px-2 py-1.5 text-xs text-status-warning-text\">\n {metric.remediation}\n </p>\n )}\n </div>\n\n {/* Expanded: trend + optional embed */}\n <AnimatePresence>\n {isExpanded && (\n <motion.div\n initial={{ height: 0, opacity: 0 }}\n animate={{ height: 'auto', opacity: 1 }}\n exit={{ height: 0, opacity: 0 }}\n transition={{ duration: 0.2 }}\n className=\"overflow-hidden border-t border-border-default\"\n >\n <div className=\"px-4 py-3\">\n <MetricTrend metricId={metric.id} colorClass={config.text} />\n </div>\n {metric.iframeUrl && (\n <iframe\n src={metric.iframeUrl}\n className=\"w-full border-0\"\n style={{\n height: metric.iframeHeight ? `${metric.iframeHeight}px` : '200px',\n }}\n title={`${metric.name} embed`}\n sandbox=\"allow-scripts allow-same-origin\"\n />\n )}\n </motion.div>\n )}\n </AnimatePresence>\n </Card>\n </MotionItem>\n );\n })}\n </MotionList>\n\n <ScorecardsOverviewPanel ownerType={ownerType} highlightOwnerId={ownerId} />\n\n {renderAddMetricDialog()}\n {renderDeleteMetricDialog()}\n </div>\n );\n};\n\nconst TIER_STYLE: Record<ScorecardTier, { label: string; cls: string }> = {\n [ScorecardTier.Bronze]: {\n label: 'Bronze',\n cls: 'bg-status-warning-bg-subtle text-status-warning-text border border-status-warning-border',\n },\n [ScorecardTier.Silver]: {\n label: 'Silver',\n cls: 'bg-bg-sunken text-text-secondary border border-border-strong',\n },\n [ScorecardTier.Gold]: {\n label: 'Gold',\n cls: 'bg-status-success-bg-subtle text-status-success-text border border-status-success-border',\n },\n};\n\n/**\n * TierBadge — shows the scorecard's derived production-readiness tier (Bronze/Silver/Gold).\n * The tier is computed server-side from the tier-tagged metrics; this is read-only.\n */\nconst TierBadge: FC<{ tier: ScorecardTier }> = ({ tier }) => {\n const style = TIER_STYLE[tier];\n return (\n <span\n className={`inline-flex items-center gap-1 rounded-full px-2.5 py-1 text-xs font-semibold ${style.cls}`}\n title={`Security readiness tier: ${style.label}`}\n >\n <ShieldCheck className=\"size-3.5\" />\n {style.label}\n </span>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ScorecardPanel.js","names":[],"sources":["../../../src/components/scorecard/ScorecardPanel.tsx"],"sourcesContent":["import type { FC, FormEvent, MouseEvent } from 'react';\nimport { useState, useCallback, useEffect } from 'react';\nimport { motion, AnimatePresence } from 'framer-motion';\nimport { Plus, TrendingUp, RefreshCw, Pencil, Trash2, Info, ShieldCheck } from 'lucide-react';\nimport { LoadingState, ErrorState, Card } from '@/components/shared';\nimport { EntityShareButton } from '@/components/sharing';\nimport { ScorecardTier } from '@/generated/wspace-types';\nimport { MotionList, MotionItem } from '@burdenoff/fe-libs/shared/motion';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useActiveContext } from '@burdenoff/fe-libs/shared/providers/shell';\nimport {\n useGetScorecardQuery,\n useUpsertScorecardMutation,\n useAddScorecardMetricMutation,\n useUpdateScorecardMetricMutation,\n useRemoveScorecardMetricMutation,\n useApplyScorecardTemplateMutation,\n} from '@/generated/wspace-operations';\nimport {\n type ScorecardMetricType,\n type ScorecardMetricStatus,\n type ScorecardMetricDirection,\n METRIC_TYPE_OPTIONS,\n DIRECTION_OPTIONS,\n STATUS_OPTIONS,\n statusConfig,\n deriveStatus,\n} from './scorecardConstants';\nimport { ScorecardEmptyState } from './ScorecardEmptyState';\nimport { ScorecardsOverviewPanel } from './ScorecardsOverviewPanel';\nimport { MetricTrend } from './MetricTrend';\n\ninterface ScorecardMetric {\n id: string;\n name: string;\n description: string | null;\n type: ScorecardMetricType;\n value: number | null;\n threshold: number | null;\n unit: string | null;\n direction: ScorecardMetricDirection;\n weight: number;\n status: ScorecardMetricStatus;\n statusOverride: ScorecardMetricStatus | null;\n remediation: string | null;\n iframeUrl: string | null;\n iframeHeight: number | null;\n}\n\nexport interface ScorecardPanelProps {\n ownerType: 'CATALOG' | 'COMPONENT' | 'VIBE';\n ownerId: string;\n onAddMetric?: () => void;\n}\n\ntype StatusMode = 'AUTO' | ScorecardMetricStatus;\n\ninterface MetricFormState {\n name: string;\n description: string;\n type: ScorecardMetricType;\n value: string;\n threshold: string;\n unit: string;\n direction: ScorecardMetricDirection;\n weight: string;\n statusMode: StatusMode;\n remediation: string;\n iframeUrl: string;\n iframeHeight: string;\n}\n\nconst DEFAULT_METRIC_FORM: MetricFormState = {\n name: '',\n description: '',\n type: 'CUSTOM',\n value: '',\n threshold: '',\n unit: '',\n direction: 'HIGHER_IS_BETTER',\n weight: '1',\n statusMode: 'AUTO',\n remediation: '',\n iframeUrl: '',\n iframeHeight: '',\n};\n\nconst inputClass =\n 'w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary outline-none focus:border-action-primary-bg';\n\nexport const ScorecardPanel: FC<ScorecardPanelProps> = ({ ownerType, ownerId, onAddMetric }) => {\n const { t } = useI18n();\n const tr = useCallback(\n (key: string, fallback: string): string => {\n const v = t(key);\n return v === key ? fallback : v;\n },\n [t]\n );\n\n const { workspaceId } = useActiveContext();\n\n const [expandedMetric, setExpandedMetric] = useState<string | null>(null);\n const [isAddMetricOpen, setIsAddMetricOpen] = useState(false);\n const [editingMetricId, setEditingMetricId] = useState<string | null>(null);\n const [deletingMetric, setDeletingMetric] = useState<{ id: string; name: string } | null>(null);\n const [deleteError, setDeleteError] = useState<string | null>(null);\n const [metricForm, setMetricForm] = useState<MetricFormState>(DEFAULT_METRIC_FORM);\n const [formError, setFormError] = useState<string | null>(null);\n const [applyingTemplateId, setApplyingTemplateId] = useState<string | null>(null);\n\n const { data, loading, error, refetch } = useGetScorecardQuery({\n variables: { ownerType, ownerId },\n skip: !ownerId,\n fetchPolicy: 'cache-and-network',\n });\n\n const [upsertScorecard, { loading: upsertingScorecard }] = useUpsertScorecardMutation();\n const [addScorecardMetric, { loading: addingMetric }] = useAddScorecardMetricMutation();\n const [updateScorecardMetric, { loading: updatingMetric }] = useUpdateScorecardMetricMutation();\n const [removeScorecardMetric, { loading: removingMetric }] = useRemoveScorecardMetricMutation();\n const [applyTemplate] = useApplyScorecardTemplateMutation();\n\n const scorecard = data?.vibecontrolsScorecard ?? null;\n const isSavingMetric = upsertingScorecard || addingMetric || updatingMetric;\n const isEditing = editingMetricId !== null;\n\n const resetMetricForm = (): void => {\n setMetricForm(DEFAULT_METRIC_FORM);\n setFormError(null);\n setEditingMetricId(null);\n };\n\n const handleAddMetricClick = (): void => {\n if (onAddMetric) {\n onAddMetric();\n return;\n }\n resetMetricForm();\n setIsAddMetricOpen(true);\n };\n\n const handleEditMetricClick = (event: MouseEvent, metric: ScorecardMetric): void => {\n event.stopPropagation();\n setMetricForm({\n name: metric.name,\n description: metric.description ?? '',\n type: metric.type,\n value: metric.value !== null && metric.value !== undefined ? String(metric.value) : '',\n threshold:\n metric.threshold !== null && metric.threshold !== undefined ? String(metric.threshold) : '',\n unit: metric.unit ?? '',\n direction: metric.direction ?? 'HIGHER_IS_BETTER',\n weight: metric.weight !== null && metric.weight !== undefined ? String(metric.weight) : '1',\n statusMode: metric.statusOverride ?? 'AUTO',\n remediation: metric.remediation ?? '',\n iframeUrl: metric.iframeUrl ?? '',\n iframeHeight:\n metric.iframeHeight !== null && metric.iframeHeight !== undefined\n ? String(metric.iframeHeight)\n : '',\n });\n setFormError(null);\n setEditingMetricId(metric.id);\n setIsAddMetricOpen(true);\n };\n\n const handleRemoveMetricClick = (event: MouseEvent, metric: ScorecardMetric): void => {\n event.stopPropagation();\n setDeleteError(null);\n setDeletingMetric({ id: metric.id, name: metric.name });\n };\n\n const closeAddMetricDialog = (): void => {\n if (isSavingMetric) return;\n setIsAddMetricOpen(false);\n resetMetricForm();\n };\n\n const closeDeleteDialog = useCallback((): void => {\n if (removingMetric) return;\n setDeletingMetric(null);\n setDeleteError(null);\n }, [removingMetric]);\n\n useEffect(() => {\n if (!deletingMetric) return undefined;\n const handler = (event: KeyboardEvent): void => {\n if (event.key === 'Escape') closeDeleteDialog();\n };\n window.addEventListener('keydown', handler);\n return () => window.removeEventListener('keydown', handler);\n }, [deletingMetric, closeDeleteDialog]);\n\n const confirmRemoveMetric = async (): Promise<void> => {\n if (!deletingMetric) return;\n setDeleteError(null);\n try {\n await removeScorecardMetric({ variables: { id: deletingMetric.id } });\n await refetch();\n setDeletingMetric(null);\n } catch (caughtError) {\n setDeleteError(\n caughtError instanceof Error ? caughtError.message : 'Failed to remove metric'\n );\n }\n };\n\n const updateMetricForm = (field: keyof MetricFormState, value: string): void => {\n setMetricForm((current) => ({ ...current, [field]: value }));\n };\n\n const parseOptionalNumber = (value: string, label: string): number | undefined => {\n const trimmed = value.trim();\n if (!trimmed) return undefined;\n const parsed = Number(trimmed);\n if (!Number.isFinite(parsed)) throw new Error(`${label} must be a valid number`);\n return parsed;\n };\n\n const ensureScorecardId = async (): Promise<string> => {\n if (scorecard?.id) return scorecard.id;\n const created = await upsertScorecard({ variables: { input: { ownerType, ownerId } } });\n const id = created.data?.upsertVibecontrolsScorecard.id;\n if (!id) throw new Error(tr('scorecard.scorecardCreateFailed', 'Unable to create scorecard'));\n return id;\n };\n\n const handleApplyTemplate = async (templateId: string): Promise<void> => {\n setApplyingTemplateId(templateId);\n try {\n await applyTemplate({ variables: { templateId, ownerType, ownerId } });\n await refetch();\n } finally {\n setApplyingTemplateId(null);\n }\n };\n\n const handleAddMetricSubmit = async (event: FormEvent<HTMLFormElement>): Promise<void> => {\n event.preventDefault();\n setFormError(null);\n\n const name = metricForm.name.trim();\n if (!name) {\n setFormError(tr('scorecard.metricNameRequired', 'Metric name is required'));\n return;\n }\n\n // 'AUTO' → derive status (send no explicit status). Otherwise pin it.\n const statusOverride =\n metricForm.statusMode === 'AUTO'\n ? undefined\n : (metricForm.statusMode as ScorecardMetricStatus);\n\n try {\n const sharedInput = {\n name,\n description: metricForm.description.trim() || null,\n type: metricForm.type,\n value: parseOptionalNumber(metricForm.value, tr('scorecard.value', 'Value')) ?? null,\n threshold:\n parseOptionalNumber(metricForm.threshold, tr('scorecard.threshold', 'Threshold')) ?? null,\n unit: metricForm.unit.trim() || null,\n direction: metricForm.direction,\n weight: parseOptionalNumber(metricForm.weight, tr('scorecard.weight', 'Weight')) ?? 1,\n status: statusOverride ?? null,\n remediation: metricForm.remediation.trim() || null,\n iframeUrl: metricForm.iframeUrl.trim() || null,\n iframeHeight:\n parseOptionalNumber(\n metricForm.iframeHeight,\n tr('scorecard.iframeHeight', 'Iframe height')\n ) ?? null,\n };\n\n if (editingMetricId) {\n await updateScorecardMetric({ variables: { id: editingMetricId, input: sharedInput } });\n } else {\n const scorecardId = await ensureScorecardId();\n await addScorecardMetric({ variables: { scorecardId, input: sharedInput } });\n }\n\n await refetch();\n setIsAddMetricOpen(false);\n resetMetricForm();\n } catch (caughtError) {\n setFormError(\n caughtError instanceof Error\n ? caughtError.message\n : editingMetricId\n ? 'Failed to update metric'\n : 'Failed to add metric'\n );\n }\n };\n\n // Live status preview for the form.\n const previewStatus =\n metricForm.statusMode === 'AUTO'\n ? deriveStatus(\n metricForm.value.trim() ? Number(metricForm.value) : null,\n metricForm.threshold.trim() ? Number(metricForm.threshold) : null,\n metricForm.direction\n )\n : (metricForm.statusMode as ScorecardMetricStatus);\n\n const renderDeleteMetricDialog = () => (\n <AnimatePresence>\n {deletingMetric && (\n <motion.div\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n className=\"fixed inset-0 z-50 flex items-center justify-center bg-bg-overlay p-4\"\n onClick={closeDeleteDialog}\n >\n <motion.div\n initial={{ scale: 0.96, y: 12 }}\n animate={{ scale: 1, y: 0 }}\n exit={{ scale: 0.96, y: 12 }}\n className=\"w-full max-w-md rounded-xl border border-border-default bg-bg-surface shadow-xl\"\n onClick={(event) => event.stopPropagation()}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"scorecard-delete-title\"\n >\n <div className=\"border-b border-border-default px-5 py-4\">\n <h3 id=\"scorecard-delete-title\" className=\"text-lg font-semibold text-text-primary\">\n {tr('scorecard.removeMetricTitle', 'Remove metric')}\n </h3>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {tr(\n 'scorecard.removeMetricConfirm',\n 'Are you sure you want to remove this metric? This cannot be undone.'\n )}\n </p>\n </div>\n <div className=\"px-5 py-4 text-sm text-text-primary\">\n <p className=\"font-medium\">{deletingMetric.name}</p>\n {deleteError && (\n <p className=\"mt-3 rounded-md border border-status-error-border bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {deleteError}\n </p>\n )}\n </div>\n <div className=\"flex items-center justify-end gap-3 border-t border-border-default px-5 py-4\">\n <button\n type=\"button\"\n onClick={closeDeleteDialog}\n disabled={removingMetric}\n className=\"rounded-md border border-border-default px-4 py-2 text-sm text-text-primary hover:bg-bg-sunken disabled:cursor-not-allowed disabled:opacity-60\"\n >\n {tr('common.cancel', 'Cancel')}\n </button>\n <button\n type=\"button\"\n onClick={confirmRemoveMetric}\n disabled={removingMetric}\n className=\"rounded-md bg-status-error-bg px-4 py-2 text-sm text-status-error-text hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-60\"\n >\n {removingMetric\n ? tr('scorecard.removingMetric', 'Removing...')\n : tr('scorecard.removeMetric', 'Remove metric')}\n </button>\n </div>\n </motion.div>\n </motion.div>\n )}\n </AnimatePresence>\n );\n\n const renderAddMetricDialog = () => {\n const previewConf = statusConfig[previewStatus] ?? statusConfig.UNKNOWN;\n return (\n <AnimatePresence>\n {isAddMetricOpen && (\n <motion.div\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n className=\"fixed inset-0 z-50 flex items-center justify-center bg-bg-overlay p-4\"\n onClick={closeAddMetricDialog}\n >\n <motion.div\n initial={{ scale: 0.96, y: 12 }}\n animate={{ scale: 1, y: 0 }}\n exit={{ scale: 0.96, y: 12 }}\n className=\"w-full max-w-2xl rounded-xl border border-border-default bg-bg-surface shadow-xl\"\n onClick={(event) => event.stopPropagation()}\n >\n <form onSubmit={handleAddMetricSubmit} className=\"flex max-h-[85vh] flex-col\">\n <div className=\"border-b border-border-default px-5 py-4\">\n <h3 className=\"text-lg font-semibold text-text-primary\">\n {isEditing\n ? tr('scorecard.editMetricTitle', 'Edit scorecard metric')\n : tr('scorecard.addMetricTitle', 'Add scorecard metric')}\n </h3>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {tr(\n 'scorecard.addMetricDescription',\n 'Define a metric. Status is derived from value vs threshold unless you pin it.'\n )}\n </p>\n </div>\n\n <div className=\"space-y-4 overflow-y-auto px-5 py-4\">\n {formError && (\n <div className=\"rounded-md border border-status-error-border bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {formError}\n </div>\n )}\n\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.metricName', 'Metric name')}\n </span>\n <input\n aria-label=\"control\"\n value={metricForm.name}\n onChange={(event) => updateMetricForm('name', event.target.value)}\n className={inputClass}\n placeholder={tr('scorecard.metricNamePlaceholder', 'Test coverage')}\n />\n </label>\n\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.metricDescription', 'Description')}\n </span>\n <textarea\n aria-label=\"control\"\n value={metricForm.description}\n onChange={(event) => updateMetricForm('description', event.target.value)}\n className={`min-h-16 ${inputClass}`}\n placeholder={tr(\n 'scorecard.metricDescriptionPlaceholder',\n 'Explain what this metric measures'\n )}\n />\n </label>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.metricType', 'Type')}\n </span>\n <select\n value={metricForm.type}\n onChange={(event) =>\n updateMetricForm('type', event.target.value as ScorecardMetricType)\n }\n className={inputClass}\n >\n {METRIC_TYPE_OPTIONS.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n </label>\n\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.metricDirection', 'Direction')}\n </span>\n <select\n value={metricForm.direction}\n onChange={(event) =>\n updateMetricForm(\n 'direction',\n event.target.value as ScorecardMetricDirection\n )\n }\n className={inputClass}\n >\n {DIRECTION_OPTIONS.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n </label>\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-4\">\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.value', 'Value')}\n </span>\n <input\n aria-label=\"value\"\n value={metricForm.value}\n onChange={(event) => updateMetricForm('value', event.target.value)}\n inputMode=\"decimal\"\n className={inputClass}\n placeholder=\"86\"\n />\n </label>\n\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.threshold', 'Threshold')}\n </span>\n <input\n aria-label=\"threshold\"\n value={metricForm.threshold}\n onChange={(event) => updateMetricForm('threshold', event.target.value)}\n inputMode=\"decimal\"\n className={inputClass}\n placeholder=\"80\"\n />\n </label>\n\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.unit', 'Unit')}\n </span>\n <input\n aria-label=\"unit\"\n value={metricForm.unit}\n onChange={(event) => updateMetricForm('unit', event.target.value)}\n className={inputClass}\n placeholder=\"%\"\n />\n </label>\n\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.weight', 'Weight')}\n </span>\n <input\n aria-label=\"weight\"\n value={metricForm.weight}\n onChange={(event) => updateMetricForm('weight', event.target.value)}\n inputMode=\"decimal\"\n className={inputClass}\n placeholder=\"1\"\n />\n </label>\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.metricStatus', 'Status')}\n </span>\n <select\n value={metricForm.statusMode}\n onChange={(event) =>\n updateMetricForm('statusMode', event.target.value as StatusMode)\n }\n className={inputClass}\n >\n {STATUS_OPTIONS.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n </label>\n\n <div className=\"flex items-end\">\n <div\n className={`flex items-center gap-2 rounded-md px-3 py-2 text-sm ${previewConf.bg} ${previewConf.text}`}\n >\n <previewConf.icon className=\"size-4\" />\n <span>\n {metricForm.statusMode === 'AUTO' ? 'Derived: ' : 'Pinned: '}\n {previewConf.label}\n </span>\n </div>\n </div>\n </div>\n\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.remediation', 'Remediation guidance')}\n </span>\n <textarea\n aria-label=\"control\"\n value={metricForm.remediation}\n onChange={(event) => updateMetricForm('remediation', event.target.value)}\n className={`min-h-16 ${inputClass}`}\n placeholder={tr(\n 'scorecard.remediationPlaceholder',\n 'What should someone do when this metric is failing?'\n )}\n />\n </label>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-[1fr_10rem]\">\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.iframeUrl', 'Embed URL')}\n </span>\n <input\n aria-label=\"control\"\n value={metricForm.iframeUrl}\n onChange={(event) => updateMetricForm('iframeUrl', event.target.value)}\n className={inputClass}\n placeholder={tr('scorecard.iframeUrlPlaceholder', 'Optional dashboard URL')}\n />\n </label>\n\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.iframeHeight', 'Iframe height')}\n </span>\n <input\n aria-label=\"iframe height\"\n value={metricForm.iframeHeight}\n onChange={(event) => updateMetricForm('iframeHeight', event.target.value)}\n inputMode=\"numeric\"\n className={inputClass}\n placeholder=\"320\"\n />\n </label>\n </div>\n </div>\n\n <div className=\"flex items-center justify-end gap-3 border-t border-border-default px-5 py-4\">\n <button\n type=\"button\"\n onClick={closeAddMetricDialog}\n disabled={isSavingMetric}\n className=\"rounded-md border border-border-default px-4 py-2 text-sm text-text-primary hover:bg-bg-sunken disabled:cursor-not-allowed disabled:opacity-60\"\n >\n {tr('common.cancel', 'Cancel')}\n </button>\n <button\n type=\"submit\"\n disabled={isSavingMetric}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm text-action-primary-text hover:bg-action-primary-bg-hover disabled:cursor-not-allowed disabled:opacity-60\"\n >\n {isSavingMetric\n ? tr('scorecard.savingMetric', 'Saving...')\n : isEditing\n ? tr('scorecard.updateMetric', 'Update Metric')\n : tr('scorecard.saveMetric', 'Save Metric')}\n </button>\n </div>\n </form>\n </motion.div>\n </motion.div>\n )}\n </AnimatePresence>\n );\n };\n\n if (loading && !scorecard) {\n return <LoadingState message={tr('scorecard.loading', 'Loading scorecard...')} />;\n }\n\n if (error && !scorecard) {\n return (\n <ErrorState\n title={tr('scorecard.failedLoad', 'Failed to load scorecard')}\n message={error.message}\n onRetry={() => refetch()}\n />\n );\n }\n\n if (!scorecard || scorecard.metrics.length === 0) {\n return (\n <>\n <ScorecardEmptyState\n ownerType={ownerType}\n ownerId={ownerId}\n workspaceId={workspaceId}\n applyingTemplateId={applyingTemplateId}\n onApplyTemplate={handleApplyTemplate}\n onAddManual={handleAddMetricClick}\n />\n {renderAddMetricDialog()}\n {renderDeleteMetricDialog()}\n </>\n );\n }\n\n const overallScore = scorecard.overallScore ?? 0;\n const scoreColor =\n overallScore >= 80\n ? 'text-status-success-text'\n : overallScore >= 50\n ? 'text-status-warning-text'\n : 'text-status-error-text';\n\n return (\n <div className=\"space-y-6\">\n {/* Overall Score */}\n <Card padding=\"lg\">\n <div className=\"flex items-center justify-between\">\n <div>\n <p className=\"flex items-center gap-1.5 text-sm font-medium text-text-secondary\">\n {tr('scorecard.overallScore', 'Overall Score')}\n <span title=\"Weighted average of each metric's score (0–100). Status is derived from value vs threshold.\">\n <Info className=\"size-3.5 text-text-muted\" />\n </span>\n </p>\n <p className={`text-4xl font-bold ${scoreColor}`}>\n {overallScore}\n <span className=\"text-lg text-text-muted\">/100</span>\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n {scorecard.securityTier && <TierBadge tier={scorecard.securityTier} />}\n <TrendingUp className={`size-6 ${scoreColor}`} />\n <EntityShareButton\n entity=\"scorecard\"\n resource={{\n id: scorecard.id,\n // Scorecard has no display-name field; use its id as the title.\n name: scorecard.id,\n owner: { ownerId, ownerType },\n }}\n label={tr('scorecard.share', 'Share')}\n />\n <button\n type=\"button\"\n onClick={() => refetch()}\n className=\"rounded-md p-2 text-text-secondary transition-colors hover:bg-bg-sunken\"\n title={tr('common.refresh', 'Refresh')}\n >\n <RefreshCw className=\"size-4\" />\n </button>\n <button\n type=\"button\"\n onClick={handleAddMetricClick}\n className=\"flex items-center gap-2 rounded-md bg-action-primary-bg px-3 py-2 text-sm text-action-primary-text transition-colors hover:bg-action-primary-bg-hover\"\n >\n <Plus className=\"size-4\" />\n {tr('scorecard.addMetric', 'Add Metric')}\n </button>\n </div>\n </div>\n </Card>\n\n {/* Metrics Grid */}\n <MotionList stagger={0.04} className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {scorecard.metrics.map((metric: ScorecardMetric) => {\n const config = statusConfig[metric.status] || statusConfig.UNKNOWN;\n const StatusIcon = config.icon;\n const value = metric.value ?? 0;\n const threshold = metric.threshold ?? 100;\n const percentage =\n threshold > 0 ? Math.min(100, Math.max(0, (value / threshold) * 100)) : 0;\n const isExpanded = expandedMetric === metric.id;\n const showRemediation =\n metric.remediation && (metric.status === 'WARNING' || metric.status === 'FAILING');\n\n return (\n <MotionItem key={metric.id}>\n <Card hoverable padding=\"none\">\n <div\n role=\"button\"\n tabIndex={0}\n onClick={() => setExpandedMetric(isExpanded ? null : metric.id)}\n onKeyDown={(event) => {\n if (event.target !== event.currentTarget) return;\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n setExpandedMetric(isExpanded ? null : metric.id);\n }\n }}\n className=\"cursor-pointer rounded-lg p-4 focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n aria-expanded={isExpanded}\n aria-label={`${metric.name} metric details`}\n >\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {metric.name}\n </p>\n <p className=\"mt-0.5 flex items-center gap-1.5 text-xs text-text-muted\">\n <span>\n {metric.direction === 'LOWER_IS_BETTER'\n ? '↓ lower better'\n : '↑ higher better'}\n </span>\n {metric.weight && metric.weight !== 1 ? (\n <span>· ×{metric.weight}</span>\n ) : null}\n {metric.statusOverride ? <span>· pinned</span> : null}\n </p>\n </div>\n <div className=\"flex flex-shrink-0 items-center gap-1\">\n <button\n type=\"button\"\n onClick={(event) => handleEditMetricClick(event, metric)}\n className=\"rounded-md p-1 text-text-secondary hover:bg-bg-sunken hover:text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n aria-label={tr('scorecard.editMetric', 'Edit metric')}\n title={tr('scorecard.editMetric', 'Edit metric')}\n >\n <Pencil className=\"size-4\" />\n </button>\n <button\n type=\"button\"\n onClick={(event) => handleRemoveMetricClick(event, metric)}\n className=\"rounded-md p-1 text-text-secondary hover:bg-status-error-bg hover:text-status-error-text focus:outline-none focus:ring-2 focus:ring-status-error-text\"\n aria-label={tr('scorecard.removeMetric', 'Remove metric')}\n title={tr('scorecard.removeMetric', 'Remove metric')}\n >\n <Trash2 className=\"size-4\" />\n </button>\n <div\n className={`flex size-7 items-center justify-center rounded-full ${config.bg}`}\n >\n <StatusIcon className={`size-4 ${config.text}`} />\n </div>\n </div>\n </div>\n\n {/* Value and progress */}\n <div className=\"mt-3\">\n <div className=\"flex items-baseline justify-between\">\n <span className=\"text-lg font-bold text-text-primary\">\n {value}\n {metric.unit ? ` ${metric.unit}` : ''}\n </span>\n <span className=\"text-xs text-text-muted\">\n / {threshold}\n {metric.unit ? ` ${metric.unit}` : ''}\n </span>\n </div>\n <div className=\"mt-1.5 h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <motion.div\n initial={{ width: 0 }}\n animate={{ width: `${percentage}%` }}\n transition={{ duration: 0.5, ease: 'easeOut' }}\n className={`h-full rounded-full ${config.bar}`}\n />\n </div>\n </div>\n\n {showRemediation && (\n <p className=\"mt-2 rounded-md border border-status-warning-border bg-status-warning-bg-subtle px-2 py-1.5 text-xs text-status-warning-text\">\n {metric.remediation}\n </p>\n )}\n </div>\n\n {/* Expanded: trend + optional embed */}\n <AnimatePresence>\n {isExpanded && (\n <motion.div\n initial={{ height: 0, opacity: 0 }}\n animate={{ height: 'auto', opacity: 1 }}\n exit={{ height: 0, opacity: 0 }}\n transition={{ duration: 0.2 }}\n className=\"overflow-hidden border-t border-border-default\"\n >\n <div className=\"px-4 py-3\">\n <MetricTrend metricId={metric.id} colorClass={config.text} />\n </div>\n {metric.iframeUrl && (\n <iframe\n src={metric.iframeUrl}\n className=\"w-full border-0\"\n style={{\n height: metric.iframeHeight ? `${metric.iframeHeight}px` : '200px',\n }}\n title={`${metric.name} embed`}\n sandbox=\"allow-scripts allow-same-origin\"\n />\n )}\n </motion.div>\n )}\n </AnimatePresence>\n </Card>\n </MotionItem>\n );\n })}\n </MotionList>\n\n <ScorecardsOverviewPanel ownerType={ownerType} highlightOwnerId={ownerId} />\n\n {renderAddMetricDialog()}\n {renderDeleteMetricDialog()}\n </div>\n );\n};\n\nconst TIER_STYLE: Record<ScorecardTier, { label: string; cls: string }> = {\n [ScorecardTier.Bronze]: {\n label: 'Bronze',\n cls: 'bg-status-warning-bg-subtle text-status-warning-text border border-status-warning-border',\n },\n [ScorecardTier.Silver]: {\n label: 'Silver',\n cls: 'bg-bg-sunken text-text-secondary border border-border-strong',\n },\n [ScorecardTier.Gold]: {\n label: 'Gold',\n cls: 'bg-status-success-bg-subtle text-status-success-text border border-status-success-border',\n },\n};\n\n/**\n * TierBadge — shows the scorecard's derived production-readiness tier (Bronze/Silver/Gold).\n * The tier is computed server-side from the tier-tagged metrics; this is read-only.\n */\nconst TierBadge: FC<{ tier: ScorecardTier }> = ({ tier }) => {\n const style = TIER_STYLE[tier];\n return (\n <span\n className={`inline-flex items-center gap-1 rounded-full px-2.5 py-1 text-xs font-semibold ${style.cls}`}\n title={`Security readiness tier: ${style.label}`}\n >\n <ShieldCheck className=\"size-3.5\" />\n {style.label}\n </span>\n );\n};\n"],"mappings":"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"}
|
|
@@ -5,15 +5,15 @@ import { Fragment as l, jsx as u, jsxs as d } from "react/jsx-runtime";
|
|
|
5
5
|
//#region src/components/security/SecurityScanList.tsx
|
|
6
6
|
var f = {
|
|
7
7
|
queued: "bg-bg-sunken text-text-secondary",
|
|
8
|
-
running: "bg-status-info-bg
|
|
9
|
-
succeeded: "bg-status-success-bg
|
|
10
|
-
failed: "bg-status-error-bg
|
|
11
|
-
errored: "bg-status-error-bg
|
|
8
|
+
running: "bg-status-info-bg-subtle text-status-info-text",
|
|
9
|
+
succeeded: "bg-status-success-bg-subtle text-status-success-text",
|
|
10
|
+
failed: "bg-status-error-bg-subtle text-status-error-text",
|
|
11
|
+
errored: "bg-status-error-bg-subtle text-status-error-text",
|
|
12
12
|
cancelled: "bg-bg-sunken text-text-tertiary"
|
|
13
13
|
}, p = {
|
|
14
|
-
pass: "bg-status-success-bg
|
|
15
|
-
warn: "bg-status-warning-bg
|
|
16
|
-
fail: "bg-status-error-bg
|
|
14
|
+
pass: "bg-status-success-bg-subtle text-status-success-text",
|
|
15
|
+
warn: "bg-status-warning-bg-subtle text-status-warning-text",
|
|
16
|
+
fail: "bg-status-error-bg-subtle text-status-error-text",
|
|
17
17
|
skipped: "bg-bg-sunken text-text-tertiary"
|
|
18
18
|
};
|
|
19
19
|
function m(e) {
|
|
@@ -70,17 +70,17 @@ function v({ run: e }) {
|
|
|
70
70
|
className: "flex items-center gap-1 text-xs",
|
|
71
71
|
children: [
|
|
72
72
|
t.critical > 0 && /* @__PURE__ */ d("span", {
|
|
73
|
-
className: "inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 bg-status-error-bg
|
|
73
|
+
className: "inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 bg-status-error-bg-subtle text-status-error-text font-mono",
|
|
74
74
|
title: `${t.critical} critical`,
|
|
75
75
|
children: ["C ", t.critical]
|
|
76
76
|
}),
|
|
77
77
|
t.high > 0 && /* @__PURE__ */ d("span", {
|
|
78
|
-
className: "inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 bg-status-warning-bg
|
|
78
|
+
className: "inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 bg-status-warning-bg-subtle text-status-warning-text font-mono",
|
|
79
79
|
title: `${t.high} high`,
|
|
80
80
|
children: ["H ", t.high]
|
|
81
81
|
}),
|
|
82
82
|
t.medium > 0 && /* @__PURE__ */ d("span", {
|
|
83
|
-
className: "inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 bg-status-warning-bg
|
|
83
|
+
className: "inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 bg-status-warning-bg-subtle text-status-warning-text/80 font-mono",
|
|
84
84
|
title: `${t.medium} medium`,
|
|
85
85
|
children: ["M ", t.medium]
|
|
86
86
|
}),
|
|
@@ -165,7 +165,7 @@ function b({ vibeId: r, agentId: i, profile: a, limit: o = 20 }) {
|
|
|
165
165
|
})]
|
|
166
166
|
}),
|
|
167
167
|
x && /* @__PURE__ */ d("div", {
|
|
168
|
-
className: "m-4 flex items-start gap-2 rounded-md border border-status-error-border bg-status-error-bg/10 p-3 text-sm",
|
|
168
|
+
className: "m-4 flex items-start gap-2 rounded-md border border-status-error-border bg-status-error-bg-subtle/10 p-3 text-sm",
|
|
169
169
|
children: [/* @__PURE__ */ u(n, { className: "size-4 text-status-error-text mt-0.5 shrink-0" }), /* @__PURE__ */ d("div", {
|
|
170
170
|
className: "text-text-primary",
|
|
171
171
|
children: [/* @__PURE__ */ u("p", {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SecurityScanList.js","names":[],"sources":["../../../src/components/security/SecurityScanList.tsx"],"sourcesContent":["/**\n * Recent scan runs list for a vibe, read directly from the agent over\n * the tunnel. No backend GraphQL — see `./agentClient.ts` for the\n * REST surface and `./hooks.ts` for the React Query wrappers.\n *\n * The agent doesn't expose a list-runs endpoint; we derive distinct\n * run ids from the findings cache (see `listRecentScanRuns`). When a\n * vibe has zero findings the list will be empty even if scans have\n * been queued — that's correct: an in-flight queued/running scan\n * doesn't yet have any findings, and the empty state already nudges\n * the user toward kicking off a scan.\n */\nimport { useMemo, type ReactElement } from 'react';\nimport {\n AlertCircle,\n CheckCircle2,\n Clock,\n Loader2,\n MinusCircle,\n ShieldAlert,\n XCircle,\n} from 'lucide-react';\n\nimport { useSecurityRecentScansQuery, type UseSecurityAgentContext } from './hooks';\nimport type { AgentScanConclusion, AgentScanStatus, SecurityScanRun } from './agentClient';\n\ninterface Props extends UseSecurityAgentContext {\n vibeId: string;\n /** Max rows to surface. Defaults to 20. */\n limit?: number;\n}\n\nconst STATUS_BADGE: Record<AgentScanStatus, string> = {\n queued: 'bg-bg-sunken text-text-secondary',\n running: 'bg-status-info-bg/20 text-status-info-text',\n succeeded: 'bg-status-success-bg/20 text-status-success-text',\n failed: 'bg-status-error-bg/20 text-status-error-text',\n errored: 'bg-status-error-bg/20 text-status-error-text',\n cancelled: 'bg-bg-sunken text-text-tertiary',\n};\n\nconst CONCLUSION_BADGE: Record<AgentScanConclusion, string> = {\n pass: 'bg-status-success-bg/20 text-status-success-text',\n warn: 'bg-status-warning-bg/20 text-status-warning-text',\n fail: 'bg-status-error-bg/20 text-status-error-text',\n skipped: 'bg-bg-sunken text-text-tertiary',\n};\n\nfunction statusIcon(status: AgentScanStatus): ReactElement {\n switch (status) {\n case 'queued':\n return <Clock className=\"size-3.5\" aria-hidden=\"true\" />;\n case 'running':\n return <Loader2 className=\"size-3.5 animate-spin\" aria-hidden=\"true\" />;\n case 'succeeded':\n return <CheckCircle2 className=\"size-3.5\" aria-hidden=\"true\" />;\n case 'failed':\n case 'errored':\n return <XCircle className=\"size-3.5\" aria-hidden=\"true\" />;\n case 'cancelled':\n return <MinusCircle className=\"size-3.5\" aria-hidden=\"true\" />;\n }\n}\n\nfunction relativeTime(epochMs?: number): string {\n if (!epochMs) return '—';\n const diff = Date.now() - epochMs;\n const sec = Math.round(diff / 1000);\n if (sec < 60) return `${sec}s ago`;\n const min = Math.round(sec / 60);\n if (min < 60) return `${min}m ago`;\n const hr = Math.round(min / 60);\n if (hr < 24) return `${hr}h ago`;\n const day = Math.round(hr / 24);\n return `${day}d ago`;\n}\n\nfunction formatDuration(ms?: number): string {\n if (ms === undefined || ms === null) return '—';\n if (ms < 1000) return `${ms}ms`;\n const sec = ms / 1000;\n if (sec < 60) return `${sec.toFixed(1)}s`;\n const min = sec / 60;\n if (min < 60) return `${min.toFixed(1)}m`;\n const hr = min / 60;\n return `${hr.toFixed(1)}h`;\n}\n\n/**\n * Prettify the agent's dotted stage id (e.g. `pull_request.fast` →\n * `pull_request fast`). We don't gate on the canonical 14-stage taxonomy\n * here because providers can — in principle — emit arbitrary stages and\n * we want to surface them rather than hide them.\n */\nfunction formatStage(stage: string): string {\n return stage.replace(/\\./g, ' ');\n}\n\nfunction SummaryCell({ run }: { run: SecurityScanRun }): ReactElement {\n const s = run.summary;\n const total = s.critical + s.high + s.medium + s.low + s.info;\n if (total === 0) {\n return <span className=\"text-xs text-text-tertiary\">clean</span>;\n }\n return (\n <div className=\"flex items-center gap-1 text-xs\">\n {s.critical > 0 && (\n <span\n className=\"inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 bg-status-error-bg/20 text-status-error-text font-mono\"\n title={`${s.critical} critical`}\n >\n C {s.critical}\n </span>\n )}\n {s.high > 0 && (\n <span\n className=\"inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 bg-status-warning-bg/20 text-status-warning-text font-mono\"\n title={`${s.high} high`}\n >\n H {s.high}\n </span>\n )}\n {s.medium > 0 && (\n <span\n className=\"inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 bg-status-warning-bg/10 text-status-warning-text/80 font-mono\"\n title={`${s.medium} medium`}\n >\n M {s.medium}\n </span>\n )}\n {s.low > 0 && (\n <span\n className=\"inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 bg-bg-sunken text-text-secondary font-mono\"\n title={`${s.low} low`}\n >\n L {s.low}\n </span>\n )}\n {s.info > 0 && (\n <span\n className=\"inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 bg-bg-sunken text-text-tertiary font-mono\"\n title={`${s.info} info`}\n >\n I {s.info}\n </span>\n )}\n </div>\n );\n}\n\nfunction SkeletonRow({ rowKey }: { rowKey: number }): ReactElement {\n return (\n <tr key={rowKey} className=\"border-t border-border-subtle\">\n <td className=\"px-3 py-2\">\n <div className=\"h-3 w-20 rounded bg-bg-sunken animate-pulse\" />\n </td>\n <td className=\"px-3 py-2\">\n <div className=\"h-3 w-24 rounded bg-bg-sunken animate-pulse\" />\n </td>\n <td className=\"px-3 py-2\">\n <div className=\"h-3 w-16 rounded bg-bg-sunken animate-pulse\" />\n </td>\n <td className=\"px-3 py-2\">\n <div className=\"h-3 w-16 rounded bg-bg-sunken animate-pulse\" />\n </td>\n <td className=\"px-3 py-2\">\n <div className=\"h-3 w-20 rounded bg-bg-sunken animate-pulse\" />\n </td>\n <td className=\"px-3 py-2\">\n <div className=\"h-3 w-14 rounded bg-bg-sunken animate-pulse\" />\n </td>\n <td className=\"px-3 py-2\">\n <div className=\"h-3 w-28 rounded bg-bg-sunken animate-pulse\" />\n </td>\n </tr>\n );\n}\n\nexport function SecurityScanList({ vibeId, agentId, profile, limit = 20 }: Props): ReactElement {\n const ctx = useMemo(() => ({ agentId, profile }), [agentId, profile]);\n const { data, isLoading, error, isFetching, refetch } = useSecurityRecentScansQuery(\n ctx,\n vibeId,\n limit\n );\n\n // F4 Wave C: agentApiKey + tunnelUrl no longer needed in the browser —\n // the svc proxy owns them. A non-null `agentId` is the only readiness\n // gate (svc rejects unreachable agents with a structured envelope).\n const ready = !!agentId;\n\n if (!ready) {\n return (\n <section\n data-testid=\"security-scan-list\"\n className=\"rounded-lg border border-border-default bg-bg-surface p-4 text-sm text-text-secondary\"\n >\n Connect to a reachable agent to load scan history. Scan runs are read from the agent via the\n svc GraphQL proxy — there's no backend cache.\n </section>\n );\n }\n\n return (\n <section\n data-testid=\"security-scan-list\"\n className=\"rounded-lg border border-border-default bg-bg-surface\"\n >\n <header className=\"flex items-center justify-between gap-3 border-b border-border-default px-4 py-3\">\n <div className=\"min-w-0\">\n <h3 className=\"text-sm font-semibold text-text-primary\">Recent scan runs</h3>\n <p className=\"text-xs text-text-secondary mt-0.5\">\n Read live from the agent. Showing up to {limit} most recently active runs for this vibe.\n </p>\n </div>\n <button\n type=\"button\"\n onClick={() => void refetch()}\n disabled={isFetching}\n className=\"px-2.5 py-1 text-xs rounded-md border border-border-default bg-bg-sunken text-text-primary hover:bg-action-primary-bg-hover disabled:opacity-50\"\n >\n {isFetching ? 'Refreshing…' : 'Refresh'}\n </button>\n </header>\n\n {error && (\n <div className=\"m-4 flex items-start gap-2 rounded-md border border-status-error-border bg-status-error-bg/10 p-3 text-sm\">\n <AlertCircle className=\"size-4 text-status-error-text mt-0.5 shrink-0\" />\n <div className=\"text-text-primary\">\n <p className=\"font-medium\">Couldn't load scan runs</p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">{error.message}</p>\n </div>\n </div>\n )}\n\n <div className=\"overflow-x-auto\">\n <table className=\"w-full text-sm\">\n <thead className=\"bg-bg-sunken/40 text-text-secondary\">\n <tr>\n <th className=\"px-3 py-2 text-left text-xs font-medium\">Stage</th>\n <th className=\"px-3 py-2 text-left text-xs font-medium\">Provider</th>\n <th className=\"px-3 py-2 text-left text-xs font-medium\">Status</th>\n <th className=\"px-3 py-2 text-left text-xs font-medium\">Conclusion</th>\n <th className=\"px-3 py-2 text-left text-xs font-medium\">Started</th>\n <th className=\"px-3 py-2 text-left text-xs font-medium\">Duration</th>\n <th className=\"px-3 py-2 text-left text-xs font-medium\">Findings</th>\n </tr>\n </thead>\n <tbody>\n {isLoading && data === undefined && (\n <>\n <SkeletonRow rowKey={0} />\n <SkeletonRow rowKey={1} />\n <SkeletonRow rowKey={2} />\n </>\n )}\n {!isLoading && (data?.length ?? 0) === 0 && !error && (\n <tr>\n <td colSpan={7} className=\"px-3 py-8\">\n <div className=\"flex flex-col items-center gap-2 text-center\">\n <ShieldAlert className=\"size-6 text-text-tertiary\" />\n <p className=\"text-sm text-text-primary\">No scans yet</p>\n <p className=\"text-xs text-text-secondary max-w-md\">\n Install a provider above and run a scan to see results here. Findings live in\n the agent's local SQLite cache.\n </p>\n </div>\n </td>\n </tr>\n )}\n {(data ?? []).map((run) => (\n <tr key={run.runId} className=\"border-t border-border-subtle align-middle\">\n <td className=\"px-3 py-2\">\n <span className=\"inline-flex items-center rounded-md bg-bg-sunken px-2 py-0.5 text-xs text-text-primary\">\n {formatStage(run.stage)}\n </span>\n </td>\n <td className=\"px-3 py-2 text-xs text-text-primary\">{run.providerName}</td>\n <td className=\"px-3 py-2\">\n <span\n className={`inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-xs font-medium ${STATUS_BADGE[run.status]}`}\n >\n {statusIcon(run.status)}\n {run.status}\n </span>\n </td>\n <td className=\"px-3 py-2\">\n {run.conclusion ? (\n <span\n className={`inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium ${CONCLUSION_BADGE[run.conclusion]}`}\n >\n {run.conclusion}\n </span>\n ) : (\n <span className=\"text-xs text-text-tertiary\">—</span>\n )}\n </td>\n <td className=\"px-3 py-2 text-xs text-text-secondary\">\n <span title={run.startedAt ? new Date(run.startedAt).toLocaleString() : ''}>\n {relativeTime(run.startedAt)}\n </span>\n </td>\n <td className=\"px-3 py-2 text-xs text-text-secondary font-mono\">\n {formatDuration(run.durationMs)}\n </td>\n <td className=\"px-3 py-2\">\n <SummaryCell run={run} />\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </section>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SecurityScanList.js","names":[],"sources":["../../../src/components/security/SecurityScanList.tsx"],"sourcesContent":["/**\n * Recent scan runs list for a vibe, read directly from the agent over\n * the tunnel. No backend GraphQL — see `./agentClient.ts` for the\n * REST surface and `./hooks.ts` for the React Query wrappers.\n *\n * The agent doesn't expose a list-runs endpoint; we derive distinct\n * run ids from the findings cache (see `listRecentScanRuns`). When a\n * vibe has zero findings the list will be empty even if scans have\n * been queued — that's correct: an in-flight queued/running scan\n * doesn't yet have any findings, and the empty state already nudges\n * the user toward kicking off a scan.\n */\nimport { useMemo, type ReactElement } from 'react';\nimport {\n AlertCircle,\n CheckCircle2,\n Clock,\n Loader2,\n MinusCircle,\n ShieldAlert,\n XCircle,\n} from 'lucide-react';\n\nimport { useSecurityRecentScansQuery, type UseSecurityAgentContext } from './hooks';\nimport type { AgentScanConclusion, AgentScanStatus, SecurityScanRun } from './agentClient';\n\ninterface Props extends UseSecurityAgentContext {\n vibeId: string;\n /** Max rows to surface. Defaults to 20. */\n limit?: number;\n}\n\nconst STATUS_BADGE: Record<AgentScanStatus, string> = {\n queued: 'bg-bg-sunken text-text-secondary',\n running: 'bg-status-info-bg-subtle text-status-info-text',\n succeeded: 'bg-status-success-bg-subtle text-status-success-text',\n failed: 'bg-status-error-bg-subtle text-status-error-text',\n errored: 'bg-status-error-bg-subtle text-status-error-text',\n cancelled: 'bg-bg-sunken text-text-tertiary',\n};\n\nconst CONCLUSION_BADGE: Record<AgentScanConclusion, string> = {\n pass: 'bg-status-success-bg-subtle text-status-success-text',\n warn: 'bg-status-warning-bg-subtle text-status-warning-text',\n fail: 'bg-status-error-bg-subtle text-status-error-text',\n skipped: 'bg-bg-sunken text-text-tertiary',\n};\n\nfunction statusIcon(status: AgentScanStatus): ReactElement {\n switch (status) {\n case 'queued':\n return <Clock className=\"size-3.5\" aria-hidden=\"true\" />;\n case 'running':\n return <Loader2 className=\"size-3.5 animate-spin\" aria-hidden=\"true\" />;\n case 'succeeded':\n return <CheckCircle2 className=\"size-3.5\" aria-hidden=\"true\" />;\n case 'failed':\n case 'errored':\n return <XCircle className=\"size-3.5\" aria-hidden=\"true\" />;\n case 'cancelled':\n return <MinusCircle className=\"size-3.5\" aria-hidden=\"true\" />;\n }\n}\n\nfunction relativeTime(epochMs?: number): string {\n if (!epochMs) return '—';\n const diff = Date.now() - epochMs;\n const sec = Math.round(diff / 1000);\n if (sec < 60) return `${sec}s ago`;\n const min = Math.round(sec / 60);\n if (min < 60) return `${min}m ago`;\n const hr = Math.round(min / 60);\n if (hr < 24) return `${hr}h ago`;\n const day = Math.round(hr / 24);\n return `${day}d ago`;\n}\n\nfunction formatDuration(ms?: number): string {\n if (ms === undefined || ms === null) return '—';\n if (ms < 1000) return `${ms}ms`;\n const sec = ms / 1000;\n if (sec < 60) return `${sec.toFixed(1)}s`;\n const min = sec / 60;\n if (min < 60) return `${min.toFixed(1)}m`;\n const hr = min / 60;\n return `${hr.toFixed(1)}h`;\n}\n\n/**\n * Prettify the agent's dotted stage id (e.g. `pull_request.fast` →\n * `pull_request fast`). We don't gate on the canonical 14-stage taxonomy\n * here because providers can — in principle — emit arbitrary stages and\n * we want to surface them rather than hide them.\n */\nfunction formatStage(stage: string): string {\n return stage.replace(/\\./g, ' ');\n}\n\nfunction SummaryCell({ run }: { run: SecurityScanRun }): ReactElement {\n const s = run.summary;\n const total = s.critical + s.high + s.medium + s.low + s.info;\n if (total === 0) {\n return <span className=\"text-xs text-text-tertiary\">clean</span>;\n }\n return (\n <div className=\"flex items-center gap-1 text-xs\">\n {s.critical > 0 && (\n <span\n className=\"inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 bg-status-error-bg-subtle text-status-error-text font-mono\"\n title={`${s.critical} critical`}\n >\n C {s.critical}\n </span>\n )}\n {s.high > 0 && (\n <span\n className=\"inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 bg-status-warning-bg-subtle text-status-warning-text font-mono\"\n title={`${s.high} high`}\n >\n H {s.high}\n </span>\n )}\n {s.medium > 0 && (\n <span\n className=\"inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 bg-status-warning-bg-subtle text-status-warning-text/80 font-mono\"\n title={`${s.medium} medium`}\n >\n M {s.medium}\n </span>\n )}\n {s.low > 0 && (\n <span\n className=\"inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 bg-bg-sunken text-text-secondary font-mono\"\n title={`${s.low} low`}\n >\n L {s.low}\n </span>\n )}\n {s.info > 0 && (\n <span\n className=\"inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 bg-bg-sunken text-text-tertiary font-mono\"\n title={`${s.info} info`}\n >\n I {s.info}\n </span>\n )}\n </div>\n );\n}\n\nfunction SkeletonRow({ rowKey }: { rowKey: number }): ReactElement {\n return (\n <tr key={rowKey} className=\"border-t border-border-subtle\">\n <td className=\"px-3 py-2\">\n <div className=\"h-3 w-20 rounded bg-bg-sunken animate-pulse\" />\n </td>\n <td className=\"px-3 py-2\">\n <div className=\"h-3 w-24 rounded bg-bg-sunken animate-pulse\" />\n </td>\n <td className=\"px-3 py-2\">\n <div className=\"h-3 w-16 rounded bg-bg-sunken animate-pulse\" />\n </td>\n <td className=\"px-3 py-2\">\n <div className=\"h-3 w-16 rounded bg-bg-sunken animate-pulse\" />\n </td>\n <td className=\"px-3 py-2\">\n <div className=\"h-3 w-20 rounded bg-bg-sunken animate-pulse\" />\n </td>\n <td className=\"px-3 py-2\">\n <div className=\"h-3 w-14 rounded bg-bg-sunken animate-pulse\" />\n </td>\n <td className=\"px-3 py-2\">\n <div className=\"h-3 w-28 rounded bg-bg-sunken animate-pulse\" />\n </td>\n </tr>\n );\n}\n\nexport function SecurityScanList({ vibeId, agentId, profile, limit = 20 }: Props): ReactElement {\n const ctx = useMemo(() => ({ agentId, profile }), [agentId, profile]);\n const { data, isLoading, error, isFetching, refetch } = useSecurityRecentScansQuery(\n ctx,\n vibeId,\n limit\n );\n\n // F4 Wave C: agentApiKey + tunnelUrl no longer needed in the browser —\n // the svc proxy owns them. A non-null `agentId` is the only readiness\n // gate (svc rejects unreachable agents with a structured envelope).\n const ready = !!agentId;\n\n if (!ready) {\n return (\n <section\n data-testid=\"security-scan-list\"\n className=\"rounded-lg border border-border-default bg-bg-surface p-4 text-sm text-text-secondary\"\n >\n Connect to a reachable agent to load scan history. Scan runs are read from the agent via the\n svc GraphQL proxy — there's no backend cache.\n </section>\n );\n }\n\n return (\n <section\n data-testid=\"security-scan-list\"\n className=\"rounded-lg border border-border-default bg-bg-surface\"\n >\n <header className=\"flex items-center justify-between gap-3 border-b border-border-default px-4 py-3\">\n <div className=\"min-w-0\">\n <h3 className=\"text-sm font-semibold text-text-primary\">Recent scan runs</h3>\n <p className=\"text-xs text-text-secondary mt-0.5\">\n Read live from the agent. Showing up to {limit} most recently active runs for this vibe.\n </p>\n </div>\n <button\n type=\"button\"\n onClick={() => void refetch()}\n disabled={isFetching}\n className=\"px-2.5 py-1 text-xs rounded-md border border-border-default bg-bg-sunken text-text-primary hover:bg-action-primary-bg-hover disabled:opacity-50\"\n >\n {isFetching ? 'Refreshing…' : 'Refresh'}\n </button>\n </header>\n\n {error && (\n <div className=\"m-4 flex items-start gap-2 rounded-md border border-status-error-border bg-status-error-bg-subtle/10 p-3 text-sm\">\n <AlertCircle className=\"size-4 text-status-error-text mt-0.5 shrink-0\" />\n <div className=\"text-text-primary\">\n <p className=\"font-medium\">Couldn't load scan runs</p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">{error.message}</p>\n </div>\n </div>\n )}\n\n <div className=\"overflow-x-auto\">\n <table className=\"w-full text-sm\">\n <thead className=\"bg-bg-sunken/40 text-text-secondary\">\n <tr>\n <th className=\"px-3 py-2 text-left text-xs font-medium\">Stage</th>\n <th className=\"px-3 py-2 text-left text-xs font-medium\">Provider</th>\n <th className=\"px-3 py-2 text-left text-xs font-medium\">Status</th>\n <th className=\"px-3 py-2 text-left text-xs font-medium\">Conclusion</th>\n <th className=\"px-3 py-2 text-left text-xs font-medium\">Started</th>\n <th className=\"px-3 py-2 text-left text-xs font-medium\">Duration</th>\n <th className=\"px-3 py-2 text-left text-xs font-medium\">Findings</th>\n </tr>\n </thead>\n <tbody>\n {isLoading && data === undefined && (\n <>\n <SkeletonRow rowKey={0} />\n <SkeletonRow rowKey={1} />\n <SkeletonRow rowKey={2} />\n </>\n )}\n {!isLoading && (data?.length ?? 0) === 0 && !error && (\n <tr>\n <td colSpan={7} className=\"px-3 py-8\">\n <div className=\"flex flex-col items-center gap-2 text-center\">\n <ShieldAlert className=\"size-6 text-text-tertiary\" />\n <p className=\"text-sm text-text-primary\">No scans yet</p>\n <p className=\"text-xs text-text-secondary max-w-md\">\n Install a provider above and run a scan to see results here. Findings live in\n the agent's local SQLite cache.\n </p>\n </div>\n </td>\n </tr>\n )}\n {(data ?? []).map((run) => (\n <tr key={run.runId} className=\"border-t border-border-subtle align-middle\">\n <td className=\"px-3 py-2\">\n <span className=\"inline-flex items-center rounded-md bg-bg-sunken px-2 py-0.5 text-xs text-text-primary\">\n {formatStage(run.stage)}\n </span>\n </td>\n <td className=\"px-3 py-2 text-xs text-text-primary\">{run.providerName}</td>\n <td className=\"px-3 py-2\">\n <span\n className={`inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-xs font-medium ${STATUS_BADGE[run.status]}`}\n >\n {statusIcon(run.status)}\n {run.status}\n </span>\n </td>\n <td className=\"px-3 py-2\">\n {run.conclusion ? (\n <span\n className={`inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium ${CONCLUSION_BADGE[run.conclusion]}`}\n >\n {run.conclusion}\n </span>\n ) : (\n <span className=\"text-xs text-text-tertiary\">—</span>\n )}\n </td>\n <td className=\"px-3 py-2 text-xs text-text-secondary\">\n <span title={run.startedAt ? new Date(run.startedAt).toLocaleString() : ''}>\n {relativeTime(run.startedAt)}\n </span>\n </td>\n <td className=\"px-3 py-2 text-xs text-text-secondary font-mono\">\n {formatDuration(run.durationMs)}\n </td>\n <td className=\"px-3 py-2\">\n <SummaryCell run={run} />\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </section>\n );\n}\n"],"mappings":"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"}
|
|
@@ -32,7 +32,7 @@ var _ = {
|
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
34
|
whileTap: { scale: .98 },
|
|
35
|
-
className: `bg-bg-surface border rounded-
|
|
35
|
+
className: `bg-bg-surface border rounded-card shadow-elevation-1 p-5 hover:border-border-strong hover:shadow-elevation-2 transition-all duration-200 ease-[var(--motion-easing-out)] cursor-pointer ${E ? "border-transparent ring-1 ring-[var(--color-focus-ring)] bg-[var(--color-accent-soft)]" : "border-border-subtle"} ${O}`,
|
|
36
36
|
onClick: () => y?.(v),
|
|
37
37
|
children: [
|
|
38
38
|
/* @__PURE__ */ g("div", {
|
|
@@ -54,8 +54,8 @@ var _ = {
|
|
|
54
54
|
})
|
|
55
55
|
}),
|
|
56
56
|
/* @__PURE__ */ h("div", {
|
|
57
|
-
className: "flex-shrink-0 size-
|
|
58
|
-
children: /* @__PURE__ */ h(d, { className: "size-4 sm:
|
|
57
|
+
className: "flex-shrink-0 size-9 sm:size-10 rounded-lg bg-[var(--color-accent-soft)] flex items-center justify-center",
|
|
58
|
+
children: /* @__PURE__ */ h(d, { className: "size-4 sm:size-5 text-action-primary-bg" })
|
|
59
59
|
}),
|
|
60
60
|
/* @__PURE__ */ g("div", {
|
|
61
61
|
className: "min-w-0 flex-1",
|
|
@@ -63,7 +63,7 @@ var _ = {
|
|
|
63
63
|
/* @__PURE__ */ g("div", {
|
|
64
64
|
className: "flex items-center gap-1.5 sm:gap-2 flex-wrap",
|
|
65
65
|
children: [/* @__PURE__ */ h("h3", {
|
|
66
|
-
className: "font-semibold text-text-primary truncate max-w-[120px] sm:max-w-none text-sm sm:text-base",
|
|
66
|
+
className: "font-semibold text-text-primary truncate max-w-[120px] sm:max-w-none text-sm sm:text-base tracking-tight",
|
|
67
67
|
children: v.name
|
|
68
68
|
}), /* @__PURE__ */ h("span", {
|
|
69
69
|
className: "text-base sm:text-lg flex-shrink-0",
|
|
@@ -90,8 +90,12 @@ var _ = {
|
|
|
90
90
|
onClick: (e) => {
|
|
91
91
|
e.stopPropagation(), j(!A);
|
|
92
92
|
},
|
|
93
|
-
className: "
|
|
94
|
-
|
|
93
|
+
className: "inline-flex size-8 items-center justify-center rounded-button text-text-secondary hover:bg-bg-sunken hover:text-text-primary transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus",
|
|
94
|
+
"aria-label": k("vibecontrols.sessions.card.actions", "Session actions"),
|
|
95
|
+
children: /* @__PURE__ */ h(o, {
|
|
96
|
+
className: "size-4",
|
|
97
|
+
"aria-hidden": "true"
|
|
98
|
+
})
|
|
95
99
|
}), /* @__PURE__ */ h(p, { children: A && /* @__PURE__ */ g(m.div, {
|
|
96
100
|
initial: {
|
|
97
101
|
opacity: 0,
|
|
@@ -109,14 +113,14 @@ var _ = {
|
|
|
109
113
|
y: -4
|
|
110
114
|
},
|
|
111
115
|
transition: { duration: .15 },
|
|
112
|
-
className: "absolute right-0 mt-1 w-
|
|
116
|
+
className: "absolute right-0 mt-1.5 w-44 bg-bg-elevated border border-border-subtle rounded-card shadow-elevation-3 z-10 overflow-hidden py-1",
|
|
113
117
|
children: [
|
|
114
118
|
P && b && /* @__PURE__ */ g(m.button, {
|
|
115
119
|
whileTap: { scale: .97 },
|
|
116
120
|
onClick: (e) => {
|
|
117
121
|
e.stopPropagation(), b(v), j(!1);
|
|
118
122
|
},
|
|
119
|
-
className: "flex items-center gap-2 w-full px-3 py-2 text-sm text-text-primary hover:bg-bg-sunken
|
|
123
|
+
className: "flex items-center gap-2 w-full px-3 py-2 text-sm text-text-primary hover:bg-bg-sunken transition-colors",
|
|
120
124
|
children: [/* @__PURE__ */ h(s, { className: "size-4" }), k("vibecontrols.sessions.card.start", "Start")]
|
|
121
125
|
}),
|
|
122
126
|
N && x && /* @__PURE__ */ g(m.button, {
|
|
@@ -124,7 +128,7 @@ var _ = {
|
|
|
124
128
|
onClick: (e) => {
|
|
125
129
|
e.stopPropagation(), x(v), j(!1);
|
|
126
130
|
},
|
|
127
|
-
className: "flex items-center gap-2 w-full px-3 py-2 text-sm text-text-primary hover:bg-bg-sunken",
|
|
131
|
+
className: "flex items-center gap-2 w-full px-3 py-2 text-sm text-text-primary hover:bg-bg-sunken transition-colors",
|
|
128
132
|
children: [/* @__PURE__ */ h(u, { className: "size-4" }), k("vibecontrols.sessions.card.stop", "Stop")]
|
|
129
133
|
}),
|
|
130
134
|
N && S && /* @__PURE__ */ g(m.button, {
|
|
@@ -132,7 +136,7 @@ var _ = {
|
|
|
132
136
|
onClick: (e) => {
|
|
133
137
|
e.stopPropagation(), S(v), j(!1);
|
|
134
138
|
},
|
|
135
|
-
className: "flex items-center gap-2 w-full px-3 py-2 text-sm text-text-primary hover:bg-bg-sunken",
|
|
139
|
+
className: "flex items-center gap-2 w-full px-3 py-2 text-sm text-text-primary hover:bg-bg-sunken transition-colors",
|
|
136
140
|
children: [/* @__PURE__ */ h(c, { className: "size-4" }), k("vibecontrols.sessions.card.restart", "Restart")]
|
|
137
141
|
}),
|
|
138
142
|
N && w && /* @__PURE__ */ g(m.button, {
|
|
@@ -140,7 +144,7 @@ var _ = {
|
|
|
140
144
|
onClick: (e) => {
|
|
141
145
|
e.stopPropagation(), w(v), j(!1);
|
|
142
146
|
},
|
|
143
|
-
className: "flex items-center gap-2 w-full px-3 py-2 text-sm text-text-primary hover:bg-bg-sunken",
|
|
147
|
+
className: "flex items-center gap-2 w-full px-3 py-2 text-sm text-text-primary hover:bg-bg-sunken transition-colors",
|
|
144
148
|
children: [/* @__PURE__ */ h(d, { className: "size-4" }), k("vibecontrols.sessions.card.openTerminal", "Open Terminal")]
|
|
145
149
|
}),
|
|
146
150
|
N && T && /* @__PURE__ */ g(m.button, {
|
|
@@ -148,7 +152,7 @@ var _ = {
|
|
|
148
152
|
onClick: (e) => {
|
|
149
153
|
e.stopPropagation(), T(v), j(!1);
|
|
150
154
|
},
|
|
151
|
-
className: "flex items-center gap-2 w-full px-3 py-2 text-sm text-text-primary hover:bg-bg-sunken",
|
|
155
|
+
className: "flex items-center gap-2 w-full px-3 py-2 text-sm text-text-primary hover:bg-bg-sunken transition-colors",
|
|
152
156
|
children: [/* @__PURE__ */ h(l, { className: "size-4" }), k("vibecontrols.sessions.card.shareSession", "Share Session")]
|
|
153
157
|
}),
|
|
154
158
|
C && /* @__PURE__ */ g(m.button, {
|
|
@@ -156,7 +160,7 @@ var _ = {
|
|
|
156
160
|
onClick: (e) => {
|
|
157
161
|
e.stopPropagation(), C(v), j(!1);
|
|
158
162
|
},
|
|
159
|
-
className: "flex items-center gap-2 w-full px-3 py-2 text-sm text-status-error-text hover:bg-bg-
|
|
163
|
+
className: "flex items-center gap-2 w-full px-3 py-2 text-sm text-status-error-text hover:bg-status-error-bg-subtle transition-colors",
|
|
160
164
|
children: [/* @__PURE__ */ h(f, { className: "size-4" }), k("vibecontrols.sessions.card.terminate", "Terminate")]
|
|
161
165
|
})
|
|
162
166
|
]
|
|
@@ -167,7 +171,7 @@ var _ = {
|
|
|
167
171
|
(() => {
|
|
168
172
|
let e = v.metadata || {}, t = typeof e.tmuxSessionName == "string" && e.tmuxSessionName || typeof e.weztermWorkspace == "string" && e.weztermWorkspace || typeof e.zellijSessionName == "string" && e.zellijSessionName || typeof e.externalSessionName == "string" && e.externalSessionName || v.providerSessionName || null;
|
|
169
173
|
return t ? /* @__PURE__ */ g("div", {
|
|
170
|
-
className: "mt-
|
|
174
|
+
className: "mt-3 text-[10px] font-mono text-text-muted",
|
|
171
175
|
title: `Provider session: ${t}`,
|
|
172
176
|
"data-testid": "session-card-external-name",
|
|
173
177
|
children: [/* @__PURE__ */ g("span", {
|
|
@@ -180,20 +184,23 @@ var _ = {
|
|
|
180
184
|
}) : null;
|
|
181
185
|
})(),
|
|
182
186
|
v.command && /* @__PURE__ */ g("div", {
|
|
183
|
-
className: "mt-
|
|
187
|
+
className: "mt-4 text-xs font-mono bg-bg-sunken/60 px-2 py-1.5 rounded truncate text-text-secondary",
|
|
184
188
|
children: ["$ ", v.command]
|
|
185
189
|
}),
|
|
186
190
|
v.lastOutput && /* @__PURE__ */ h("div", {
|
|
187
|
-
className: "mt-2 text-xs font-mono bg-bg-sunken text-status-success-text
|
|
191
|
+
className: "mt-2 text-xs font-mono bg-bg-sunken/60 text-status-success-text px-2 py-1.5 rounded truncate",
|
|
188
192
|
children: v.lastOutput
|
|
189
193
|
}),
|
|
190
194
|
/* @__PURE__ */ g("div", {
|
|
191
|
-
className: "mt-
|
|
195
|
+
className: "mt-4 flex items-center justify-between text-sm text-text-secondary",
|
|
192
196
|
children: [/* @__PURE__ */ g("div", {
|
|
193
197
|
className: "flex items-center gap-2",
|
|
194
|
-
children: [/* @__PURE__ */ h(a, { className: "size-4" }), /* @__PURE__ */ h("span", {
|
|
198
|
+
children: [/* @__PURE__ */ h(a, { className: "size-4" }), /* @__PURE__ */ h("span", {
|
|
199
|
+
className: "tabular-nums",
|
|
200
|
+
children: M(v.updatedAt)
|
|
201
|
+
})]
|
|
195
202
|
}), v.pid && /* @__PURE__ */ g("span", {
|
|
196
|
-
className: "text-xs font-mono",
|
|
203
|
+
className: "text-xs font-mono tabular-nums",
|
|
197
204
|
children: ["PID: ", v.pid]
|
|
198
205
|
})]
|
|
199
206
|
})
|