@burdenoff/microfe-admin 2026.626.1 → 2026.720.1
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/admin/AdminRoot.js +2 -2
- package/dist/admin/AdminRoot.js.map +1 -1
- package/dist/admin/AdminRoutes.js +130 -78
- package/dist/admin/AdminRoutes.js.map +1 -1
- package/dist/admin/components/ConfirmDialog.js.map +1 -1
- package/dist/admin/components/DataTable.js.map +1 -1
- package/dist/admin/components/MarkdownEditor.js +116 -0
- package/dist/admin/components/MarkdownEditor.js.map +1 -0
- package/dist/admin/components/index.js +1 -0
- package/dist/admin/constants/featureFlags.js +12 -0
- package/dist/admin/constants/featureFlags.js.map +1 -0
- package/dist/admin/pages/AuditLogsPage.js.map +1 -1
- package/dist/admin/pages/CommunicationsPage.js.map +1 -1
- package/dist/admin/pages/ContentPagesPage.js +197 -197
- package/dist/admin/pages/ContentPagesPage.js.map +1 -1
- package/dist/admin/pages/DashboardPage.js +5 -5
- package/dist/admin/pages/FeatureFlagsPage.js.map +1 -1
- package/dist/admin/pages/ReferralsPage.js.map +1 -1
- package/dist/admin/pages/RolesPermissionsPage.js.map +1 -1
- package/dist/admin/pages/SREPage.js.map +1 -1
- package/dist/admin/pages/SandboxSettingsPage.js.map +1 -1
- package/dist/admin/pages/SettingsPage.js +6 -6
- package/dist/admin/pages/SettingsPage.js.map +1 -1
- package/dist/admin/pages/SyncFailuresPage.js.map +1 -1
- package/dist/admin/pages/TenantDetailPage.js +169 -157
- package/dist/admin/pages/TenantDetailPage.js.map +1 -1
- package/dist/admin/pages/TenantFormRoutePage.js +75 -74
- package/dist/admin/pages/TenantFormRoutePage.js.map +1 -1
- package/dist/admin/pages/TenantsListPage.js +169 -157
- package/dist/admin/pages/TenantsListPage.js.map +1 -1
- package/dist/admin/pages/UsersListPage.js +5 -5
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SandboxSettingsPage.js","names":[],"sources":["../../../src/admin/pages/SandboxSettingsPage.tsx"],"sourcesContent":["import { useState, useCallback, useEffect } from 'react';\nimport { Box, Cpu, MemoryStick, Timer, Users, CheckSquare, Save } from 'lucide-react';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport {\n PageLayout,\n Card,\n CardHeader,\n CardTitle,\n CardContent,\n CardDescription,\n Button,\n Input,\n Separator,\n MetricsGrid,\n MetricCard,\n useToast,\n PermissionGuard,\n} from '../components';\nimport { PagePurpose, NextSteps } from '@burdenoff/fe-libs/ui';\nimport { useAdmin } from '../providers/AdminProvider';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nconst SANDBOX_TYPES = [\n 'VIBECONTROLS',\n 'AI_BUILDER',\n 'WORKFLOW_MULTI_NODE',\n 'MISC_SHELL',\n 'CUSTOM',\n] as const;\n\ntype SandboxType = (typeof SANDBOX_TYPES)[number];\n\nconst TTL_OPTIONS = [\n { label: '30 minutes', value: 1800 },\n { label: '1 hour', value: 3600 },\n { label: '2 hours', value: 7200 },\n { label: '4 hours', value: 14400 },\n { label: '8 hours', value: 28800 },\n { label: '24 hours', value: 86400 },\n] as const;\n\ninterface SandboxConfig {\n defaultTtl: number;\n maxTtl: number;\n allowedSandboxTypes: SandboxType[];\n maxConcurrentPerUser: number;\n maxConcurrentPerWorkspace: number;\n defaultCpuRequest: string;\n defaultCpuLimit: string;\n defaultMemoryRequest: string;\n defaultMemoryLimit: string;\n}\n\n// ============================================================================\n// Mocked Stats (will be wired to query later)\n// ============================================================================\n\nconst MOCK_STATS = {\n totalActive: 12,\n totalQueued: 3,\n totalCompleted: 847,\n totalFailed: 14,\n};\n\n// ============================================================================\n// Helpers — Gateway URL derivation\n// ============================================================================\n\n/**\n * Derive the workspace gateway URL from the global gateway URL.\n *\n * Convention:\n * global gateway → https://graphql.burdenoff.com/global/graphql\n * wspace gateway → https://graphqlworkspaces.burdenoff.com/workspaces/graphql\n *\n * For local dev the global gateway is at localhost:4000 and the workspace\n * gateway is at localhost:4003, so we swap the port as a fallback.\n */\nfunction deriveWorkspaceGatewayUrl(apiGatewayUrl: string | undefined): string {\n if (!apiGatewayUrl) {\n return 'http://localhost:4003/workspaces/graphql';\n }\n\n // Local port-forward convention: 4000 → 4003\n if (apiGatewayUrl.includes('localhost:4000')) {\n return apiGatewayUrl\n .replace('localhost:4000', 'localhost:4003')\n .replace('/global/', '/workspaces/');\n }\n\n // Alpha / prod convention: alphagraphql → alphagraphqlworkspaces, graphql → graphqlworkspaces\n if (apiGatewayUrl.includes('/global/')) {\n return apiGatewayUrl\n .replace(/\\/\\/graphql\\./, '//graphqlworkspaces.')\n .replace(/\\/\\/alphagraphql\\./, '//alphagraphqlworkspaces.')\n .replace('/global/', '/workspaces/');\n }\n\n // Fallback: assume local dev\n return 'http://localhost:4003/workspaces/graphql';\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\n/**\n * Sandbox Settings page for superadmin sandbox configuration.\n * Manages platform-wide sandbox defaults including TTLs, resource limits,\n * allowed types, and concurrency caps.\n */\nexport function SandboxSettingsPage() {\n const { canManageSandbox, isLoading: permissionsLoading } = useAdminPermissions();\n const toast = useToast();\n const { apiGatewayUrl, authToken, tenantId } = useAdmin();\n const workspaceGatewayUrl = deriveWorkspaceGatewayUrl(apiGatewayUrl);\n\n const [config, setConfig] = useState<SandboxConfig>({\n defaultTtl: 3600,\n maxTtl: 14400,\n allowedSandboxTypes: [\n 'VIBECONTROLS',\n 'AI_BUILDER',\n 'WORKFLOW_MULTI_NODE',\n 'MISC_SHELL',\n 'CUSTOM',\n ],\n maxConcurrentPerUser: 3,\n maxConcurrentPerWorkspace: 10,\n defaultCpuRequest: '500m',\n defaultCpuLimit: '2',\n defaultMemoryRequest: '512Mi',\n defaultMemoryLimit: '4Gi',\n });\n\n const [isSaving, setIsSaving] = useState(false);\n const [isLoading, setIsLoading] = useState(false);\n\n // ---------------------------------------------------------------------------\n // Load config on mount\n // ---------------------------------------------------------------------------\n useEffect(() => {\n if (!tenantId || permissionsLoading || !canManageSandbox) return;\n\n // Auth tokens go to the gateway (edge) which validates and strips them.\n // X-Workspace-ID is forwarded to downstream subgraphs as workspace context.\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n if (authToken) headers['Authorization'] = `Bearer ${authToken}`;\n if (tenantId) headers['X-Workspace-ID'] = tenantId;\n\n setIsLoading(true);\n fetch(workspaceGatewayUrl, {\n method: 'POST',\n headers,\n body: JSON.stringify({\n query: `query GetSandboxConfig($context: ContextInput!) {\n getSandboxConfig(context: $context) {\n id\n defaultTtlSeconds\n maxTtlSeconds\n allowedTemplates\n maxConcurrentPerUser\n maxConcurrentPerWorkspace\n defaultCpuRequest\n defaultCpuLimit\n defaultMemoryRequest\n defaultMemoryLimit\n }\n }`,\n variables: { context: { workspaceId: tenantId } },\n }),\n })\n .then((r) => r.json())\n .then((result) => {\n if (result.data?.getSandboxConfig) {\n const c = result.data.getSandboxConfig;\n setConfig({\n defaultTtl: c.defaultTtlSeconds,\n maxTtl: c.maxTtlSeconds,\n allowedSandboxTypes: c.allowedTemplates,\n maxConcurrentPerUser: c.maxConcurrentPerUser,\n maxConcurrentPerWorkspace: c.maxConcurrentPerWorkspace,\n defaultCpuRequest: c.defaultCpuRequest,\n defaultCpuLimit: c.defaultCpuLimit,\n defaultMemoryRequest: c.defaultMemoryRequest,\n defaultMemoryLimit: c.defaultMemoryLimit,\n });\n }\n })\n .catch((err) => {\n console.error('Failed to load sandbox config:', err);\n toast.error('Load Failed', 'Failed to load sandbox configuration.');\n })\n .finally(() => setIsLoading(false));\n // eslint-disable-next-line react-hooks/exhaustive-deps -- toast excluded to avoid re-fetching when its reference changes\n }, [tenantId, authToken, workspaceGatewayUrl, permissionsLoading, canManageSandbox]);\n\n const updateConfig = useCallback(\n <K extends keyof SandboxConfig>(key: K, value: SandboxConfig[K]) => {\n setConfig((prev) => ({ ...prev, [key]: value }));\n },\n []\n );\n\n const toggleSandboxType = useCallback((type: SandboxType) => {\n setConfig((prev) => {\n const types = prev.allowedSandboxTypes.includes(type)\n ? prev.allowedSandboxTypes.filter((t) => t !== type)\n : [...prev.allowedSandboxTypes, type];\n return { ...prev, allowedSandboxTypes: types };\n });\n }, []);\n\n const handleSave = useCallback(async () => {\n // Validate max TTL >= default TTL\n if (config.maxTtl < config.defaultTtl) {\n toast.error('Validation Error', 'Max TTL must be greater than or equal to Default TTL.');\n return;\n }\n\n if (config.allowedSandboxTypes.length === 0) {\n toast.error('Validation Error', 'At least one sandbox type must be allowed.');\n return;\n }\n\n setIsSaving(true);\n try {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n if (authToken) headers['Authorization'] = `Bearer ${authToken}`;\n if (tenantId) headers['X-Workspace-ID'] = tenantId;\n\n const response = await fetch(workspaceGatewayUrl, {\n method: 'POST',\n headers,\n body: JSON.stringify({\n query: `mutation UpdateSandboxConfig($input: UpdateSandboxConfigInput!) {\n updateSandboxConfig(input: $input) {\n id\n defaultTtlSeconds\n maxTtlSeconds\n allowedTemplates\n maxConcurrentPerUser\n maxConcurrentPerWorkspace\n defaultCpuRequest\n defaultCpuLimit\n defaultMemoryRequest\n defaultMemoryLimit\n }\n }`,\n variables: {\n input: {\n defaultTtlSeconds: config.defaultTtl,\n maxTtlSeconds: config.maxTtl,\n allowedTemplates: config.allowedSandboxTypes,\n maxConcurrentPerUser: config.maxConcurrentPerUser,\n maxConcurrentPerWorkspace: config.maxConcurrentPerWorkspace,\n defaultCpuRequest: config.defaultCpuRequest,\n defaultCpuLimit: config.defaultCpuLimit,\n defaultMemoryRequest: config.defaultMemoryRequest,\n defaultMemoryLimit: config.defaultMemoryLimit,\n context: { workspaceId: tenantId },\n },\n },\n }),\n });\n\n const result = await response.json();\n if (result.errors) {\n throw new Error(result.errors[0]?.message || 'Failed to save sandbox configuration');\n }\n\n toast.success('Settings Saved', 'Sandbox configuration has been updated successfully.');\n } catch (err) {\n toast.error(\n 'Save Failed',\n err instanceof Error\n ? err.message\n : 'Failed to update sandbox configuration. Please try again.'\n );\n } finally {\n setIsSaving(false);\n }\n }, [config, toast, authToken, tenantId, workspaceGatewayUrl]);\n\n const selectClasses =\n 'h-10 w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm';\n\n return (\n <PermissionGuard\n allowed={canManageSandbox}\n isLoading={permissionsLoading}\n pageTitle=\"Sandbox Settings\"\n denyMessage=\"You don't have permission to manage sandbox settings.\"\n >\n <PageLayout\n title=\"Sandbox Settings\"\n gradientWord=\"Settings\"\n description=\"Configure platform-wide sandbox defaults and resource limits\"\n >\n <PagePurpose className=\"mb-6\">\n Sandboxes are the disposable, isolated environments users spin up to safely run or\n preview their code without touching anything real. This page sets the platform-wide\n defaults for them — how long a sandbox lives before it's cleaned up, how much CPU\n and memory it gets, and how many a user or tenant may run at once. Tune these to balance\n a smooth experience against runaway resource costs.\n </PagePurpose>\n\n <NextSteps\n storageKey=\"admin-sandbox\"\n steps={[\n {\n id: 'set-ttl',\n label: 'Set the sandbox TTL',\n description: 'Decide how long idle sandboxes live before automatic cleanup.',\n href: '#ttl',\n },\n {\n id: 'set-limits',\n label: 'Cap CPU & memory',\n description: 'Set the resource ceiling each sandbox is allowed.',\n href: '#resources',\n },\n {\n id: 'save-config',\n label: 'Save your configuration',\n description: 'Apply these defaults across the whole platform.',\n onClick: () => void handleSave(),\n },\n ]}\n />\n\n {/* Sandbox Stats */}\n <Card className=\"mb-6\">\n <CardHeader>\n <div className=\"flex items-center gap-2\">\n <Box className=\"h-5 w-5 text-text-secondary\" />\n <CardTitle className=\"text-lg\">Sandbox Stats</CardTitle>\n </div>\n <CardDescription>Current sandbox usage across the platform</CardDescription>\n </CardHeader>\n <CardContent>\n <MetricsGrid>\n <MetricCard title=\"Active Sandboxes\" hero value={MOCK_STATS.totalActive} trend=\"up\" />\n <MetricCard title=\"Queued\" value={MOCK_STATS.totalQueued} />\n <MetricCard title=\"Completed (all time)\" value={MOCK_STATS.totalCompleted} />\n <MetricCard title=\"Failed (all time)\" value={MOCK_STATS.totalFailed} trend=\"down\" />\n </MetricsGrid>\n </CardContent>\n </Card>\n\n <div className=\"grid gap-6 lg:grid-cols-3\">\n {/* Main settings */}\n <div className=\"space-y-6 lg:col-span-2\">\n {/* TTL Configuration */}\n <Card id=\"ttl\">\n <CardHeader>\n <div className=\"flex items-center gap-2\">\n <Timer className=\"h-5 w-5 text-text-secondary\" />\n <CardTitle className=\"text-lg\">Time-to-Live (TTL)</CardTitle>\n </div>\n <CardDescription>\n Control how long sandboxes stay alive before automatic cleanup\n </CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\">Default TTL</label>\n <select\n className={selectClasses}\n value={config.defaultTtl}\n onChange={(e) => updateConfig('defaultTtl', Number(e.target.value))}\n >\n {TTL_OPTIONS.map((opt) => (\n <option key={opt.value} value={opt.value}>\n {opt.label}\n </option>\n ))}\n </select>\n <p className=\"text-xs text-text-secondary\">\n Default lifetime for new sandboxes\n </p>\n </div>\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\">Max TTL</label>\n <select\n className={selectClasses}\n value={config.maxTtl}\n onChange={(e) => updateConfig('maxTtl', Number(e.target.value))}\n >\n {TTL_OPTIONS.map((opt) => (\n <option key={opt.value} value={opt.value}>\n {opt.label}\n </option>\n ))}\n </select>\n <p className=\"text-xs text-text-secondary\">\n Maximum allowed TTL (must be >= default)\n </p>\n </div>\n </div>\n </CardContent>\n </Card>\n\n {/* Allowed Sandbox Types */}\n <Card>\n <CardHeader>\n <div className=\"flex items-center gap-2\">\n <CheckSquare className=\"h-5 w-5 text-text-secondary\" />\n <CardTitle className=\"text-lg\">Allowed Sandbox Types</CardTitle>\n </div>\n <CardDescription>\n Select which sandbox types users are permitted to create\n </CardDescription>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-3\">\n {SANDBOX_TYPES.map((type) => (\n <label\n key={type}\n className=\"flex items-center gap-3 rounded-lg border border-border-subtle p-4 cursor-pointer hover:bg-bg-sunken/50 transition-colors\"\n >\n <input\n type=\"checkbox\"\n checked={config.allowedSandboxTypes.includes(type)}\n onChange={() => toggleSandboxType(type)}\n className=\"h-4 w-4 rounded border-border-default text-primary focus:ring-primary\"\n />\n <div>\n <div className=\"font-medium text-sm\">{formatSandboxType(type)}</div>\n <div className=\"text-xs text-text-secondary\">\n {getSandboxTypeDescription(type)}\n </div>\n </div>\n </label>\n ))}\n </div>\n </CardContent>\n </Card>\n\n {/* Resource Limits */}\n <Card id=\"resources\">\n <CardHeader>\n <div className=\"flex items-center gap-2\">\n <Cpu className=\"h-5 w-5 text-text-secondary\" />\n <CardTitle className=\"text-lg\">Default Resource Limits</CardTitle>\n </div>\n <CardDescription>\n Kubernetes resource requests and limits applied to new sandboxes\n </CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\">CPU Request</label>\n <Input\n value={config.defaultCpuRequest}\n onChange={(e) => updateConfig('defaultCpuRequest', e.target.value)}\n placeholder=\"500m\"\n />\n <p className=\"text-xs text-text-secondary\">\n Kubernetes CPU request (e.g. 500m, 1)\n </p>\n </div>\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\">CPU Limit</label>\n <Input\n value={config.defaultCpuLimit}\n onChange={(e) => updateConfig('defaultCpuLimit', e.target.value)}\n placeholder=\"2\"\n />\n <p className=\"text-xs text-text-secondary\">\n Kubernetes CPU limit (e.g. 1, 2, 4)\n </p>\n </div>\n </div>\n <Separator />\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\">Memory Request</label>\n <Input\n value={config.defaultMemoryRequest}\n onChange={(e) => updateConfig('defaultMemoryRequest', e.target.value)}\n placeholder=\"512Mi\"\n />\n <p className=\"text-xs text-text-secondary\">\n Kubernetes memory request (e.g. 512Mi, 1Gi)\n </p>\n </div>\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\">Memory Limit</label>\n <Input\n value={config.defaultMemoryLimit}\n onChange={(e) => updateConfig('defaultMemoryLimit', e.target.value)}\n placeholder=\"4Gi\"\n />\n <p className=\"text-xs text-text-secondary\">\n Kubernetes memory limit (e.g. 2Gi, 4Gi, 8Gi)\n </p>\n </div>\n </div>\n </CardContent>\n </Card>\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-6\">\n {/* Concurrency Limits */}\n <Card>\n <CardHeader>\n <div className=\"flex items-center gap-2\">\n <Users className=\"h-5 w-5 text-text-secondary\" />\n <CardTitle className=\"text-lg\">Concurrency</CardTitle>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\">Max Per User</label>\n <Input\n type=\"number\"\n min={1}\n max={50}\n value={config.maxConcurrentPerUser}\n onChange={(e) => updateConfig('maxConcurrentPerUser', Number(e.target.value))}\n />\n <p className=\"text-xs text-text-secondary\">\n Maximum concurrent sandboxes per user\n </p>\n </div>\n <Separator />\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\">Max Per Workspace</label>\n <Input\n type=\"number\"\n min={1}\n max={200}\n value={config.maxConcurrentPerWorkspace}\n onChange={(e) =>\n updateConfig('maxConcurrentPerWorkspace', Number(e.target.value))\n }\n />\n <p className=\"text-xs text-text-secondary\">\n Maximum concurrent sandboxes per workspace\n </p>\n </div>\n </CardContent>\n </Card>\n\n {/* Resource Presets Info */}\n <Card>\n <CardHeader>\n <div className=\"flex items-center gap-2\">\n <MemoryStick className=\"h-5 w-5 text-text-secondary\" />\n <CardTitle className=\"text-lg\">Resource Info</CardTitle>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <p className=\"text-sm text-text-secondary\">\n Resource defaults are applied to all new sandboxes. Individual sandboxes can\n override these within the max limits defined by the sandbox type.\n </p>\n <Separator />\n <div className=\"space-y-2 text-xs text-text-secondary\">\n <div className=\"flex justify-between\">\n <span>CPU units</span>\n <span className=\"font-mono\">millicores (m)</span>\n </div>\n <div className=\"flex justify-between\">\n <span>Memory units</span>\n <span className=\"font-mono\">Mi / Gi</span>\n </div>\n </div>\n </CardContent>\n </Card>\n\n {/* Save Button */}\n <Card>\n <CardContent className=\"p-6\">\n <Button className=\"w-full\" onClick={handleSave} disabled={isSaving || isLoading}>\n <Save className=\"mr-2 h-4 w-4\" />\n {isSaving ? 'Saving...' : isLoading ? 'Loading...' : 'Save Configuration'}\n </Button>\n </CardContent>\n </Card>\n </div>\n </div>\n </PageLayout>\n </PermissionGuard>\n );\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction formatSandboxType(type: SandboxType): string {\n const labels: Record<SandboxType, string> = {\n VIBECONTROLS: 'VibeControls',\n AI_BUILDER: 'AI Builder',\n WORKFLOW_MULTI_NODE: 'Workflow (Multi-Node)',\n MISC_SHELL: 'Misc Shell',\n CUSTOM: 'Custom',\n };\n return labels[type];\n}\n\nfunction getSandboxTypeDescription(type: SandboxType): string {\n const descriptions: Record<SandboxType, string> = {\n VIBECONTROLS: 'Full VibeControls development environment with pre-configured tooling',\n AI_BUILDER: 'AI agent development sandbox with OpenCode SDK and model access',\n WORKFLOW_MULTI_NODE: 'Multi-node workflow execution environment for complex pipelines',\n MISC_SHELL: 'General-purpose shell environment for ad-hoc tasks',\n CUSTOM: 'User-defined sandbox from custom container images',\n };\n return descriptions[type];\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SandboxSettingsPage.js","names":[],"sources":["../../../src/admin/pages/SandboxSettingsPage.tsx"],"sourcesContent":["import { useState, useCallback, useEffect } from 'react';\nimport { Box, Cpu, MemoryStick, Timer, Users, CheckSquare, Save } from 'lucide-react';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport {\n PageLayout,\n Card,\n CardHeader,\n CardTitle,\n CardContent,\n CardDescription,\n Button,\n Input,\n Separator,\n MetricsGrid,\n MetricCard,\n useToast,\n PermissionGuard,\n} from '../components';\nimport { PagePurpose, NextSteps } from '@burdenoff/fe-libs/ui';\nimport { useAdmin } from '../providers/AdminProvider';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nconst SANDBOX_TYPES = [\n 'VIBECONTROLS',\n 'AI_BUILDER',\n 'WORKFLOW_MULTI_NODE',\n 'MISC_SHELL',\n 'CUSTOM',\n] as const;\n\ntype SandboxType = (typeof SANDBOX_TYPES)[number];\n\nconst TTL_OPTIONS = [\n { label: '30 minutes', value: 1800 },\n { label: '1 hour', value: 3600 },\n { label: '2 hours', value: 7200 },\n { label: '4 hours', value: 14400 },\n { label: '8 hours', value: 28800 },\n { label: '24 hours', value: 86400 },\n] as const;\n\ninterface SandboxConfig {\n defaultTtl: number;\n maxTtl: number;\n allowedSandboxTypes: SandboxType[];\n maxConcurrentPerUser: number;\n maxConcurrentPerWorkspace: number;\n defaultCpuRequest: string;\n defaultCpuLimit: string;\n defaultMemoryRequest: string;\n defaultMemoryLimit: string;\n}\n\n// ============================================================================\n// Mocked Stats (will be wired to query later)\n// ============================================================================\n\nconst MOCK_STATS = {\n totalActive: 12,\n totalQueued: 3,\n totalCompleted: 847,\n totalFailed: 14,\n};\n\n// ============================================================================\n// Helpers — Gateway URL derivation\n// ============================================================================\n\n/**\n * Derive the workspace gateway URL from the global gateway URL.\n *\n * Convention:\n * global gateway → https://graphql.burdenoff.com/global/graphql\n * wspace gateway → https://graphqlworkspaces.burdenoff.com/workspaces/graphql\n *\n * For local dev the global gateway is at localhost:4000 and the workspace\n * gateway is at localhost:4003, so we swap the port as a fallback.\n */\nfunction deriveWorkspaceGatewayUrl(apiGatewayUrl: string | undefined): string {\n if (!apiGatewayUrl) {\n return 'http://localhost:4003/workspaces/graphql';\n }\n\n // Local port-forward convention: 4000 → 4003\n if (apiGatewayUrl.includes('localhost:4000')) {\n return apiGatewayUrl\n .replace('localhost:4000', 'localhost:4003')\n .replace('/global/', '/workspaces/');\n }\n\n // Alpha / prod convention: alphagraphql → alphagraphqlworkspaces, graphql → graphqlworkspaces\n if (apiGatewayUrl.includes('/global/')) {\n return apiGatewayUrl\n .replace(/\\/\\/graphql\\./, '//graphqlworkspaces.')\n .replace(/\\/\\/alphagraphql\\./, '//alphagraphqlworkspaces.')\n .replace('/global/', '/workspaces/');\n }\n\n // Fallback: assume local dev\n return 'http://localhost:4003/workspaces/graphql';\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\n/**\n * Sandbox Settings page for superadmin sandbox configuration.\n * Manages platform-wide sandbox defaults including TTLs, resource limits,\n * allowed types, and concurrency caps.\n */\nexport function SandboxSettingsPage() {\n const { canManageSandbox, isLoading: permissionsLoading } = useAdminPermissions();\n const toast = useToast();\n const { apiGatewayUrl, authToken, tenantId } = useAdmin();\n const workspaceGatewayUrl = deriveWorkspaceGatewayUrl(apiGatewayUrl);\n\n const [config, setConfig] = useState<SandboxConfig>({\n defaultTtl: 3600,\n maxTtl: 14400,\n allowedSandboxTypes: [\n 'VIBECONTROLS',\n 'AI_BUILDER',\n 'WORKFLOW_MULTI_NODE',\n 'MISC_SHELL',\n 'CUSTOM',\n ],\n maxConcurrentPerUser: 3,\n maxConcurrentPerWorkspace: 10,\n defaultCpuRequest: '500m',\n defaultCpuLimit: '2',\n defaultMemoryRequest: '512Mi',\n defaultMemoryLimit: '4Gi',\n });\n\n const [isSaving, setIsSaving] = useState(false);\n const [isLoading, setIsLoading] = useState(false);\n\n // ---------------------------------------------------------------------------\n // Load config on mount\n // ---------------------------------------------------------------------------\n useEffect(() => {\n if (!tenantId || permissionsLoading || !canManageSandbox) return;\n\n // Auth tokens go to the gateway (edge) which validates and strips them.\n // X-Workspace-ID is forwarded to downstream subgraphs as workspace context.\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n if (authToken) headers['Authorization'] = `Bearer ${authToken}`;\n if (tenantId) headers['X-Workspace-ID'] = tenantId;\n\n setIsLoading(true);\n fetch(workspaceGatewayUrl, {\n method: 'POST',\n headers,\n body: JSON.stringify({\n query: `query GetSandboxConfig($context: ContextInput!) {\n getSandboxConfig(context: $context) {\n id\n defaultTtlSeconds\n maxTtlSeconds\n allowedTemplates\n maxConcurrentPerUser\n maxConcurrentPerWorkspace\n defaultCpuRequest\n defaultCpuLimit\n defaultMemoryRequest\n defaultMemoryLimit\n }\n }`,\n variables: { context: { workspaceId: tenantId } },\n }),\n })\n .then((r) => r.json())\n .then((result) => {\n if (result.data?.getSandboxConfig) {\n const c = result.data.getSandboxConfig;\n setConfig({\n defaultTtl: c.defaultTtlSeconds,\n maxTtl: c.maxTtlSeconds,\n allowedSandboxTypes: c.allowedTemplates,\n maxConcurrentPerUser: c.maxConcurrentPerUser,\n maxConcurrentPerWorkspace: c.maxConcurrentPerWorkspace,\n defaultCpuRequest: c.defaultCpuRequest,\n defaultCpuLimit: c.defaultCpuLimit,\n defaultMemoryRequest: c.defaultMemoryRequest,\n defaultMemoryLimit: c.defaultMemoryLimit,\n });\n }\n })\n .catch((err) => {\n console.error('Failed to load sandbox config:', err);\n toast.error('Load Failed', 'Failed to load sandbox configuration.');\n })\n .finally(() => setIsLoading(false));\n // eslint-disable-next-line react-hooks/exhaustive-deps -- toast excluded to avoid re-fetching when its reference changes\n }, [tenantId, authToken, workspaceGatewayUrl, permissionsLoading, canManageSandbox]);\n\n const updateConfig = useCallback(\n <K extends keyof SandboxConfig>(key: K, value: SandboxConfig[K]) => {\n setConfig((prev) => ({ ...prev, [key]: value }));\n },\n []\n );\n\n const toggleSandboxType = useCallback((type: SandboxType) => {\n setConfig((prev) => {\n const types = prev.allowedSandboxTypes.includes(type)\n ? prev.allowedSandboxTypes.filter((t) => t !== type)\n : [...prev.allowedSandboxTypes, type];\n return { ...prev, allowedSandboxTypes: types };\n });\n }, []);\n\n const handleSave = useCallback(async () => {\n // Validate max TTL >= default TTL\n if (config.maxTtl < config.defaultTtl) {\n toast.error('Validation Error', 'Max TTL must be greater than or equal to Default TTL.');\n return;\n }\n\n if (config.allowedSandboxTypes.length === 0) {\n toast.error('Validation Error', 'At least one sandbox type must be allowed.');\n return;\n }\n\n setIsSaving(true);\n try {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n if (authToken) headers['Authorization'] = `Bearer ${authToken}`;\n if (tenantId) headers['X-Workspace-ID'] = tenantId;\n\n const response = await fetch(workspaceGatewayUrl, {\n method: 'POST',\n headers,\n body: JSON.stringify({\n query: `mutation UpdateSandboxConfig($input: UpdateSandboxConfigInput!) {\n updateSandboxConfig(input: $input) {\n id\n defaultTtlSeconds\n maxTtlSeconds\n allowedTemplates\n maxConcurrentPerUser\n maxConcurrentPerWorkspace\n defaultCpuRequest\n defaultCpuLimit\n defaultMemoryRequest\n defaultMemoryLimit\n }\n }`,\n variables: {\n input: {\n defaultTtlSeconds: config.defaultTtl,\n maxTtlSeconds: config.maxTtl,\n allowedTemplates: config.allowedSandboxTypes,\n maxConcurrentPerUser: config.maxConcurrentPerUser,\n maxConcurrentPerWorkspace: config.maxConcurrentPerWorkspace,\n defaultCpuRequest: config.defaultCpuRequest,\n defaultCpuLimit: config.defaultCpuLimit,\n defaultMemoryRequest: config.defaultMemoryRequest,\n defaultMemoryLimit: config.defaultMemoryLimit,\n context: { workspaceId: tenantId },\n },\n },\n }),\n });\n\n const result = await response.json();\n if (result.errors) {\n throw new Error(result.errors[0]?.message || 'Failed to save sandbox configuration');\n }\n\n toast.success('Settings Saved', 'Sandbox configuration has been updated successfully.');\n } catch (err) {\n toast.error(\n 'Save Failed',\n err instanceof Error\n ? err.message\n : 'Failed to update sandbox configuration. Please try again.'\n );\n } finally {\n setIsSaving(false);\n }\n }, [config, toast, authToken, tenantId, workspaceGatewayUrl]);\n\n const selectClasses =\n 'h-10 w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm';\n\n return (\n <PermissionGuard\n allowed={canManageSandbox}\n isLoading={permissionsLoading}\n pageTitle=\"Sandbox Settings\"\n denyMessage=\"You don't have permission to manage sandbox settings.\"\n >\n <PageLayout\n title=\"Sandbox Settings\"\n gradientWord=\"Settings\"\n description=\"Configure platform-wide sandbox defaults and resource limits\"\n >\n <PagePurpose className=\"mb-6\">\n Sandboxes are the disposable, isolated environments users spin up to safely run or preview\n their code without touching anything real. This page sets the platform-wide defaults for\n them — how long a sandbox lives before it's cleaned up, how much CPU and memory it\n gets, and how many a user or tenant may run at once. Tune these to balance a smooth\n experience against runaway resource costs.\n </PagePurpose>\n\n <NextSteps\n storageKey=\"admin-sandbox\"\n steps={[\n {\n id: 'set-ttl',\n label: 'Set the sandbox TTL',\n description: 'Decide how long idle sandboxes live before automatic cleanup.',\n href: '#ttl',\n },\n {\n id: 'set-limits',\n label: 'Cap CPU & memory',\n description: 'Set the resource ceiling each sandbox is allowed.',\n href: '#resources',\n },\n {\n id: 'save-config',\n label: 'Save your configuration',\n description: 'Apply these defaults across the whole platform.',\n onClick: () => void handleSave(),\n },\n ]}\n />\n\n {/* Sandbox Stats */}\n <Card className=\"mb-6\">\n <CardHeader>\n <div className=\"flex items-center gap-2\">\n <Box className=\"h-5 w-5 text-text-secondary\" />\n <CardTitle className=\"text-lg\">Sandbox Stats</CardTitle>\n </div>\n <CardDescription>Current sandbox usage across the platform</CardDescription>\n </CardHeader>\n <CardContent>\n <MetricsGrid>\n <MetricCard title=\"Active Sandboxes\" hero value={MOCK_STATS.totalActive} trend=\"up\" />\n <MetricCard title=\"Queued\" value={MOCK_STATS.totalQueued} />\n <MetricCard title=\"Completed (all time)\" value={MOCK_STATS.totalCompleted} />\n <MetricCard title=\"Failed (all time)\" value={MOCK_STATS.totalFailed} trend=\"down\" />\n </MetricsGrid>\n </CardContent>\n </Card>\n\n <div className=\"grid gap-6 lg:grid-cols-3\">\n {/* Main settings */}\n <div className=\"space-y-6 lg:col-span-2\">\n {/* TTL Configuration */}\n <Card id=\"ttl\">\n <CardHeader>\n <div className=\"flex items-center gap-2\">\n <Timer className=\"h-5 w-5 text-text-secondary\" />\n <CardTitle className=\"text-lg\">Time-to-Live (TTL)</CardTitle>\n </div>\n <CardDescription>\n Control how long sandboxes stay alive before automatic cleanup\n </CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\">Default TTL</label>\n <select\n className={selectClasses}\n value={config.defaultTtl}\n onChange={(e) => updateConfig('defaultTtl', Number(e.target.value))}\n >\n {TTL_OPTIONS.map((opt) => (\n <option key={opt.value} value={opt.value}>\n {opt.label}\n </option>\n ))}\n </select>\n <p className=\"text-xs text-text-secondary\">\n Default lifetime for new sandboxes\n </p>\n </div>\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\">Max TTL</label>\n <select\n className={selectClasses}\n value={config.maxTtl}\n onChange={(e) => updateConfig('maxTtl', Number(e.target.value))}\n >\n {TTL_OPTIONS.map((opt) => (\n <option key={opt.value} value={opt.value}>\n {opt.label}\n </option>\n ))}\n </select>\n <p className=\"text-xs text-text-secondary\">\n Maximum allowed TTL (must be >= default)\n </p>\n </div>\n </div>\n </CardContent>\n </Card>\n\n {/* Allowed Sandbox Types */}\n <Card>\n <CardHeader>\n <div className=\"flex items-center gap-2\">\n <CheckSquare className=\"h-5 w-5 text-text-secondary\" />\n <CardTitle className=\"text-lg\">Allowed Sandbox Types</CardTitle>\n </div>\n <CardDescription>\n Select which sandbox types users are permitted to create\n </CardDescription>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-3\">\n {SANDBOX_TYPES.map((type) => (\n <label\n key={type}\n className=\"flex items-center gap-3 rounded-lg border border-border-subtle p-4 cursor-pointer hover:bg-bg-sunken/50 transition-colors\"\n >\n <input\n type=\"checkbox\"\n checked={config.allowedSandboxTypes.includes(type)}\n onChange={() => toggleSandboxType(type)}\n className=\"h-4 w-4 rounded border-border-default text-primary focus:ring-primary\"\n />\n <div>\n <div className=\"font-medium text-sm\">{formatSandboxType(type)}</div>\n <div className=\"text-xs text-text-secondary\">\n {getSandboxTypeDescription(type)}\n </div>\n </div>\n </label>\n ))}\n </div>\n </CardContent>\n </Card>\n\n {/* Resource Limits */}\n <Card id=\"resources\">\n <CardHeader>\n <div className=\"flex items-center gap-2\">\n <Cpu className=\"h-5 w-5 text-text-secondary\" />\n <CardTitle className=\"text-lg\">Default Resource Limits</CardTitle>\n </div>\n <CardDescription>\n Kubernetes resource requests and limits applied to new sandboxes\n </CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\">CPU Request</label>\n <Input\n value={config.defaultCpuRequest}\n onChange={(e) => updateConfig('defaultCpuRequest', e.target.value)}\n placeholder=\"500m\"\n />\n <p className=\"text-xs text-text-secondary\">\n Kubernetes CPU request (e.g. 500m, 1)\n </p>\n </div>\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\">CPU Limit</label>\n <Input\n value={config.defaultCpuLimit}\n onChange={(e) => updateConfig('defaultCpuLimit', e.target.value)}\n placeholder=\"2\"\n />\n <p className=\"text-xs text-text-secondary\">\n Kubernetes CPU limit (e.g. 1, 2, 4)\n </p>\n </div>\n </div>\n <Separator />\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\">Memory Request</label>\n <Input\n value={config.defaultMemoryRequest}\n onChange={(e) => updateConfig('defaultMemoryRequest', e.target.value)}\n placeholder=\"512Mi\"\n />\n <p className=\"text-xs text-text-secondary\">\n Kubernetes memory request (e.g. 512Mi, 1Gi)\n </p>\n </div>\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\">Memory Limit</label>\n <Input\n value={config.defaultMemoryLimit}\n onChange={(e) => updateConfig('defaultMemoryLimit', e.target.value)}\n placeholder=\"4Gi\"\n />\n <p className=\"text-xs text-text-secondary\">\n Kubernetes memory limit (e.g. 2Gi, 4Gi, 8Gi)\n </p>\n </div>\n </div>\n </CardContent>\n </Card>\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-6\">\n {/* Concurrency Limits */}\n <Card>\n <CardHeader>\n <div className=\"flex items-center gap-2\">\n <Users className=\"h-5 w-5 text-text-secondary\" />\n <CardTitle className=\"text-lg\">Concurrency</CardTitle>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\">Max Per User</label>\n <Input\n type=\"number\"\n min={1}\n max={50}\n value={config.maxConcurrentPerUser}\n onChange={(e) => updateConfig('maxConcurrentPerUser', Number(e.target.value))}\n />\n <p className=\"text-xs text-text-secondary\">\n Maximum concurrent sandboxes per user\n </p>\n </div>\n <Separator />\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\">Max Per Workspace</label>\n <Input\n type=\"number\"\n min={1}\n max={200}\n value={config.maxConcurrentPerWorkspace}\n onChange={(e) =>\n updateConfig('maxConcurrentPerWorkspace', Number(e.target.value))\n }\n />\n <p className=\"text-xs text-text-secondary\">\n Maximum concurrent sandboxes per workspace\n </p>\n </div>\n </CardContent>\n </Card>\n\n {/* Resource Presets Info */}\n <Card>\n <CardHeader>\n <div className=\"flex items-center gap-2\">\n <MemoryStick className=\"h-5 w-5 text-text-secondary\" />\n <CardTitle className=\"text-lg\">Resource Info</CardTitle>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <p className=\"text-sm text-text-secondary\">\n Resource defaults are applied to all new sandboxes. Individual sandboxes can\n override these within the max limits defined by the sandbox type.\n </p>\n <Separator />\n <div className=\"space-y-2 text-xs text-text-secondary\">\n <div className=\"flex justify-between\">\n <span>CPU units</span>\n <span className=\"font-mono\">millicores (m)</span>\n </div>\n <div className=\"flex justify-between\">\n <span>Memory units</span>\n <span className=\"font-mono\">Mi / Gi</span>\n </div>\n </div>\n </CardContent>\n </Card>\n\n {/* Save Button */}\n <Card>\n <CardContent className=\"p-6\">\n <Button className=\"w-full\" onClick={handleSave} disabled={isSaving || isLoading}>\n <Save className=\"mr-2 h-4 w-4\" />\n {isSaving ? 'Saving...' : isLoading ? 'Loading...' : 'Save Configuration'}\n </Button>\n </CardContent>\n </Card>\n </div>\n </div>\n </PageLayout>\n </PermissionGuard>\n );\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction formatSandboxType(type: SandboxType): string {\n const labels: Record<SandboxType, string> = {\n VIBECONTROLS: 'VibeControls',\n AI_BUILDER: 'AI Builder',\n WORKFLOW_MULTI_NODE: 'Workflow (Multi-Node)',\n MISC_SHELL: 'Misc Shell',\n CUSTOM: 'Custom',\n };\n return labels[type];\n}\n\nfunction getSandboxTypeDescription(type: SandboxType): string {\n const descriptions: Record<SandboxType, string> = {\n VIBECONTROLS: 'Full VibeControls development environment with pre-configured tooling',\n AI_BUILDER: 'AI agent development sandbox with OpenCode SDK and model access',\n WORKFLOW_MULTI_NODE: 'Multi-node workflow execution environment for complex pipelines',\n MISC_SHELL: 'General-purpose shell environment for ad-hoc tasks',\n CUSTOM: 'User-defined sandbox from custom container images',\n };\n return descriptions[type];\n}\n"],"mappings":"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"}
|
|
@@ -6,11 +6,11 @@ import { PermissionGuard as d } from "../components/PermissionGuard.js";
|
|
|
6
6
|
import "../components/index.js";
|
|
7
7
|
import { Bell as f, Database as p, Globe as m, Key as h, Settings as g, Shield as _ } from "lucide-react";
|
|
8
8
|
import { jsx as v, jsxs as y } from "react/jsx-runtime";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { useI18n as b } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
10
|
+
import { EmphasisPanel as x, PagePurpose as S, ShowMore as C } from "@burdenoff/fe-libs/ui";
|
|
11
11
|
//#region src/admin/pages/SettingsPage.tsx
|
|
12
12
|
function w() {
|
|
13
|
-
let { canManageSettings: w, isLoading: T } = e(), { t: E } =
|
|
13
|
+
let { canManageSettings: w, isLoading: T } = e(), { t: E } = b(), D = (e, t, n) => u(E, e, t, n), O = [
|
|
14
14
|
{
|
|
15
15
|
key: "newTenantSignups",
|
|
16
16
|
label: D("admin.settings.notifications.items.newTenantSignups.label", "New tenant signups"),
|
|
@@ -41,7 +41,7 @@ function w() {
|
|
|
41
41
|
title: D("admin.settings.title", "Settings"),
|
|
42
42
|
gradientWord: D("admin.settings.title", "Settings"),
|
|
43
43
|
description: D("admin.settings.description", "Configure platform settings and preferences"),
|
|
44
|
-
children: [/* @__PURE__ */ v(
|
|
44
|
+
children: [/* @__PURE__ */ v(S, {
|
|
45
45
|
className: "mb-6",
|
|
46
46
|
children: D("admin.settings.purpose", "Platform-wide configuration in one place — the general details every product inherits, security defaults, API access, and which events page you. Change these to set the baseline behaviour for the whole platform, not a single tenant.")
|
|
47
47
|
}), /* @__PURE__ */ y("div", {
|
|
@@ -49,7 +49,7 @@ function w() {
|
|
|
49
49
|
children: [/* @__PURE__ */ y("div", {
|
|
50
50
|
className: "space-y-6 lg:col-span-2",
|
|
51
51
|
children: [
|
|
52
|
-
/* @__PURE__ */ v(
|
|
52
|
+
/* @__PURE__ */ v(x, {
|
|
53
53
|
className: "p-0",
|
|
54
54
|
children: /* @__PURE__ */ y(n, {
|
|
55
55
|
className: "border-0 bg-transparent shadow-none hover:border-0",
|
|
@@ -193,7 +193,7 @@ function w() {
|
|
|
193
193
|
className: "text-lg",
|
|
194
194
|
children: D("admin.settings.notifications.title", "Notifications")
|
|
195
195
|
})]
|
|
196
|
-
}), /* @__PURE__ */ v(i, { children: D("admin.settings.notifications.description", "Configure notification preferences") })] }), /* @__PURE__ */ v(r, { children: /* @__PURE__ */ v(
|
|
196
|
+
}), /* @__PURE__ */ v(i, { children: D("admin.settings.notifications.description", "Configure notification preferences") })] }), /* @__PURE__ */ v(r, { children: /* @__PURE__ */ v(C, {
|
|
197
197
|
collapsedHeight: 180,
|
|
198
198
|
label: "More notification settings",
|
|
199
199
|
children: /* @__PURE__ */ v("div", {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SettingsPage.js","names":[],"sources":["../../../src/admin/pages/SettingsPage.tsx"],"sourcesContent":["import { Settings, Shield, Key, Bell, Globe, Database } from 'lucide-react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { EmphasisPanel, ShowMore, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n Card,\n CardHeader,\n CardTitle,\n CardContent,\n CardDescription,\n Button,\n Input,\n Separator,\n PermissionGuard,\n} from '../components';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport { tWithFallback } from '../utils/i18n';\n\n/**\n * Settings page\n */\nexport function SettingsPage() {\n const { canManageSettings, isLoading: permissionsLoading } = useAdminPermissions();\n const { t } = useI18n();\n const tr = (key: string, fallback: string, params?: Record<string, string | number>): string =>\n tWithFallback(t, key, fallback, params);\n\n const notificationSettings = [\n {\n key: 'newTenantSignups',\n label: tr('admin.settings.notifications.items.newTenantSignups.label', 'New tenant signups'),\n description: tr(\n 'admin.settings.notifications.items.newTenantSignups.description',\n 'Get notified when new tenants register'\n ),\n },\n {\n key: 'paymentFailures',\n label: tr('admin.settings.notifications.items.paymentFailures.label', 'Payment failures'),\n description: tr(\n 'admin.settings.notifications.items.paymentFailures.description',\n 'Alert when payments fail'\n ),\n },\n {\n key: 'criticalTickets',\n label: tr('admin.settings.notifications.items.criticalTickets.label', 'Critical tickets'),\n description: tr(\n 'admin.settings.notifications.items.criticalTickets.description',\n 'Notify on critical support tickets'\n ),\n },\n {\n key: 'systemAlerts',\n label: tr('admin.settings.notifications.items.systemAlerts.label', 'System alerts'),\n description: tr(\n 'admin.settings.notifications.items.systemAlerts.description',\n 'Infrastructure and health alerts'\n ),\n },\n ];\n\n return (\n <PermissionGuard\n allowed={canManageSettings}\n isLoading={permissionsLoading}\n pageTitle={tr('admin.settings.title', 'Settings')}\n denyMessage={tr(\n 'admin.settings.denyMessage',\n \"You don't have permission to manage platform settings.\"\n )}\n >\n <PageLayout\n title={tr('admin.settings.title', 'Settings')}\n gradientWord={tr('admin.settings.title', 'Settings')}\n description={tr(\n 'admin.settings.description',\n 'Configure platform settings and preferences'\n )}\n >\n <PagePurpose className=\"mb-6\">\n {tr(\n 'admin.settings.purpose',\n 'Platform-wide configuration in one place — the general details every product inherits, security defaults, API access, and which events page you. Change these to set the baseline behaviour for the whole platform, not a single tenant.'\n )}\n </PagePurpose>\n\n <div className=\"grid gap-6 lg:grid-cols-3\">\n {/* Main settings */}\n <div className=\"space-y-6 lg:col-span-2\">\n {/* General — primary settings zone (emphasis) */}\n <EmphasisPanel className=\"p-0\">\n <Card className=\"border-0 bg-transparent shadow-none hover:border-0\">\n <CardHeader>\n <div className=\"flex items-center gap-2\">\n <Settings className=\"h-5 w-5 text-text-secondary\" />\n <CardTitle className=\"text-lg\">\n {tr('admin.settings.general.title', 'General')}\n </CardTitle>\n </div>\n <CardDescription>\n {tr('admin.settings.general.description', 'Basic platform configuration')}\n </CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\">\n {tr('admin.settings.general.platformName', 'Platform Name')}\n </label>\n <Input defaultValue=\"Burdenoff Admin\" />\n </div>\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\">\n {tr('admin.settings.general.supportEmail', 'Support Email')}\n </label>\n <Input defaultValue=\"support@burdenoff.com\" type=\"email\" />\n </div>\n </div>\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\">\n {tr('admin.settings.general.defaultTimezone', 'Default Timezone')}\n </label>\n <select className=\"h-10 w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm\">\n <option value=\"UTC\">UTC</option>\n <option value=\"America/New_York\">\n {tr('admin.settings.general.timezones.eastern', 'Eastern Time (ET)')}\n </option>\n <option value=\"America/Los_Angeles\">\n {tr('admin.settings.general.timezones.pacific', 'Pacific Time (PT)')}\n </option>\n <option value=\"Europe/London\">\n {tr('admin.settings.general.timezones.london', 'London (GMT)')}\n </option>\n <option value=\"Asia/Kolkata\">\n {tr('admin.settings.general.timezones.india', 'India (IST)')}\n </option>\n </select>\n </div>\n <div className=\"flex justify-end\">\n <Button>{tr('admin.settings.general.saveChanges', 'Save Changes')}</Button>\n </div>\n </CardContent>\n </Card>\n </EmphasisPanel>\n\n {/* Security */}\n <Card>\n <CardHeader>\n <div className=\"flex items-center gap-2\">\n <Shield className=\"h-5 w-5 text-text-secondary\" />\n <CardTitle className=\"text-lg\">\n {tr('admin.settings.security.title', 'Security')}\n </CardTitle>\n </div>\n <CardDescription>\n {tr(\n 'admin.settings.security.description',\n 'Security and authentication settings'\n )}\n </CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle p-4\">\n <div>\n <div className=\"font-medium\">\n {tr('admin.settings.security.twoFactor.title', 'Two-Factor Authentication')}\n </div>\n <div className=\"text-sm text-text-secondary\">\n {tr(\n 'admin.settings.security.twoFactor.description',\n 'Require 2FA for all admin accounts'\n )}\n </div>\n </div>\n <Button variant=\"outline\">\n {tr('admin.settings.security.twoFactor.configure', 'Configure')}\n </Button>\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle p-4\">\n <div>\n <div className=\"font-medium\">\n {tr('admin.settings.security.sessionTimeout.title', 'Session Timeout')}\n </div>\n <div className=\"text-sm text-text-secondary\">\n {tr(\n 'admin.settings.security.sessionTimeout.description',\n 'Auto-logout after inactivity'\n )}\n </div>\n </div>\n <select className=\"h-9 rounded-md border border-border-default bg-bg-surface px-3 text-sm\">\n <option value=\"15\">\n {tr('admin.settings.security.sessionTimeout.options.15m', '15 minutes')}\n </option>\n <option value=\"30\">\n {tr('admin.settings.security.sessionTimeout.options.30m', '30 minutes')}\n </option>\n <option value=\"60\">\n {tr('admin.settings.security.sessionTimeout.options.1h', '1 hour')}\n </option>\n <option value=\"480\">\n {tr('admin.settings.security.sessionTimeout.options.8h', '8 hours')}\n </option>\n </select>\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle p-4\">\n <div>\n <div className=\"font-medium\">\n {tr('admin.settings.security.ipAllowlist.title', 'IP Allowlist')}\n </div>\n <div className=\"text-sm text-text-secondary\">\n {tr(\n 'admin.settings.security.ipAllowlist.description',\n 'Restrict access to specific IPs'\n )}\n </div>\n </div>\n <Button variant=\"outline\">\n {tr('admin.settings.security.ipAllowlist.manage', 'Manage')}\n </Button>\n </div>\n </CardContent>\n </Card>\n\n {/* Notifications */}\n <Card>\n <CardHeader>\n <div className=\"flex items-center gap-2\">\n <Bell className=\"h-5 w-5 text-text-secondary\" />\n <CardTitle className=\"text-lg\">\n {tr('admin.settings.notifications.title', 'Notifications')}\n </CardTitle>\n </div>\n <CardDescription>\n {tr(\n 'admin.settings.notifications.description',\n 'Configure notification preferences'\n )}\n </CardDescription>\n </CardHeader>\n <CardContent>\n <ShowMore collapsedHeight={180} label=\"More notification settings\">\n <div className=\"space-y-4\">\n {notificationSettings.map((item) => (\n <div\n key={item.key}\n className=\"flex items-center justify-between rounded-lg border border-border-seam p-4\"\n >\n <div>\n <div className=\"font-medium\">{item.label}</div>\n <div className=\"text-sm text-text-secondary\">{item.description}</div>\n </div>\n <label className=\"relative inline-flex cursor-pointer items-center\">\n <input type=\"checkbox\" defaultChecked className=\"peer sr-only\" />\n <div className=\"peer h-6 w-11 rounded-full bg-bg-sunken after:absolute after:left-[2px] after:top-0.5 after:h-5 after:w-5 after:rounded-full after:bg-bg-surface after:transition-all peer-checked:bg-action-primary-bg peer-checked:after:translate-x-full peer-focus:ring-2 peer-focus:ring-[var(--color-focus-ring)]\" />\n </label>\n </div>\n ))}\n </div>\n </ShowMore>\n </CardContent>\n </Card>\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-6\">\n {/* API Keys */}\n <Card>\n <CardHeader>\n <div className=\"flex items-center gap-2\">\n <Key className=\"h-5 w-5 text-text-secondary\" />\n <CardTitle className=\"text-lg\">\n {tr('admin.settings.apiKeys.title', 'API Keys')}\n </CardTitle>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'admin.settings.apiKeys.description',\n 'Manage API keys for programmatic access to the admin API.'\n )}\n </p>\n <Button variant=\"outline\" className=\"w-full\">\n {tr('admin.settings.apiKeys.manage', 'Manage API Keys')}\n </Button>\n </CardContent>\n </Card>\n\n {/* Integrations */}\n <Card>\n <CardHeader>\n <div className=\"flex items-center gap-2\">\n <Globe className=\"h-5 w-5 text-text-secondary\" />\n <CardTitle className=\"text-lg\">\n {tr('admin.settings.integrations.title', 'Integrations')}\n </CardTitle>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'admin.settings.integrations.description',\n 'Connect third-party services and tools.'\n )}\n </p>\n <Button variant=\"outline\" className=\"w-full\">\n {tr('admin.settings.integrations.view', 'View Integrations')}\n </Button>\n </CardContent>\n </Card>\n\n {/* Data Management */}\n <Card>\n <CardHeader>\n <div className=\"flex items-center gap-2\">\n <Database className=\"h-5 w-5 text-text-secondary\" />\n <CardTitle className=\"text-lg\">\n {tr('admin.settings.data.title', 'Data')}\n </CardTitle>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <Button variant=\"outline\" className=\"w-full justify-start\">\n {tr('admin.settings.data.export', 'Export Data')}\n </Button>\n <Button variant=\"outline\" className=\"w-full justify-start\">\n {tr('admin.settings.data.auditLogs', 'Audit Logs')}\n </Button>\n <Separator />\n <Button variant=\"outline\" className=\"w-full justify-start text-status-error-text\">\n {tr('admin.settings.data.dangerZone', 'Danger Zone')}\n </Button>\n </CardContent>\n </Card>\n </div>\n </div>\n </PageLayout>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SettingsPage.js","names":[],"sources":["../../../src/admin/pages/SettingsPage.tsx"],"sourcesContent":["import { Settings, Shield, Key, Bell, Globe, Database } from 'lucide-react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { EmphasisPanel, ShowMore, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n Card,\n CardHeader,\n CardTitle,\n CardContent,\n CardDescription,\n Button,\n Input,\n Separator,\n PermissionGuard,\n} from '../components';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport { tWithFallback } from '../utils/i18n';\n\n/**\n * Settings page\n */\nexport function SettingsPage() {\n const { canManageSettings, isLoading: permissionsLoading } = useAdminPermissions();\n const { t } = useI18n();\n const tr = (key: string, fallback: string, params?: Record<string, string | number>): string =>\n tWithFallback(t, key, fallback, params);\n\n const notificationSettings = [\n {\n key: 'newTenantSignups',\n label: tr('admin.settings.notifications.items.newTenantSignups.label', 'New tenant signups'),\n description: tr(\n 'admin.settings.notifications.items.newTenantSignups.description',\n 'Get notified when new tenants register'\n ),\n },\n {\n key: 'paymentFailures',\n label: tr('admin.settings.notifications.items.paymentFailures.label', 'Payment failures'),\n description: tr(\n 'admin.settings.notifications.items.paymentFailures.description',\n 'Alert when payments fail'\n ),\n },\n {\n key: 'criticalTickets',\n label: tr('admin.settings.notifications.items.criticalTickets.label', 'Critical tickets'),\n description: tr(\n 'admin.settings.notifications.items.criticalTickets.description',\n 'Notify on critical support tickets'\n ),\n },\n {\n key: 'systemAlerts',\n label: tr('admin.settings.notifications.items.systemAlerts.label', 'System alerts'),\n description: tr(\n 'admin.settings.notifications.items.systemAlerts.description',\n 'Infrastructure and health alerts'\n ),\n },\n ];\n\n return (\n <PermissionGuard\n allowed={canManageSettings}\n isLoading={permissionsLoading}\n pageTitle={tr('admin.settings.title', 'Settings')}\n denyMessage={tr(\n 'admin.settings.denyMessage',\n \"You don't have permission to manage platform settings.\"\n )}\n >\n <PageLayout\n title={tr('admin.settings.title', 'Settings')}\n gradientWord={tr('admin.settings.title', 'Settings')}\n description={tr(\n 'admin.settings.description',\n 'Configure platform settings and preferences'\n )}\n >\n <PagePurpose className=\"mb-6\">\n {tr(\n 'admin.settings.purpose',\n 'Platform-wide configuration in one place — the general details every product inherits, security defaults, API access, and which events page you. Change these to set the baseline behaviour for the whole platform, not a single tenant.'\n )}\n </PagePurpose>\n\n <div className=\"grid gap-6 lg:grid-cols-3\">\n {/* Main settings */}\n <div className=\"space-y-6 lg:col-span-2\">\n {/* General — primary settings zone (emphasis) */}\n <EmphasisPanel className=\"p-0\">\n <Card className=\"border-0 bg-transparent shadow-none hover:border-0\">\n <CardHeader>\n <div className=\"flex items-center gap-2\">\n <Settings className=\"h-5 w-5 text-text-secondary\" />\n <CardTitle className=\"text-lg\">\n {tr('admin.settings.general.title', 'General')}\n </CardTitle>\n </div>\n <CardDescription>\n {tr('admin.settings.general.description', 'Basic platform configuration')}\n </CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\">\n {tr('admin.settings.general.platformName', 'Platform Name')}\n </label>\n <Input defaultValue=\"Burdenoff Admin\" />\n </div>\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\">\n {tr('admin.settings.general.supportEmail', 'Support Email')}\n </label>\n <Input defaultValue=\"support@burdenoff.com\" type=\"email\" />\n </div>\n </div>\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\">\n {tr('admin.settings.general.defaultTimezone', 'Default Timezone')}\n </label>\n <select className=\"h-10 w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm\">\n <option value=\"UTC\">UTC</option>\n <option value=\"America/New_York\">\n {tr('admin.settings.general.timezones.eastern', 'Eastern Time (ET)')}\n </option>\n <option value=\"America/Los_Angeles\">\n {tr('admin.settings.general.timezones.pacific', 'Pacific Time (PT)')}\n </option>\n <option value=\"Europe/London\">\n {tr('admin.settings.general.timezones.london', 'London (GMT)')}\n </option>\n <option value=\"Asia/Kolkata\">\n {tr('admin.settings.general.timezones.india', 'India (IST)')}\n </option>\n </select>\n </div>\n <div className=\"flex justify-end\">\n <Button>{tr('admin.settings.general.saveChanges', 'Save Changes')}</Button>\n </div>\n </CardContent>\n </Card>\n </EmphasisPanel>\n\n {/* Security */}\n <Card>\n <CardHeader>\n <div className=\"flex items-center gap-2\">\n <Shield className=\"h-5 w-5 text-text-secondary\" />\n <CardTitle className=\"text-lg\">\n {tr('admin.settings.security.title', 'Security')}\n </CardTitle>\n </div>\n <CardDescription>\n {tr(\n 'admin.settings.security.description',\n 'Security and authentication settings'\n )}\n </CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle p-4\">\n <div>\n <div className=\"font-medium\">\n {tr('admin.settings.security.twoFactor.title', 'Two-Factor Authentication')}\n </div>\n <div className=\"text-sm text-text-secondary\">\n {tr(\n 'admin.settings.security.twoFactor.description',\n 'Require 2FA for all admin accounts'\n )}\n </div>\n </div>\n <Button variant=\"outline\">\n {tr('admin.settings.security.twoFactor.configure', 'Configure')}\n </Button>\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle p-4\">\n <div>\n <div className=\"font-medium\">\n {tr('admin.settings.security.sessionTimeout.title', 'Session Timeout')}\n </div>\n <div className=\"text-sm text-text-secondary\">\n {tr(\n 'admin.settings.security.sessionTimeout.description',\n 'Auto-logout after inactivity'\n )}\n </div>\n </div>\n <select className=\"h-9 rounded-md border border-border-default bg-bg-surface px-3 text-sm\">\n <option value=\"15\">\n {tr('admin.settings.security.sessionTimeout.options.15m', '15 minutes')}\n </option>\n <option value=\"30\">\n {tr('admin.settings.security.sessionTimeout.options.30m', '30 minutes')}\n </option>\n <option value=\"60\">\n {tr('admin.settings.security.sessionTimeout.options.1h', '1 hour')}\n </option>\n <option value=\"480\">\n {tr('admin.settings.security.sessionTimeout.options.8h', '8 hours')}\n </option>\n </select>\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle p-4\">\n <div>\n <div className=\"font-medium\">\n {tr('admin.settings.security.ipAllowlist.title', 'IP Allowlist')}\n </div>\n <div className=\"text-sm text-text-secondary\">\n {tr(\n 'admin.settings.security.ipAllowlist.description',\n 'Restrict access to specific IPs'\n )}\n </div>\n </div>\n <Button variant=\"outline\">\n {tr('admin.settings.security.ipAllowlist.manage', 'Manage')}\n </Button>\n </div>\n </CardContent>\n </Card>\n\n {/* Notifications */}\n <Card>\n <CardHeader>\n <div className=\"flex items-center gap-2\">\n <Bell className=\"h-5 w-5 text-text-secondary\" />\n <CardTitle className=\"text-lg\">\n {tr('admin.settings.notifications.title', 'Notifications')}\n </CardTitle>\n </div>\n <CardDescription>\n {tr(\n 'admin.settings.notifications.description',\n 'Configure notification preferences'\n )}\n </CardDescription>\n </CardHeader>\n <CardContent>\n <ShowMore collapsedHeight={180} label=\"More notification settings\">\n <div className=\"space-y-4\">\n {notificationSettings.map((item) => (\n <div\n key={item.key}\n className=\"flex items-center justify-between rounded-lg border border-border-seam p-4\"\n >\n <div>\n <div className=\"font-medium\">{item.label}</div>\n <div className=\"text-sm text-text-secondary\">{item.description}</div>\n </div>\n <label className=\"relative inline-flex cursor-pointer items-center\">\n <input type=\"checkbox\" defaultChecked className=\"peer sr-only\" />\n <div className=\"peer h-6 w-11 rounded-full bg-bg-sunken after:absolute after:left-[2px] after:top-0.5 after:h-5 after:w-5 after:rounded-full after:bg-bg-surface after:transition-all peer-checked:bg-action-primary-bg peer-checked:after:translate-x-full peer-focus:ring-2 peer-focus:ring-[var(--color-focus-ring)]\" />\n </label>\n </div>\n ))}\n </div>\n </ShowMore>\n </CardContent>\n </Card>\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-6\">\n {/* API Keys */}\n <Card>\n <CardHeader>\n <div className=\"flex items-center gap-2\">\n <Key className=\"h-5 w-5 text-text-secondary\" />\n <CardTitle className=\"text-lg\">\n {tr('admin.settings.apiKeys.title', 'API Keys')}\n </CardTitle>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'admin.settings.apiKeys.description',\n 'Manage API keys for programmatic access to the admin API.'\n )}\n </p>\n <Button variant=\"outline\" className=\"w-full\">\n {tr('admin.settings.apiKeys.manage', 'Manage API Keys')}\n </Button>\n </CardContent>\n </Card>\n\n {/* Integrations */}\n <Card>\n <CardHeader>\n <div className=\"flex items-center gap-2\">\n <Globe className=\"h-5 w-5 text-text-secondary\" />\n <CardTitle className=\"text-lg\">\n {tr('admin.settings.integrations.title', 'Integrations')}\n </CardTitle>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'admin.settings.integrations.description',\n 'Connect third-party services and tools.'\n )}\n </p>\n <Button variant=\"outline\" className=\"w-full\">\n {tr('admin.settings.integrations.view', 'View Integrations')}\n </Button>\n </CardContent>\n </Card>\n\n {/* Data Management */}\n <Card>\n <CardHeader>\n <div className=\"flex items-center gap-2\">\n <Database className=\"h-5 w-5 text-text-secondary\" />\n <CardTitle className=\"text-lg\">\n {tr('admin.settings.data.title', 'Data')}\n </CardTitle>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <Button variant=\"outline\" className=\"w-full justify-start\">\n {tr('admin.settings.data.export', 'Export Data')}\n </Button>\n <Button variant=\"outline\" className=\"w-full justify-start\">\n {tr('admin.settings.data.auditLogs', 'Audit Logs')}\n </Button>\n <Separator />\n <Button variant=\"outline\" className=\"w-full justify-start text-status-error-text\">\n {tr('admin.settings.data.dangerZone', 'Danger Zone')}\n </Button>\n </CardContent>\n </Card>\n </div>\n </div>\n </PageLayout>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SyncFailuresPage.js","names":[],"sources":["../../../src/admin/pages/SyncFailuresPage.tsx"],"sourcesContent":["import { useCallback, useMemo, useState } from 'react';\nimport { RefreshCw, AlertOctagon, Clock, CheckCircle2, RotateCw } from 'lucide-react';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport {\n PageLayout,\n MetricCard,\n MetricsGrid,\n Card,\n CardContent,\n Badge,\n Button,\n DataTable,\n Pagination,\n EmptyState,\n PermissionGuard,\n ConfirmDialog,\n useToast,\n} from '../components';\nimport { formatDateTime, formatRelativeTime } from '../utils/formatters';\nimport { PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { SyncOutboxStatus } from '../../generated/global-types';\nimport {\n useListSyncOutboxEntriesQuery,\n useSyncOutboxStatsQuery,\n useRetrySyncOutboxEntryMutation,\n type ListSyncOutboxEntriesQuery,\n} from '../../generated/global-operations';\n\nconst PAGE_SIZE = 25;\n\n// Single source of truth for the underlying string values. Authored as a\n// const-tuple so TypeScript derives both the union type and a runtime guard.\n// The mapping below is type-checked: if codegen ever drops or renames a\n// SyncOutboxStatus enum member, the build fails at STATUS_TO_GQL, not at\n// runtime.\nconst STATUS_VALUES = ['PENDING', 'RETRYING', 'SUCCESS', 'PERMANENTLY_FAILED'] as const;\ntype StatusValue = (typeof STATUS_VALUES)[number];\ntype StatusFilter = 'all' | StatusValue;\n\ntype OutboxEntry = ListSyncOutboxEntriesQuery['listSyncOutboxEntries']['items'][number];\n\n// Typed bridge between our boundary string union and the codegen-generated\n// enum. Explicit mapping makes the codegen casing dependency a compile-time\n// constraint instead of a runtime cast.\nconst STATUS_TO_GQL: Record<StatusValue, SyncOutboxStatus> = {\n PENDING: SyncOutboxStatus.Pending,\n RETRYING: SyncOutboxStatus.Retrying,\n SUCCESS: SyncOutboxStatus.Success,\n PERMANENTLY_FAILED: SyncOutboxStatus.PermanentlyFailed,\n};\n\n// Reverse map: codegen enum value → boundary string. Falls back to\n// PERMANENTLY_FAILED if codegen ever emits an unknown value — a loud-but-safe\n// default that surfaces the drift without breaking the page.\nfunction asStatusValue(s: SyncOutboxStatus): StatusValue {\n const raw = String(s) as StatusValue;\n return (STATUS_VALUES as readonly string[]).includes(raw) ? raw : 'PERMANENTLY_FAILED';\n}\n\nconst STATUS_VARIANTS: Record<StatusValue, string> = {\n PENDING: 'bg-status-warning-bg text-status-warning-text border-status-warning-border',\n RETRYING: 'bg-status-warning-bg text-status-warning-text border-status-warning-border',\n SUCCESS: 'bg-status-success-bg text-status-success-text border-status-success-border',\n PERMANENTLY_FAILED: 'bg-status-error-bg text-status-error-text border-status-error-border',\n};\n\nconst STATUS_LABELS: Record<StatusValue, string> = {\n PENDING: 'Pending',\n RETRYING: 'Retrying',\n SUCCESS: 'Success',\n PERMANENTLY_FAILED: 'Permanently failed',\n};\n\nfunction StatusBadge({ status }: { status: SyncOutboxStatus }) {\n const key = asStatusValue(status);\n return (\n <Badge variant=\"outline\" className={STATUS_VARIANTS[key]}>\n {STATUS_LABELS[key]}\n </Badge>\n );\n}\n\n/**\n * Sync Failures admin page.\n *\n * Shows the state of the cross-layer sync outbox so a super-admin can spot\n * users/workspaces that didn't get fully provisioned (e.g. signup happened\n * while a downstream service was down) and manually requeue them once the\n * underlying problem is fixed.\n *\n * Manual retry only flips the row to RETRYING; the background reconciler in\n * global-tenant-svc actually performs the sync on its next tick. That keeps\n * the existing single-writer retry semantics intact.\n */\nexport function SyncFailuresPage() {\n const {\n canViewSyncFailures,\n canManageSyncFailures,\n isLoading: permissionsLoading,\n } = useAdminPermissions();\n const toast = useToast();\n\n const [page, setPage] = useState(1);\n const [statusFilter, setStatusFilter] = useState<StatusFilter>('PERMANENTLY_FAILED');\n const [confirmRetryId, setConfirmRetryId] = useState<string | null>(null);\n const [expandedErrorId, setExpandedErrorId] = useState<string | null>(null);\n\n const filter = useMemo(() => {\n if (statusFilter === 'all') return undefined;\n // Explicit, type-checked mapping — replaces the previous `as unknown as`\n // double-cast. If codegen ever renames a SyncOutboxStatus enum member,\n // the build fails at STATUS_TO_GQL above, not silently at runtime.\n return { statuses: [STATUS_TO_GQL[statusFilter]] };\n }, [statusFilter]);\n\n const skipQueries = permissionsLoading || !canViewSyncFailures;\n\n const {\n data: listData,\n loading: listLoading,\n error: listError,\n refetch: refetchList,\n } = useListSyncOutboxEntriesQuery({\n variables: {\n filter,\n limit: PAGE_SIZE,\n offset: (page - 1) * PAGE_SIZE,\n },\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n\n const {\n data: statsData,\n loading: statsLoading,\n refetch: refetchStats,\n } = useSyncOutboxStatsQuery({\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n\n const [retryEntry, { loading: retrying }] = useRetrySyncOutboxEntryMutation({\n onCompleted: (data) => {\n const result = data.retrySyncOutboxEntry;\n if (result.success) {\n toast.success(\n 'Requeued for retry',\n result.message ?? 'The reconciler will pick it up on the next tick.'\n );\n void refetchList();\n void refetchStats();\n } else {\n toast.error('Retry failed', result.message ?? 'Could not requeue this entry.');\n }\n },\n onError: (err) => {\n toast.error('Retry failed', err.message);\n },\n });\n\n const handleRefresh = useCallback(() => {\n void refetchList();\n void refetchStats();\n }, [refetchList, refetchStats]);\n\n const handleConfirmRetry = useCallback(async () => {\n if (!confirmRetryId) return;\n // Apollo's mutate promise rejects on network/GraphQL error; without the\n // finally the dialog would stay open and ConfirmDialog's awaited\n // onConfirm would see an unhandled rejection. The onError toast still\n // surfaces the failure to the user.\n try {\n await retryEntry({ variables: { id: confirmRetryId } });\n } finally {\n setConfirmRetryId(null);\n }\n }, [confirmRetryId, retryEntry]);\n\n const entries = listData?.listSyncOutboxEntries.items ?? [];\n const total = listData?.listSyncOutboxEntries.total ?? 0;\n const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));\n const stats = statsData?.syncOutboxStats;\n\n const columns = useMemo(\n () => [\n {\n key: 'operation',\n header: 'Operation',\n width: '180px',\n render: (entry: OutboxEntry) => (\n <code className=\"text-xs bg-bg-sunken px-1.5 py-0.5 rounded\">{entry.operation}</code>\n ),\n },\n {\n key: 'workspaceId',\n header: 'Workspace',\n render: (entry: OutboxEntry) => (\n <span className=\"font-mono text-xs text-text-secondary\">{entry.workspaceId}</span>\n ),\n },\n {\n key: 'status',\n header: 'Status',\n width: '170px',\n render: (entry: OutboxEntry) => <StatusBadge status={entry.status} />,\n },\n {\n key: 'attempts',\n header: 'Attempts',\n width: '100px',\n align: 'right' as const,\n render: (entry: OutboxEntry) => (\n <span className=\"text-sm\">\n {entry.attempts}\n <span className=\"text-text-secondary\">/{entry.maxAttempts}</span>\n </span>\n ),\n },\n {\n key: 'nextRetryAt',\n header: 'Next retry',\n width: '160px',\n render: (entry: OutboxEntry) =>\n entry.nextRetryAt ? (\n <span className=\"text-sm text-text-secondary\">\n {formatRelativeTime(entry.nextRetryAt)}\n </span>\n ) : (\n <span className=\"text-xs text-text-secondary\">—</span>\n ),\n },\n {\n key: 'createdAt',\n header: 'Created',\n width: '160px',\n render: (entry: OutboxEntry) => (\n <span className=\"text-sm text-text-secondary\">\n {formatRelativeTime(entry.createdAt)}\n </span>\n ),\n },\n {\n key: 'actions',\n header: '',\n width: '120px',\n align: 'right' as const,\n render: (entry: OutboxEntry) => {\n // Retry is only meaningful for entries that have exhausted automatic\n // retries. PENDING/RETRYING entries already get picked up by the\n // background reconciler. SUCCESS entries don't need retrying.\n const isRetryable = asStatusValue(entry.status) === 'PERMANENTLY_FAILED';\n if (!canManageSyncFailures || !isRetryable) {\n return null;\n }\n return (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={(e) => {\n e.stopPropagation();\n setConfirmRetryId(entry.id);\n }}\n disabled={retrying}\n >\n <RotateCw className=\"mr-1.5 h-3.5 w-3.5\" />\n Retry\n </Button>\n );\n },\n },\n ],\n [canManageSyncFailures, retrying]\n );\n\n return (\n <PermissionGuard\n allowed={canViewSyncFailures}\n isLoading={permissionsLoading}\n pageTitle=\"Sync Failures\"\n denyMessage=\"Only super-admins can view cross-layer sync failures.\"\n >\n <PageLayout\n title=\"Sync Failures\"\n gradientWord=\"Failures\"\n description=\"Cross-layer sync operations (global → workspace) that did not complete. Automatic retries run in the background; use the manual retry once the underlying issue is fixed.\"\n actions={\n <Button variant=\"outline\" onClick={handleRefresh} disabled={listLoading || statsLoading}>\n <RefreshCw className={`mr-2 h-4 w-4 ${listLoading ? 'animate-spin' : ''}`} />\n Refresh\n </Button>\n }\n >\n <div className=\"space-y-6\">\n <PagePurpose>\n When the platform can't deliver an event to a downstream system, it lands in this\n sync outbox. Use this page to see what failed, retry an entry once the issue is fixed,\n and clear out events that are stuck — keeping integrations in sync across services.\n </PagePurpose>\n\n {/* Stats */}\n <MetricsGrid>\n <MetricCard\n title=\"Pending\"\n hero\n value={stats?.pending ?? 0}\n icon={<Clock className=\"h-5 w-5\" />}\n subtitle=\"Waiting for first retry\"\n loading={statsLoading}\n />\n <MetricCard\n title=\"Retrying\"\n value={stats?.retrying ?? 0}\n icon={<RotateCw className=\"h-5 w-5\" />}\n subtitle=\"Automatic backoff in progress\"\n loading={statsLoading}\n />\n <MetricCard\n title=\"Permanently failed\"\n value={stats?.permanentlyFailed ?? 0}\n icon={<AlertOctagon className=\"h-5 w-5\" />}\n subtitle=\"Exhausted automatic retries\"\n priority={stats?.permanentlyFailed ? 'critical' : 'normal'}\n loading={statsLoading}\n />\n <MetricCard\n title=\"Successful\"\n value={stats?.successful ?? 0}\n icon={<CheckCircle2 className=\"h-5 w-5\" />}\n subtitle=\"Synced (auto-purged after 7 days)\"\n loading={statsLoading}\n />\n </MetricsGrid>\n\n {/* Filters */}\n <div className=\"flex flex-wrap items-center gap-2\">\n {(\n [\n ['PERMANENTLY_FAILED', 'Permanently failed'],\n ['RETRYING', 'Retrying'],\n ['PENDING', 'Pending'],\n ['SUCCESS', 'Successful'],\n ['all', 'All'],\n ] as Array<[StatusFilter, string]>\n ).map(([value, label]) => (\n <Button\n key={value}\n variant={statusFilter === value ? 'default' : 'outline'}\n size=\"sm\"\n onClick={() => {\n setStatusFilter(value);\n setPage(1);\n // Clear any expanded error detail card — the row it referenced\n // may not be in the new result set.\n setExpandedErrorId(null);\n }}\n >\n {label}\n </Button>\n ))}\n </div>\n\n {/* Error banner */}\n {listError && (\n <Card>\n <CardContent className=\"py-4 text-sm text-status-error-text\">\n Failed to load sync outbox entries: {listError.message}\n </CardContent>\n </Card>\n )}\n\n {/* Table */}\n <DataTable\n columns={columns}\n data={entries}\n keyExtractor={(entry) => entry.id}\n loading={listLoading}\n onRowClick={(entry: OutboxEntry) =>\n setExpandedErrorId((current) => (current === entry.id ? null : entry.id))\n }\n emptyState={\n <EmptyState\n illustration=\"empty-data\"\n icon={<CheckCircle2 className=\"h-8 w-8\" />}\n title=\"No sync failures\"\n description={\n statusFilter === 'PERMANENTLY_FAILED'\n ? 'Nothing has exhausted automatic retries.'\n : 'No outbox entries match the current filter.'\n }\n />\n }\n />\n\n {/* Expanded error detail */}\n {expandedErrorId &&\n entries\n .filter((e: OutboxEntry) => e.id === expandedErrorId)\n .map((entry: OutboxEntry) => (\n <Card key={entry.id}>\n <CardContent className=\"space-y-2 py-4 text-sm\">\n <div className=\"flex items-center justify-between\">\n <span className=\"font-medium\">Last error</span>\n <span className=\"text-xs text-text-secondary\">\n {entry.lastAttemptAt\n ? `Last attempt: ${formatDateTime(entry.lastAttemptAt)}`\n : 'Never attempted'}\n </span>\n </div>\n <pre className=\"whitespace-pre-wrap break-words rounded bg-bg-sunken px-3 py-2 text-xs text-text-secondary\">\n {entry.lastError ?? 'No error recorded'}\n </pre>\n <div className=\"text-xs text-text-secondary\">\n Entry id: <code className=\"font-mono\">{entry.id}</code>\n </div>\n </CardContent>\n </Card>\n ))}\n\n {/* Pagination */}\n {total > PAGE_SIZE && (\n <Pagination\n page={page}\n totalPages={totalPages}\n totalItems={total}\n pageSize={PAGE_SIZE}\n onPageChange={(p) => {\n setPage(p);\n // Same rationale as the filter buttons — the previously\n // expanded row may not be in the new page's result set.\n setExpandedErrorId(null);\n }}\n />\n )}\n </div>\n\n {/* Retry confirm */}\n <ConfirmDialog\n open={!!confirmRetryId}\n onClose={() => setConfirmRetryId(null)}\n onConfirm={handleConfirmRetry}\n title=\"Retry this sync?\"\n description={\n <>\n The entry will be flipped to <strong>RETRYING</strong> and the background reconciler\n will pick it up on its next tick. Attempts will reset to 0 so the entry gets a fresh\n retry budget.\n <br />\n <br />\n Make sure the underlying issue (e.g. a downstream service that was down) is actually\n resolved — otherwise it will fail again.\n </>\n }\n confirmLabel={retrying ? 'Requeueing…' : 'Retry'}\n loading={retrying}\n />\n </PageLayout>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SyncFailuresPage.js","names":[],"sources":["../../../src/admin/pages/SyncFailuresPage.tsx"],"sourcesContent":["import { useCallback, useMemo, useState } from 'react';\nimport { RefreshCw, AlertOctagon, Clock, CheckCircle2, RotateCw } from 'lucide-react';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport {\n PageLayout,\n MetricCard,\n MetricsGrid,\n Card,\n CardContent,\n Badge,\n Button,\n DataTable,\n Pagination,\n EmptyState,\n PermissionGuard,\n ConfirmDialog,\n useToast,\n} from '../components';\nimport { formatDateTime, formatRelativeTime } from '../utils/formatters';\nimport { PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { SyncOutboxStatus } from '../../generated/global-types';\nimport {\n useListSyncOutboxEntriesQuery,\n useSyncOutboxStatsQuery,\n useRetrySyncOutboxEntryMutation,\n type ListSyncOutboxEntriesQuery,\n} from '../../generated/global-operations';\n\nconst PAGE_SIZE = 25;\n\n// Single source of truth for the underlying string values. Authored as a\n// const-tuple so TypeScript derives both the union type and a runtime guard.\n// The mapping below is type-checked: if codegen ever drops or renames a\n// SyncOutboxStatus enum member, the build fails at STATUS_TO_GQL, not at\n// runtime.\nconst STATUS_VALUES = ['PENDING', 'RETRYING', 'SUCCESS', 'PERMANENTLY_FAILED'] as const;\ntype StatusValue = (typeof STATUS_VALUES)[number];\ntype StatusFilter = 'all' | StatusValue;\n\ntype OutboxEntry = ListSyncOutboxEntriesQuery['listSyncOutboxEntries']['items'][number];\n\n// Typed bridge between our boundary string union and the codegen-generated\n// enum. Explicit mapping makes the codegen casing dependency a compile-time\n// constraint instead of a runtime cast.\nconst STATUS_TO_GQL: Record<StatusValue, SyncOutboxStatus> = {\n PENDING: SyncOutboxStatus.Pending,\n RETRYING: SyncOutboxStatus.Retrying,\n SUCCESS: SyncOutboxStatus.Success,\n PERMANENTLY_FAILED: SyncOutboxStatus.PermanentlyFailed,\n};\n\n// Reverse map: codegen enum value → boundary string. Falls back to\n// PERMANENTLY_FAILED if codegen ever emits an unknown value — a loud-but-safe\n// default that surfaces the drift without breaking the page.\nfunction asStatusValue(s: SyncOutboxStatus): StatusValue {\n const raw = String(s) as StatusValue;\n return (STATUS_VALUES as readonly string[]).includes(raw) ? raw : 'PERMANENTLY_FAILED';\n}\n\nconst STATUS_VARIANTS: Record<StatusValue, string> = {\n PENDING: 'bg-status-warning-bg text-status-warning-text border-status-warning-border',\n RETRYING: 'bg-status-warning-bg text-status-warning-text border-status-warning-border',\n SUCCESS: 'bg-status-success-bg text-status-success-text border-status-success-border',\n PERMANENTLY_FAILED: 'bg-status-error-bg text-status-error-text border-status-error-border',\n};\n\nconst STATUS_LABELS: Record<StatusValue, string> = {\n PENDING: 'Pending',\n RETRYING: 'Retrying',\n SUCCESS: 'Success',\n PERMANENTLY_FAILED: 'Permanently failed',\n};\n\nfunction StatusBadge({ status }: { status: SyncOutboxStatus }) {\n const key = asStatusValue(status);\n return (\n <Badge variant=\"outline\" className={STATUS_VARIANTS[key]}>\n {STATUS_LABELS[key]}\n </Badge>\n );\n}\n\n/**\n * Sync Failures admin page.\n *\n * Shows the state of the cross-layer sync outbox so a super-admin can spot\n * users/workspaces that didn't get fully provisioned (e.g. signup happened\n * while a downstream service was down) and manually requeue them once the\n * underlying problem is fixed.\n *\n * Manual retry only flips the row to RETRYING; the background reconciler in\n * global-tenant-svc actually performs the sync on its next tick. That keeps\n * the existing single-writer retry semantics intact.\n */\nexport function SyncFailuresPage() {\n const {\n canViewSyncFailures,\n canManageSyncFailures,\n isLoading: permissionsLoading,\n } = useAdminPermissions();\n const toast = useToast();\n\n const [page, setPage] = useState(1);\n const [statusFilter, setStatusFilter] = useState<StatusFilter>('PERMANENTLY_FAILED');\n const [confirmRetryId, setConfirmRetryId] = useState<string | null>(null);\n const [expandedErrorId, setExpandedErrorId] = useState<string | null>(null);\n\n const filter = useMemo(() => {\n if (statusFilter === 'all') return undefined;\n // Explicit, type-checked mapping — replaces the previous `as unknown as`\n // double-cast. If codegen ever renames a SyncOutboxStatus enum member,\n // the build fails at STATUS_TO_GQL above, not silently at runtime.\n return { statuses: [STATUS_TO_GQL[statusFilter]] };\n }, [statusFilter]);\n\n const skipQueries = permissionsLoading || !canViewSyncFailures;\n\n const {\n data: listData,\n loading: listLoading,\n error: listError,\n refetch: refetchList,\n } = useListSyncOutboxEntriesQuery({\n variables: {\n filter,\n limit: PAGE_SIZE,\n offset: (page - 1) * PAGE_SIZE,\n },\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n\n const {\n data: statsData,\n loading: statsLoading,\n refetch: refetchStats,\n } = useSyncOutboxStatsQuery({\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n\n const [retryEntry, { loading: retrying }] = useRetrySyncOutboxEntryMutation({\n onCompleted: (data) => {\n const result = data.retrySyncOutboxEntry;\n if (result.success) {\n toast.success(\n 'Requeued for retry',\n result.message ?? 'The reconciler will pick it up on the next tick.'\n );\n void refetchList();\n void refetchStats();\n } else {\n toast.error('Retry failed', result.message ?? 'Could not requeue this entry.');\n }\n },\n onError: (err) => {\n toast.error('Retry failed', err.message);\n },\n });\n\n const handleRefresh = useCallback(() => {\n void refetchList();\n void refetchStats();\n }, [refetchList, refetchStats]);\n\n const handleConfirmRetry = useCallback(async () => {\n if (!confirmRetryId) return;\n // Apollo's mutate promise rejects on network/GraphQL error; without the\n // finally the dialog would stay open and ConfirmDialog's awaited\n // onConfirm would see an unhandled rejection. The onError toast still\n // surfaces the failure to the user.\n try {\n await retryEntry({ variables: { id: confirmRetryId } });\n } finally {\n setConfirmRetryId(null);\n }\n }, [confirmRetryId, retryEntry]);\n\n const entries = listData?.listSyncOutboxEntries.items ?? [];\n const total = listData?.listSyncOutboxEntries.total ?? 0;\n const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));\n const stats = statsData?.syncOutboxStats;\n\n const columns = useMemo(\n () => [\n {\n key: 'operation',\n header: 'Operation',\n width: '180px',\n render: (entry: OutboxEntry) => (\n <code className=\"text-xs bg-bg-sunken px-1.5 py-0.5 rounded\">{entry.operation}</code>\n ),\n },\n {\n key: 'workspaceId',\n header: 'Workspace',\n render: (entry: OutboxEntry) => (\n <span className=\"font-mono text-xs text-text-secondary\">{entry.workspaceId}</span>\n ),\n },\n {\n key: 'status',\n header: 'Status',\n width: '170px',\n render: (entry: OutboxEntry) => <StatusBadge status={entry.status} />,\n },\n {\n key: 'attempts',\n header: 'Attempts',\n width: '100px',\n align: 'right' as const,\n render: (entry: OutboxEntry) => (\n <span className=\"text-sm\">\n {entry.attempts}\n <span className=\"text-text-secondary\">/{entry.maxAttempts}</span>\n </span>\n ),\n },\n {\n key: 'nextRetryAt',\n header: 'Next retry',\n width: '160px',\n render: (entry: OutboxEntry) =>\n entry.nextRetryAt ? (\n <span className=\"text-sm text-text-secondary\">\n {formatRelativeTime(entry.nextRetryAt)}\n </span>\n ) : (\n <span className=\"text-xs text-text-secondary\">—</span>\n ),\n },\n {\n key: 'createdAt',\n header: 'Created',\n width: '160px',\n render: (entry: OutboxEntry) => (\n <span className=\"text-sm text-text-secondary\">{formatRelativeTime(entry.createdAt)}</span>\n ),\n },\n {\n key: 'actions',\n header: '',\n width: '120px',\n align: 'right' as const,\n render: (entry: OutboxEntry) => {\n // Retry is only meaningful for entries that have exhausted automatic\n // retries. PENDING/RETRYING entries already get picked up by the\n // background reconciler. SUCCESS entries don't need retrying.\n const isRetryable = asStatusValue(entry.status) === 'PERMANENTLY_FAILED';\n if (!canManageSyncFailures || !isRetryable) {\n return null;\n }\n return (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={(e) => {\n e.stopPropagation();\n setConfirmRetryId(entry.id);\n }}\n disabled={retrying}\n >\n <RotateCw className=\"mr-1.5 h-3.5 w-3.5\" />\n Retry\n </Button>\n );\n },\n },\n ],\n [canManageSyncFailures, retrying]\n );\n\n return (\n <PermissionGuard\n allowed={canViewSyncFailures}\n isLoading={permissionsLoading}\n pageTitle=\"Sync Failures\"\n denyMessage=\"Only super-admins can view cross-layer sync failures.\"\n >\n <PageLayout\n title=\"Sync Failures\"\n gradientWord=\"Failures\"\n description=\"Cross-layer sync operations (global → workspace) that did not complete. Automatic retries run in the background; use the manual retry once the underlying issue is fixed.\"\n actions={\n <Button variant=\"outline\" onClick={handleRefresh} disabled={listLoading || statsLoading}>\n <RefreshCw className={`mr-2 h-4 w-4 ${listLoading ? 'animate-spin' : ''}`} />\n Refresh\n </Button>\n }\n >\n <div className=\"space-y-6\">\n <PagePurpose>\n When the platform can't deliver an event to a downstream system, it lands in this\n sync outbox. Use this page to see what failed, retry an entry once the issue is fixed,\n and clear out events that are stuck — keeping integrations in sync across services.\n </PagePurpose>\n\n {/* Stats */}\n <MetricsGrid>\n <MetricCard\n title=\"Pending\"\n hero\n value={stats?.pending ?? 0}\n icon={<Clock className=\"h-5 w-5\" />}\n subtitle=\"Waiting for first retry\"\n loading={statsLoading}\n />\n <MetricCard\n title=\"Retrying\"\n value={stats?.retrying ?? 0}\n icon={<RotateCw className=\"h-5 w-5\" />}\n subtitle=\"Automatic backoff in progress\"\n loading={statsLoading}\n />\n <MetricCard\n title=\"Permanently failed\"\n value={stats?.permanentlyFailed ?? 0}\n icon={<AlertOctagon className=\"h-5 w-5\" />}\n subtitle=\"Exhausted automatic retries\"\n priority={stats?.permanentlyFailed ? 'critical' : 'normal'}\n loading={statsLoading}\n />\n <MetricCard\n title=\"Successful\"\n value={stats?.successful ?? 0}\n icon={<CheckCircle2 className=\"h-5 w-5\" />}\n subtitle=\"Synced (auto-purged after 7 days)\"\n loading={statsLoading}\n />\n </MetricsGrid>\n\n {/* Filters */}\n <div className=\"flex flex-wrap items-center gap-2\">\n {(\n [\n ['PERMANENTLY_FAILED', 'Permanently failed'],\n ['RETRYING', 'Retrying'],\n ['PENDING', 'Pending'],\n ['SUCCESS', 'Successful'],\n ['all', 'All'],\n ] as Array<[StatusFilter, string]>\n ).map(([value, label]) => (\n <Button\n key={value}\n variant={statusFilter === value ? 'default' : 'outline'}\n size=\"sm\"\n onClick={() => {\n setStatusFilter(value);\n setPage(1);\n // Clear any expanded error detail card — the row it referenced\n // may not be in the new result set.\n setExpandedErrorId(null);\n }}\n >\n {label}\n </Button>\n ))}\n </div>\n\n {/* Error banner */}\n {listError && (\n <Card>\n <CardContent className=\"py-4 text-sm text-status-error-text\">\n Failed to load sync outbox entries: {listError.message}\n </CardContent>\n </Card>\n )}\n\n {/* Table */}\n <DataTable\n columns={columns}\n data={entries}\n keyExtractor={(entry) => entry.id}\n loading={listLoading}\n onRowClick={(entry: OutboxEntry) =>\n setExpandedErrorId((current) => (current === entry.id ? null : entry.id))\n }\n emptyState={\n <EmptyState\n illustration=\"empty-data\"\n icon={<CheckCircle2 className=\"h-8 w-8\" />}\n title=\"No sync failures\"\n description={\n statusFilter === 'PERMANENTLY_FAILED'\n ? 'Nothing has exhausted automatic retries.'\n : 'No outbox entries match the current filter.'\n }\n />\n }\n />\n\n {/* Expanded error detail */}\n {expandedErrorId &&\n entries\n .filter((e: OutboxEntry) => e.id === expandedErrorId)\n .map((entry: OutboxEntry) => (\n <Card key={entry.id}>\n <CardContent className=\"space-y-2 py-4 text-sm\">\n <div className=\"flex items-center justify-between\">\n <span className=\"font-medium\">Last error</span>\n <span className=\"text-xs text-text-secondary\">\n {entry.lastAttemptAt\n ? `Last attempt: ${formatDateTime(entry.lastAttemptAt)}`\n : 'Never attempted'}\n </span>\n </div>\n <pre className=\"whitespace-pre-wrap break-words rounded bg-bg-sunken px-3 py-2 text-xs text-text-secondary\">\n {entry.lastError ?? 'No error recorded'}\n </pre>\n <div className=\"text-xs text-text-secondary\">\n Entry id: <code className=\"font-mono\">{entry.id}</code>\n </div>\n </CardContent>\n </Card>\n ))}\n\n {/* Pagination */}\n {total > PAGE_SIZE && (\n <Pagination\n page={page}\n totalPages={totalPages}\n totalItems={total}\n pageSize={PAGE_SIZE}\n onPageChange={(p) => {\n setPage(p);\n // Same rationale as the filter buttons — the previously\n // expanded row may not be in the new page's result set.\n setExpandedErrorId(null);\n }}\n />\n )}\n </div>\n\n {/* Retry confirm */}\n <ConfirmDialog\n open={!!confirmRetryId}\n onClose={() => setConfirmRetryId(null)}\n onConfirm={handleConfirmRetry}\n title=\"Retry this sync?\"\n description={\n <>\n The entry will be flipped to <strong>RETRYING</strong> and the background reconciler\n will pick it up on its next tick. Attempts will reset to 0 so the entry gets a fresh\n retry budget.\n <br />\n <br />\n Make sure the underlying issue (e.g. a downstream service that was down) is actually\n resolved — otherwise it will fail again.\n </>\n }\n confirmLabel={retrying ? 'Requeueing…' : 'Retry'}\n loading={retrying}\n />\n </PageLayout>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
|