availsync 0.1.0
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/.adal/skills/stripe-best-practices/SKILL.md +42 -0
- package/.adal/skills/stripe-best-practices/references/billing.md +36 -0
- package/.adal/skills/stripe-best-practices/references/connect.md +48 -0
- package/.adal/skills/stripe-best-practices/references/payments.md +79 -0
- package/.adal/skills/stripe-best-practices/references/security.md +109 -0
- package/.adal/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.adal/skills/stripe-projects/SKILL.md +139 -0
- package/.adal/skills/upgrade-stripe/SKILL.md +185 -0
- package/.agents/skills/stripe-best-practices/SKILL.md +42 -0
- package/.agents/skills/stripe-best-practices/references/billing.md +36 -0
- package/.agents/skills/stripe-best-practices/references/connect.md +48 -0
- package/.agents/skills/stripe-best-practices/references/payments.md +79 -0
- package/.agents/skills/stripe-best-practices/references/security.md +109 -0
- package/.agents/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.agents/skills/stripe-projects/SKILL.md +139 -0
- package/.agents/skills/upgrade-stripe/SKILL.md +185 -0
- package/.augment/skills/stripe-best-practices/SKILL.md +42 -0
- package/.augment/skills/stripe-best-practices/references/billing.md +36 -0
- package/.augment/skills/stripe-best-practices/references/connect.md +48 -0
- package/.augment/skills/stripe-best-practices/references/payments.md +79 -0
- package/.augment/skills/stripe-best-practices/references/security.md +109 -0
- package/.augment/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.augment/skills/stripe-projects/SKILL.md +139 -0
- package/.augment/skills/upgrade-stripe/SKILL.md +185 -0
- package/.bob/skills/stripe-best-practices/SKILL.md +42 -0
- package/.bob/skills/stripe-best-practices/references/billing.md +36 -0
- package/.bob/skills/stripe-best-practices/references/connect.md +48 -0
- package/.bob/skills/stripe-best-practices/references/payments.md +79 -0
- package/.bob/skills/stripe-best-practices/references/security.md +109 -0
- package/.bob/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.bob/skills/stripe-projects/SKILL.md +139 -0
- package/.bob/skills/upgrade-stripe/SKILL.md +185 -0
- package/.claude/settings.local.json +7 -0
- package/.claude/skills/stripe-best-practices/SKILL.md +42 -0
- package/.claude/skills/stripe-best-practices/references/billing.md +36 -0
- package/.claude/skills/stripe-best-practices/references/connect.md +48 -0
- package/.claude/skills/stripe-best-practices/references/payments.md +79 -0
- package/.claude/skills/stripe-best-practices/references/security.md +109 -0
- package/.claude/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.claude/skills/stripe-projects/SKILL.md +139 -0
- package/.claude/skills/upgrade-stripe/SKILL.md +185 -0
- package/.codebuddy/skills/stripe-best-practices/SKILL.md +42 -0
- package/.codebuddy/skills/stripe-best-practices/references/billing.md +36 -0
- package/.codebuddy/skills/stripe-best-practices/references/connect.md +48 -0
- package/.codebuddy/skills/stripe-best-practices/references/payments.md +79 -0
- package/.codebuddy/skills/stripe-best-practices/references/security.md +109 -0
- package/.codebuddy/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.codebuddy/skills/stripe-projects/SKILL.md +139 -0
- package/.codebuddy/skills/upgrade-stripe/SKILL.md +185 -0
- package/.commandcode/skills/stripe-best-practices/SKILL.md +42 -0
- package/.commandcode/skills/stripe-best-practices/references/billing.md +36 -0
- package/.commandcode/skills/stripe-best-practices/references/connect.md +48 -0
- package/.commandcode/skills/stripe-best-practices/references/payments.md +79 -0
- package/.commandcode/skills/stripe-best-practices/references/security.md +109 -0
- package/.commandcode/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.commandcode/skills/stripe-projects/SKILL.md +139 -0
- package/.commandcode/skills/upgrade-stripe/SKILL.md +185 -0
- package/.continue/skills/stripe-best-practices/SKILL.md +42 -0
- package/.continue/skills/stripe-best-practices/references/billing.md +36 -0
- package/.continue/skills/stripe-best-practices/references/connect.md +48 -0
- package/.continue/skills/stripe-best-practices/references/payments.md +79 -0
- package/.continue/skills/stripe-best-practices/references/security.md +109 -0
- package/.continue/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.continue/skills/stripe-projects/SKILL.md +139 -0
- package/.continue/skills/upgrade-stripe/SKILL.md +185 -0
- package/.cortex/skills/stripe-best-practices/SKILL.md +42 -0
- package/.cortex/skills/stripe-best-practices/references/billing.md +36 -0
- package/.cortex/skills/stripe-best-practices/references/connect.md +48 -0
- package/.cortex/skills/stripe-best-practices/references/payments.md +79 -0
- package/.cortex/skills/stripe-best-practices/references/security.md +109 -0
- package/.cortex/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.cortex/skills/stripe-projects/SKILL.md +139 -0
- package/.cortex/skills/upgrade-stripe/SKILL.md +185 -0
- package/.crush/skills/stripe-best-practices/SKILL.md +42 -0
- package/.crush/skills/stripe-best-practices/references/billing.md +36 -0
- package/.crush/skills/stripe-best-practices/references/connect.md +48 -0
- package/.crush/skills/stripe-best-practices/references/payments.md +79 -0
- package/.crush/skills/stripe-best-practices/references/security.md +109 -0
- package/.crush/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.crush/skills/stripe-projects/SKILL.md +139 -0
- package/.crush/skills/upgrade-stripe/SKILL.md +185 -0
- package/.env.example +20 -0
- package/.factory/skills/stripe-best-practices/SKILL.md +42 -0
- package/.factory/skills/stripe-best-practices/references/billing.md +36 -0
- package/.factory/skills/stripe-best-practices/references/connect.md +48 -0
- package/.factory/skills/stripe-best-practices/references/payments.md +79 -0
- package/.factory/skills/stripe-best-practices/references/security.md +109 -0
- package/.factory/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.factory/skills/stripe-projects/SKILL.md +139 -0
- package/.factory/skills/upgrade-stripe/SKILL.md +185 -0
- package/.goose/skills/stripe-best-practices/SKILL.md +42 -0
- package/.goose/skills/stripe-best-practices/references/billing.md +36 -0
- package/.goose/skills/stripe-best-practices/references/connect.md +48 -0
- package/.goose/skills/stripe-best-practices/references/payments.md +79 -0
- package/.goose/skills/stripe-best-practices/references/security.md +109 -0
- package/.goose/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.goose/skills/stripe-projects/SKILL.md +139 -0
- package/.goose/skills/upgrade-stripe/SKILL.md +185 -0
- package/.iflow/skills/stripe-best-practices/SKILL.md +42 -0
- package/.iflow/skills/stripe-best-practices/references/billing.md +36 -0
- package/.iflow/skills/stripe-best-practices/references/connect.md +48 -0
- package/.iflow/skills/stripe-best-practices/references/payments.md +79 -0
- package/.iflow/skills/stripe-best-practices/references/security.md +109 -0
- package/.iflow/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.iflow/skills/stripe-projects/SKILL.md +139 -0
- package/.iflow/skills/upgrade-stripe/SKILL.md +185 -0
- package/.junie/skills/stripe-best-practices/SKILL.md +42 -0
- package/.junie/skills/stripe-best-practices/references/billing.md +36 -0
- package/.junie/skills/stripe-best-practices/references/connect.md +48 -0
- package/.junie/skills/stripe-best-practices/references/payments.md +79 -0
- package/.junie/skills/stripe-best-practices/references/security.md +109 -0
- package/.junie/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.junie/skills/stripe-projects/SKILL.md +139 -0
- package/.junie/skills/upgrade-stripe/SKILL.md +185 -0
- package/.kilocode/skills/stripe-best-practices/SKILL.md +42 -0
- package/.kilocode/skills/stripe-best-practices/references/billing.md +36 -0
- package/.kilocode/skills/stripe-best-practices/references/connect.md +48 -0
- package/.kilocode/skills/stripe-best-practices/references/payments.md +79 -0
- package/.kilocode/skills/stripe-best-practices/references/security.md +109 -0
- package/.kilocode/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.kilocode/skills/stripe-projects/SKILL.md +139 -0
- package/.kilocode/skills/upgrade-stripe/SKILL.md +185 -0
- package/.kiro/skills/stripe-best-practices/SKILL.md +42 -0
- package/.kiro/skills/stripe-best-practices/references/billing.md +36 -0
- package/.kiro/skills/stripe-best-practices/references/connect.md +48 -0
- package/.kiro/skills/stripe-best-practices/references/payments.md +79 -0
- package/.kiro/skills/stripe-best-practices/references/security.md +109 -0
- package/.kiro/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.kiro/skills/stripe-projects/SKILL.md +139 -0
- package/.kiro/skills/upgrade-stripe/SKILL.md +185 -0
- package/.kode/skills/stripe-best-practices/SKILL.md +42 -0
- package/.kode/skills/stripe-best-practices/references/billing.md +36 -0
- package/.kode/skills/stripe-best-practices/references/connect.md +48 -0
- package/.kode/skills/stripe-best-practices/references/payments.md +79 -0
- package/.kode/skills/stripe-best-practices/references/security.md +109 -0
- package/.kode/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.kode/skills/stripe-projects/SKILL.md +139 -0
- package/.kode/skills/upgrade-stripe/SKILL.md +185 -0
- package/.mcpjam/skills/stripe-best-practices/SKILL.md +42 -0
- package/.mcpjam/skills/stripe-best-practices/references/billing.md +36 -0
- package/.mcpjam/skills/stripe-best-practices/references/connect.md +48 -0
- package/.mcpjam/skills/stripe-best-practices/references/payments.md +79 -0
- package/.mcpjam/skills/stripe-best-practices/references/security.md +109 -0
- package/.mcpjam/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.mcpjam/skills/stripe-projects/SKILL.md +139 -0
- package/.mcpjam/skills/upgrade-stripe/SKILL.md +185 -0
- package/.mux/skills/stripe-best-practices/SKILL.md +42 -0
- package/.mux/skills/stripe-best-practices/references/billing.md +36 -0
- package/.mux/skills/stripe-best-practices/references/connect.md +48 -0
- package/.mux/skills/stripe-best-practices/references/payments.md +79 -0
- package/.mux/skills/stripe-best-practices/references/security.md +109 -0
- package/.mux/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.mux/skills/stripe-projects/SKILL.md +139 -0
- package/.mux/skills/upgrade-stripe/SKILL.md +185 -0
- package/.neovate/skills/stripe-best-practices/SKILL.md +42 -0
- package/.neovate/skills/stripe-best-practices/references/billing.md +36 -0
- package/.neovate/skills/stripe-best-practices/references/connect.md +48 -0
- package/.neovate/skills/stripe-best-practices/references/payments.md +79 -0
- package/.neovate/skills/stripe-best-practices/references/security.md +109 -0
- package/.neovate/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.neovate/skills/stripe-projects/SKILL.md +139 -0
- package/.neovate/skills/upgrade-stripe/SKILL.md +185 -0
- package/.nixpacksignore +14 -0
- package/.openhands/skills/stripe-best-practices/SKILL.md +42 -0
- package/.openhands/skills/stripe-best-practices/references/billing.md +36 -0
- package/.openhands/skills/stripe-best-practices/references/connect.md +48 -0
- package/.openhands/skills/stripe-best-practices/references/payments.md +79 -0
- package/.openhands/skills/stripe-best-practices/references/security.md +109 -0
- package/.openhands/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.openhands/skills/stripe-projects/SKILL.md +139 -0
- package/.openhands/skills/upgrade-stripe/SKILL.md +185 -0
- package/.pi/skills/stripe-best-practices/SKILL.md +42 -0
- package/.pi/skills/stripe-best-practices/references/billing.md +36 -0
- package/.pi/skills/stripe-best-practices/references/connect.md +48 -0
- package/.pi/skills/stripe-best-practices/references/payments.md +79 -0
- package/.pi/skills/stripe-best-practices/references/security.md +109 -0
- package/.pi/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.pi/skills/stripe-projects/SKILL.md +139 -0
- package/.pi/skills/upgrade-stripe/SKILL.md +185 -0
- package/.pochi/skills/stripe-best-practices/SKILL.md +42 -0
- package/.pochi/skills/stripe-best-practices/references/billing.md +36 -0
- package/.pochi/skills/stripe-best-practices/references/connect.md +48 -0
- package/.pochi/skills/stripe-best-practices/references/payments.md +79 -0
- package/.pochi/skills/stripe-best-practices/references/security.md +109 -0
- package/.pochi/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.pochi/skills/stripe-projects/SKILL.md +139 -0
- package/.pochi/skills/upgrade-stripe/SKILL.md +185 -0
- package/.qoder/skills/stripe-best-practices/SKILL.md +42 -0
- package/.qoder/skills/stripe-best-practices/references/billing.md +36 -0
- package/.qoder/skills/stripe-best-practices/references/connect.md +48 -0
- package/.qoder/skills/stripe-best-practices/references/payments.md +79 -0
- package/.qoder/skills/stripe-best-practices/references/security.md +109 -0
- package/.qoder/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.qoder/skills/stripe-projects/SKILL.md +139 -0
- package/.qoder/skills/upgrade-stripe/SKILL.md +185 -0
- package/.qwen/skills/stripe-best-practices/SKILL.md +42 -0
- package/.qwen/skills/stripe-best-practices/references/billing.md +36 -0
- package/.qwen/skills/stripe-best-practices/references/connect.md +48 -0
- package/.qwen/skills/stripe-best-practices/references/payments.md +79 -0
- package/.qwen/skills/stripe-best-practices/references/security.md +109 -0
- package/.qwen/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.qwen/skills/stripe-projects/SKILL.md +139 -0
- package/.qwen/skills/upgrade-stripe/SKILL.md +185 -0
- package/.roo/skills/stripe-best-practices/SKILL.md +42 -0
- package/.roo/skills/stripe-best-practices/references/billing.md +36 -0
- package/.roo/skills/stripe-best-practices/references/connect.md +48 -0
- package/.roo/skills/stripe-best-practices/references/payments.md +79 -0
- package/.roo/skills/stripe-best-practices/references/security.md +109 -0
- package/.roo/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.roo/skills/stripe-projects/SKILL.md +139 -0
- package/.roo/skills/upgrade-stripe/SKILL.md +185 -0
- package/.trae/skills/stripe-best-practices/SKILL.md +42 -0
- package/.trae/skills/stripe-best-practices/references/billing.md +36 -0
- package/.trae/skills/stripe-best-practices/references/connect.md +48 -0
- package/.trae/skills/stripe-best-practices/references/payments.md +79 -0
- package/.trae/skills/stripe-best-practices/references/security.md +109 -0
- package/.trae/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.trae/skills/stripe-projects/SKILL.md +139 -0
- package/.trae/skills/upgrade-stripe/SKILL.md +185 -0
- package/.vibe/skills/stripe-best-practices/SKILL.md +42 -0
- package/.vibe/skills/stripe-best-practices/references/billing.md +36 -0
- package/.vibe/skills/stripe-best-practices/references/connect.md +48 -0
- package/.vibe/skills/stripe-best-practices/references/payments.md +79 -0
- package/.vibe/skills/stripe-best-practices/references/security.md +109 -0
- package/.vibe/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.vibe/skills/stripe-projects/SKILL.md +139 -0
- package/.vibe/skills/upgrade-stripe/SKILL.md +185 -0
- package/.windsurf/skills/stripe-best-practices/SKILL.md +42 -0
- package/.windsurf/skills/stripe-best-practices/references/billing.md +36 -0
- package/.windsurf/skills/stripe-best-practices/references/connect.md +48 -0
- package/.windsurf/skills/stripe-best-practices/references/payments.md +79 -0
- package/.windsurf/skills/stripe-best-practices/references/security.md +109 -0
- package/.windsurf/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.windsurf/skills/stripe-projects/SKILL.md +139 -0
- package/.windsurf/skills/upgrade-stripe/SKILL.md +185 -0
- package/.zencoder/skills/stripe-best-practices/SKILL.md +42 -0
- package/.zencoder/skills/stripe-best-practices/references/billing.md +36 -0
- package/.zencoder/skills/stripe-best-practices/references/connect.md +48 -0
- package/.zencoder/skills/stripe-best-practices/references/payments.md +79 -0
- package/.zencoder/skills/stripe-best-practices/references/security.md +109 -0
- package/.zencoder/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.zencoder/skills/stripe-projects/SKILL.md +139 -0
- package/.zencoder/skills/upgrade-stripe/SKILL.md +185 -0
- package/AUDIT.md +95 -0
- package/BLOCKERS.md +0 -0
- package/COOLIFY.md +51 -0
- package/MCP_SETUP.md +23 -0
- package/PRODUCTION_CHECKLIST.md +246 -0
- package/README.md +47 -0
- package/ROADMAP.md +91 -0
- package/docs/superpowers/plans/2026-05-11-availsync-frontend-sales-flow.md +2445 -0
- package/frontend/.env.example +2 -0
- package/frontend/app/admin/layout.tsx +13 -0
- package/frontend/app/admin/page.tsx +747 -0
- package/frontend/app/app/activity/page.tsx +257 -0
- package/frontend/app/app/agents/[agentId]/page.tsx +21 -0
- package/frontend/app/app/agents/page.tsx +1155 -0
- package/frontend/app/app/audit/page.tsx +225 -0
- package/frontend/app/app/availability/page.tsx +840 -0
- package/frontend/app/app/holds/page.tsx +262 -0
- package/frontend/app/app/layout.tsx +19 -0
- package/frontend/app/app/onboarding/page.tsx +10 -0
- package/frontend/app/app/onboarding/verify/page.tsx +309 -0
- package/frontend/app/app/page.tsx +508 -0
- package/frontend/app/app/settings/page.tsx +399 -0
- package/frontend/app/app/work/page.tsx +426 -0
- package/frontend/app/changelog/page.tsx +93 -0
- package/frontend/app/checkout/page.tsx +25 -0
- package/frontend/app/docs/api/page.tsx +157 -0
- package/frontend/app/docs/page.tsx +296 -0
- package/frontend/app/docs/pilot/page.tsx +127 -0
- package/frontend/app/docs/quickstart/page.tsx +318 -0
- package/frontend/app/docs/reliability/page.tsx +78 -0
- package/frontend/app/docs/sdk/node/page.tsx +166 -0
- package/frontend/app/globals.css +57 -0
- package/frontend/app/icon.png +0 -0
- package/frontend/app/layout.tsx +87 -0
- package/frontend/app/login/page.tsx +14 -0
- package/frontend/app/page.tsx +47 -0
- package/frontend/app/pricing/page.tsx +66 -0
- package/frontend/app/privacy/page.tsx +52 -0
- package/frontend/app/robots.ts +26 -0
- package/frontend/app/security/page.tsx +74 -0
- package/frontend/app/signup/page.tsx +14 -0
- package/frontend/app/sitemap.ts +14 -0
- package/frontend/app/terms/page.tsx +51 -0
- package/frontend/components/brand/AvailsyncLogo.tsx +56 -0
- package/frontend/components/checkout/CheckoutClient.tsx +100 -0
- package/frontend/components/dashboard/AgentForm.tsx +59 -0
- package/frontend/components/dashboard/AppShell.tsx +291 -0
- package/frontend/components/dashboard/AvailabilityChecker.tsx +117 -0
- package/frontend/components/dashboard/AvailabilityWindowForm.tsx +40 -0
- package/frontend/components/dashboard/HoldForm.tsx +133 -0
- package/frontend/components/dashboard/MetricCard.tsx +10 -0
- package/frontend/components/login/LoginForm.tsx +95 -0
- package/frontend/components/marketing/AgentCoordinationStory.tsx +1530 -0
- package/frontend/components/marketing/Faq.tsx +41 -0
- package/frontend/components/marketing/Hero.tsx +73 -0
- package/frontend/components/marketing/HowItWorks.tsx +28 -0
- package/frontend/components/marketing/ObserveModeTeaser.tsx +41 -0
- package/frontend/components/marketing/PricingTeaser.tsx +23 -0
- package/frontend/components/marketing/ProblemSolution.tsx +36 -0
- package/frontend/components/marketing/SiteFooter.tsx +59 -0
- package/frontend/components/marketing/SiteHeader.tsx +45 -0
- package/frontend/components/marketing/UseCases.tsx +27 -0
- package/frontend/components/onboarding/OnboardingClient.tsx +278 -0
- package/frontend/components/pricing/PricingCards.tsx +65 -0
- package/frontend/components/privacy/CookieConsent.tsx +230 -0
- package/frontend/components/privacy/CookieSettingsButton.tsx +15 -0
- package/frontend/components/seo/JsonLd.tsx +10 -0
- package/frontend/components/signup/SignupForm.tsx +55 -0
- package/frontend/components/ui/Badge.tsx +23 -0
- package/frontend/components/ui/Button.tsx +37 -0
- package/frontend/components/ui/Card.tsx +11 -0
- package/frontend/components/ui/ConfirmDialog.tsx +77 -0
- package/frontend/components/ui/EmptyState.tsx +24 -0
- package/frontend/components/ui/Input.tsx +14 -0
- package/frontend/components/ui/KeyDisplay.tsx +49 -0
- package/frontend/components/ui/Select.tsx +14 -0
- package/frontend/components/ui/Skeleton.tsx +24 -0
- package/frontend/components/ui/Tabs.tsx +19 -0
- package/frontend/components/ui/Textarea.tsx +14 -0
- package/frontend/components/ui/Toast.tsx +78 -0
- package/frontend/components/waitlist/WaitlistDialog.tsx +128 -0
- package/frontend/lib/api.ts +1282 -0
- package/frontend/lib/billing.ts +6 -0
- package/frontend/lib/cookieConsent.ts +113 -0
- package/frontend/lib/format.ts +16 -0
- package/frontend/lib/plans.ts +62 -0
- package/frontend/lib/schemas.ts +108 -0
- package/frontend/lib/seo.ts +376 -0
- package/frontend/lib/setupGuides.ts +630 -0
- package/frontend/lib/storage.ts +30 -0
- package/frontend/next-env.d.ts +6 -0
- package/frontend/next.config.mjs +13 -0
- package/frontend/package-lock.json +14409 -0
- package/frontend/package.json +41 -0
- package/frontend/playwright.config.ts +20 -0
- package/frontend/postcss.config.mjs +8 -0
- package/frontend/public/.gitkeep +0 -0
- package/frontend/public/brand/availsync-logo-board.png +0 -0
- package/frontend/public/brand/availsync-logo-dark.png +0 -0
- package/frontend/public/brand/availsync-mark-dark.png +0 -0
- package/frontend/public/brand/availsync-wordmark-dark.png +0 -0
- package/frontend/public/marketing/hero-agent-coordination.png +0 -0
- package/frontend/tailwind.config.ts +53 -0
- package/frontend/tests/smoke.spec.ts +89 -0
- package/frontend/tsconfig.json +23 -0
- package/jest.config.js +7 -0
- package/nixpacks.toml +11 -0
- package/package.json +53 -0
- package/packages/mcp/LICENSE +21 -0
- package/packages/mcp/README.md +60 -0
- package/packages/mcp/jest.config.cjs +8 -0
- package/packages/mcp/package.json +54 -0
- package/packages/mcp/src/helpers.ts +38 -0
- package/packages/mcp/src/index.test.ts +60 -0
- package/packages/mcp/src/index.ts +387 -0
- package/packages/mcp/tsconfig.json +20 -0
- package/packages/mcp/tsconfig.test.json +12 -0
- package/packages/node/LICENSE +21 -0
- package/packages/node/README.md +120 -0
- package/packages/node/jest.config.cjs +8 -0
- package/packages/node/package.json +46 -0
- package/packages/node/src/index.test.ts +360 -0
- package/packages/node/src/index.ts +402 -0
- package/packages/node/tsconfig.json +20 -0
- package/packages/node/tsconfig.test.json +12 -0
- package/plan.md +923 -0
- package/skills/stripe-best-practices/SKILL.md +42 -0
- package/skills/stripe-best-practices/references/billing.md +36 -0
- package/skills/stripe-best-practices/references/connect.md +48 -0
- package/skills/stripe-best-practices/references/payments.md +79 -0
- package/skills/stripe-best-practices/references/security.md +109 -0
- package/skills/stripe-best-practices/references/treasury.md +16 -0
- package/skills/stripe-projects/SKILL.md +139 -0
- package/skills/upgrade-stripe/SKILL.md +185 -0
- package/skills-lock.json +20 -0
- package/src/core/availability.ts +178 -0
- package/src/core/conflict.ts +209 -0
- package/src/core/work.ts +490 -0
- package/src/db/client.ts +17 -0
- package/src/db/migrations/001_init.sql +88 -0
- package/src/db/migrations/002_stripe.sql +2 -0
- package/src/db/migrations/003_workspace_auth.sql +19 -0
- package/src/db/migrations/004_agent_mcp_status.sql +2 -0
- package/src/db/migrations/005_hold_event_actor.sql +4 -0
- package/src/db/migrations/006_agent_activity.sql +35 -0
- package/src/db/migrations/007_work_coordination.sql +60 -0
- package/src/db/migrations/008_work_claim_leases.sql +20 -0
- package/src/db/migrations/009_billing_subscription_state.sql +23 -0
- package/src/db/migrations/010_agent_api_key_prefix.sql +10 -0
- package/src/db/migrations/011_org_verified_and_work_event_retention.sql +11 -0
- package/src/db/migrations/012_agent_enforcement_mode.sql +12 -0
- package/src/db/migrations/013_support_tickets.sql +21 -0
- package/src/db/migrations/014_paid_plan_waitlist.sql +23 -0
- package/src/db/migrations/015_agent_last_seen.sql +2 -0
- package/src/db/migrations.ts +164 -0
- package/src/db/run-migrations.ts +13 -0
- package/src/index.ts +183 -0
- package/src/lib/activity.ts +137 -0
- package/src/lib/apiKeys.ts +32 -0
- package/src/lib/appInfo.ts +26 -0
- package/src/lib/billingConfig.ts +3 -0
- package/src/lib/env.ts +75 -0
- package/src/lib/logger.ts +8 -0
- package/src/lib/plans.ts +204 -0
- package/src/mcp/server.js +5 -0
- package/src/mcp/server.ts +350 -0
- package/src/middleware/auth.ts +342 -0
- package/src/middleware/requestId.ts +16 -0
- package/src/routes/account.ts +168 -0
- package/src/routes/activity.ts +126 -0
- package/src/routes/admin.ts +514 -0
- package/src/routes/audit.ts +68 -0
- package/src/routes/auth.ts +203 -0
- package/src/routes/availability.ts +325 -0
- package/src/routes/billing.ts +406 -0
- package/src/routes/conflicts.ts +131 -0
- package/src/routes/holds.ts +437 -0
- package/src/routes/mcp.ts +57 -0
- package/src/routes/metrics.ts +39 -0
- package/src/routes/onboarding.ts +273 -0
- package/src/routes/orgs.ts +981 -0
- package/src/routes/preferences.ts +132 -0
- package/src/routes/session.ts +16 -0
- package/src/routes/support.ts +77 -0
- package/src/routes/value.ts +186 -0
- package/src/routes/waitlist.ts +63 -0
- package/src/routes/work.ts +1578 -0
- package/src/server.ts +36 -0
- package/src/types/index.ts +109 -0
- package/tests/integration/activity.route.test.ts +103 -0
- package/tests/integration/admin.route.test.ts +143 -0
- package/tests/integration/agent-keys.route.test.ts +237 -0
- package/tests/integration/availability.route.test.ts +125 -0
- package/tests/integration/billing.route.test.ts +393 -0
- package/tests/integration/conflicts.route.test.ts +131 -0
- package/tests/integration/flows.test.ts +154 -0
- package/tests/integration/helpers.ts +134 -0
- package/tests/integration/holds.route.test.ts +185 -0
- package/tests/integration/metrics.route.test.ts +100 -0
- package/tests/integration/onboarding.verify.route.test.ts +163 -0
- package/tests/integration/preferences.route.test.ts +53 -0
- package/tests/integration/session.route.test.ts +97 -0
- package/tests/integration/system.route.test.ts +92 -0
- package/tests/integration/value.route.test.ts +235 -0
- package/tests/integration/work.route.test.ts +745 -0
- package/tests/setup.ts +4 -0
- package/tests/smoke.sh +62 -0
- package/tests/unit/auth.test.ts +114 -0
- package/tests/unit/availability.test.ts +149 -0
- package/tests/unit/conflict.test.ts +118 -0
- package/tests/unit/env.test.ts +69 -0
- package/tests/unit/migrations.test.ts +135 -0
- package/tests/unit/request-id.test.ts +37 -0
- package/tmp-mobile-agents.png +0 -0
- package/tmp-next-mobile.err.log +10 -0
- package/tmp-next-mobile.log +5 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import { Activity, AlertTriangle, Building2, CreditCard, Mail, MessageSquare, RefreshCw, Search, Server, Shield } from 'lucide-react';
|
|
5
|
+
import {
|
|
6
|
+
getAdminOrgDetail,
|
|
7
|
+
getAdminOverview,
|
|
8
|
+
getAdminSystem,
|
|
9
|
+
listAdminWaitlist,
|
|
10
|
+
listAdminSupportTickets,
|
|
11
|
+
listAdminOrgs,
|
|
12
|
+
logoutWorkspace,
|
|
13
|
+
updateAdminWaitlistLead,
|
|
14
|
+
updateAdminSupportTicket,
|
|
15
|
+
type AdminOrgDetail,
|
|
16
|
+
type AdminOrgRow,
|
|
17
|
+
type AdminOverview,
|
|
18
|
+
type AdminSystem,
|
|
19
|
+
type SupportTicket,
|
|
20
|
+
type WaitlistLead,
|
|
21
|
+
} from '@/lib/api';
|
|
22
|
+
import { Badge } from '@/components/ui/Badge';
|
|
23
|
+
import { Button } from '@/components/ui/Button';
|
|
24
|
+
import { Input } from '@/components/ui/Input';
|
|
25
|
+
import { Select } from '@/components/ui/Select';
|
|
26
|
+
import { AvailsyncLogo } from '@/components/brand/AvailsyncLogo';
|
|
27
|
+
|
|
28
|
+
function formatDate(value: string | null) {
|
|
29
|
+
if (!value) return 'Never';
|
|
30
|
+
return new Intl.DateTimeFormat('da-DK', {
|
|
31
|
+
dateStyle: 'short',
|
|
32
|
+
timeStyle: 'short',
|
|
33
|
+
}).format(new Date(value));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function planVariant(plan: string): 'neutral' | 'accent' | 'success' {
|
|
37
|
+
if (plan === 'free') return 'neutral';
|
|
38
|
+
if (plan === 'individual') return 'accent';
|
|
39
|
+
return 'success';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default function AdminPage() {
|
|
43
|
+
const [overview, setOverview] = useState<AdminOverview | null>(null);
|
|
44
|
+
const [system, setSystem] = useState<AdminSystem | null>(null);
|
|
45
|
+
const [orgs, setOrgs] = useState<AdminOrgRow[]>([]);
|
|
46
|
+
const [tickets, setTickets] = useState<SupportTicket[]>([]);
|
|
47
|
+
const [waitlist, setWaitlist] = useState<WaitlistLead[]>([]);
|
|
48
|
+
const [selectedTicket, setSelectedTicket] = useState<SupportTicket | null>(null);
|
|
49
|
+
const [selectedLead, setSelectedLead] = useState<WaitlistLead | null>(null);
|
|
50
|
+
const [selectedOrg, setSelectedOrg] = useState<AdminOrgDetail | null>(null);
|
|
51
|
+
const [loading, setLoading] = useState(true);
|
|
52
|
+
const [detailLoading, setDetailLoading] = useState(false);
|
|
53
|
+
const [error, setError] = useState('');
|
|
54
|
+
const [accessDenied, setAccessDenied] = useState(false);
|
|
55
|
+
const [filters, setFilters] = useState({
|
|
56
|
+
q: '',
|
|
57
|
+
plan: '' as '' | 'free' | 'individual' | 'team',
|
|
58
|
+
active: '' as '' | 'today' | '7d' | '30d',
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const filteredPlan = filters.plan || undefined;
|
|
62
|
+
const filteredActive = filters.active || undefined;
|
|
63
|
+
|
|
64
|
+
async function load() {
|
|
65
|
+
setLoading(true);
|
|
66
|
+
setError('');
|
|
67
|
+
setAccessDenied(false);
|
|
68
|
+
try {
|
|
69
|
+
const [overviewData, orgRows, systemData] = await Promise.all([
|
|
70
|
+
getAdminOverview(),
|
|
71
|
+
listAdminOrgs({
|
|
72
|
+
q: filters.q || undefined,
|
|
73
|
+
plan: filteredPlan,
|
|
74
|
+
active: filteredActive,
|
|
75
|
+
limit: 100,
|
|
76
|
+
}),
|
|
77
|
+
getAdminSystem(),
|
|
78
|
+
]);
|
|
79
|
+
const [supportRows, waitlistRows] = await Promise.all([
|
|
80
|
+
listAdminSupportTickets({ limit: 25 }).catch(() => ({ tickets: [] })),
|
|
81
|
+
listAdminWaitlist({ limit: 25 }).catch(() => ({ leads: [] })),
|
|
82
|
+
]);
|
|
83
|
+
setOverview(overviewData);
|
|
84
|
+
setOrgs(orgRows.orgs);
|
|
85
|
+
setSystem(systemData);
|
|
86
|
+
setTickets(supportRows.tickets);
|
|
87
|
+
setWaitlist(waitlistRows.leads);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
const message = (err as Error).message;
|
|
90
|
+
if (message.includes('missing_workspace_session')) {
|
|
91
|
+
window.location.href = '/login';
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (message === 'forbidden' || message.includes('Admin access')) {
|
|
95
|
+
setAccessDenied(true);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
setError(message);
|
|
99
|
+
} finally {
|
|
100
|
+
setLoading(false);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
load();
|
|
106
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
107
|
+
}, [filters.plan, filters.active]);
|
|
108
|
+
|
|
109
|
+
async function openOrg(orgId: string) {
|
|
110
|
+
setDetailLoading(true);
|
|
111
|
+
setError('');
|
|
112
|
+
try {
|
|
113
|
+
setSelectedOrg(await getAdminOrgDetail(orgId));
|
|
114
|
+
} catch (err) {
|
|
115
|
+
setError((err as Error).message);
|
|
116
|
+
} finally {
|
|
117
|
+
setDetailLoading(false);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function updateTicket(ticket: SupportTicket, status: SupportTicket['status']) {
|
|
122
|
+
try {
|
|
123
|
+
const result = await updateAdminSupportTicket(ticket.id, status);
|
|
124
|
+
setTickets((rows) => rows.map((row) => (row.id === ticket.id ? result.ticket : row)));
|
|
125
|
+
setSelectedTicket(result.ticket);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
setError((err as Error).message);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function updateLead(lead: WaitlistLead, status: WaitlistLead['status']) {
|
|
132
|
+
try {
|
|
133
|
+
const result = await updateAdminWaitlistLead(lead.id, status);
|
|
134
|
+
setWaitlist((rows) => rows.map((row) => (row.id === lead.id ? result.lead : row)));
|
|
135
|
+
setSelectedLead(result.lead);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
setError((err as Error).message);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function signOut() {
|
|
142
|
+
await logoutWorkspace().catch(() => {});
|
|
143
|
+
window.location.href = '/login';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const paidRate = useMemo(() => {
|
|
147
|
+
if (!overview?.total_workspaces) return '0%';
|
|
148
|
+
return `${Math.round((overview.paid_workspaces / overview.total_workspaces) * 100)}%`;
|
|
149
|
+
}, [overview]);
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<main className="min-h-screen bg-bg text-text-primary">
|
|
153
|
+
<header className="border-b border-border bg-surface">
|
|
154
|
+
<div className="mx-auto flex max-w-7xl items-center justify-between px-6 py-4">
|
|
155
|
+
<div className="flex items-center gap-3">
|
|
156
|
+
<AvailsyncLogo compact glow markClassName="h-8 w-8" />
|
|
157
|
+
<div>
|
|
158
|
+
<h1 className="text-heading font-semibold">Availsync Admin</h1>
|
|
159
|
+
<p className="text-body text-text-tertiary">Read-only operations dashboard</p>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
<div className="flex items-center gap-2">
|
|
163
|
+
<Button variant="secondary" onClick={load} disabled={loading}>
|
|
164
|
+
<RefreshCw className="h-4 w-4" />
|
|
165
|
+
Refresh
|
|
166
|
+
</Button>
|
|
167
|
+
<Button variant="ghost" onClick={signOut}>Sign out</Button>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</header>
|
|
171
|
+
|
|
172
|
+
{accessDenied ? (
|
|
173
|
+
<div className="mx-auto max-w-xl px-6 py-16">
|
|
174
|
+
<div className="rounded border border-border bg-surface p-6">
|
|
175
|
+
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded border border-error/30 bg-error/10">
|
|
176
|
+
<Shield className="h-5 w-5 text-error" />
|
|
177
|
+
</div>
|
|
178
|
+
<h2 className="text-title font-semibold text-text-primary">Admin access required</h2>
|
|
179
|
+
<p className="mt-2 text-body leading-6 text-text-secondary">
|
|
180
|
+
This page is only available to internal Availsync admins listed in `ADMIN_EMAILS`.
|
|
181
|
+
</p>
|
|
182
|
+
<div className="mt-5 flex gap-2">
|
|
183
|
+
<Button href="/app" variant="secondary">Go to app</Button>
|
|
184
|
+
<Button onClick={signOut} variant="ghost">Sign out</Button>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
) : (
|
|
189
|
+
<>
|
|
190
|
+
<div className="mx-auto max-w-7xl px-6 py-6">
|
|
191
|
+
{error && (
|
|
192
|
+
<div className="mb-4 rounded border border-error/30 bg-error/10 p-3 text-body text-error">
|
|
193
|
+
{error}
|
|
194
|
+
</div>
|
|
195
|
+
)}
|
|
196
|
+
|
|
197
|
+
<SystemPanel system={system} />
|
|
198
|
+
|
|
199
|
+
<section className="mb-6 grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-7">
|
|
200
|
+
<Metric icon={Building2} label="Workspaces" value={overview?.total_workspaces ?? 0} sub={`${overview?.active_workspaces_7d ?? 0} active 7d`} />
|
|
201
|
+
<Metric icon={CreditCard} label="Paid" value={overview?.paid_workspaces ?? 0} sub={`${paidRate} · ${overview?.waitlist_new ?? 0} waitlist new`} />
|
|
202
|
+
<Metric icon={Server} label="Agents" value={overview?.total_agents ?? 0} sub={`${overview?.quiet_agents ?? 0} quiet · ${overview?.never_connected_agents ?? 0} never`} />
|
|
203
|
+
<Metric icon={Activity} label="API today" value={overview?.api_calls_today ?? 0} sub={`${overview?.api_calls_month ?? 0} month`} />
|
|
204
|
+
<Metric icon={AlertTriangle} label="Errors" value={overview?.errors_today ?? 0} />
|
|
205
|
+
<Metric icon={Shield} label="Work claims" value={overview?.active_work_claims ?? 0} />
|
|
206
|
+
<Metric icon={AlertTriangle} label="Blocked runs" value={overview?.blocked_agent_runs ?? 0} sub={`${overview?.conflicts_prevented_7d ?? 0} prevented 7d`} />
|
|
207
|
+
</section>
|
|
208
|
+
|
|
209
|
+
<SupportPanel
|
|
210
|
+
tickets={tickets}
|
|
211
|
+
onOpen={setSelectedTicket}
|
|
212
|
+
/>
|
|
213
|
+
|
|
214
|
+
<WaitlistPanel
|
|
215
|
+
leads={waitlist}
|
|
216
|
+
onOpen={setSelectedLead}
|
|
217
|
+
/>
|
|
218
|
+
|
|
219
|
+
<section className="rounded border border-border bg-surface">
|
|
220
|
+
<div className="flex flex-col gap-3 border-b border-border px-4 py-3 lg:flex-row lg:items-center lg:justify-between">
|
|
221
|
+
<div>
|
|
222
|
+
<h2 className="text-heading font-medium">Workspaces</h2>
|
|
223
|
+
<p className="text-body text-text-tertiary">Read-only account, plan, and usage overview.</p>
|
|
224
|
+
</div>
|
|
225
|
+
<div className="flex flex-col gap-2 sm:flex-row">
|
|
226
|
+
<div className="relative">
|
|
227
|
+
<Search className="pointer-events-none absolute left-3 top-3 h-4 w-4 text-text-tertiary" />
|
|
228
|
+
<Input
|
|
229
|
+
value={filters.q}
|
|
230
|
+
onChange={(event) => setFilters({ ...filters, q: event.target.value })}
|
|
231
|
+
onKeyDown={(event) => {
|
|
232
|
+
if (event.key === 'Enter') load();
|
|
233
|
+
}}
|
|
234
|
+
className="pl-9"
|
|
235
|
+
placeholder="Search org or owner"
|
|
236
|
+
/>
|
|
237
|
+
</div>
|
|
238
|
+
<Select
|
|
239
|
+
value={filters.plan}
|
|
240
|
+
onChange={(event) => setFilters({ ...filters, plan: event.target.value as typeof filters.plan })}
|
|
241
|
+
>
|
|
242
|
+
<option value="">All plans</option>
|
|
243
|
+
<option value="free">Free</option>
|
|
244
|
+
<option value="individual">Individual</option>
|
|
245
|
+
<option value="team">Team</option>
|
|
246
|
+
</Select>
|
|
247
|
+
<Select
|
|
248
|
+
value={filters.active}
|
|
249
|
+
onChange={(event) => setFilters({ ...filters, active: event.target.value as typeof filters.active })}
|
|
250
|
+
>
|
|
251
|
+
<option value="">Any activity</option>
|
|
252
|
+
<option value="today">Active today</option>
|
|
253
|
+
<option value="7d">Active 7d</option>
|
|
254
|
+
<option value="30d">Active 30d</option>
|
|
255
|
+
</Select>
|
|
256
|
+
<Button variant="secondary" onClick={load}>Search</Button>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
{loading ? (
|
|
261
|
+
<p className="p-4 text-body text-text-tertiary">Loading admin data...</p>
|
|
262
|
+
) : orgs.length === 0 ? (
|
|
263
|
+
<p className="p-4 text-body text-text-tertiary">No workspaces match the filters.</p>
|
|
264
|
+
) : (
|
|
265
|
+
<div className="overflow-x-auto">
|
|
266
|
+
<table className="w-full">
|
|
267
|
+
<thead>
|
|
268
|
+
<tr className="border-b border-border text-label uppercase text-text-tertiary">
|
|
269
|
+
<th className="px-4 py-2 text-left font-medium">Workspace</th>
|
|
270
|
+
<th className="px-4 py-2 text-left font-medium">Plan</th>
|
|
271
|
+
<th className="px-4 py-2 text-left font-medium">Agents</th>
|
|
272
|
+
<th className="px-4 py-2 text-left font-medium">API 24h</th>
|
|
273
|
+
<th className="px-4 py-2 text-left font-medium">Errors</th>
|
|
274
|
+
<th className="px-4 py-2 text-left font-medium">Work</th>
|
|
275
|
+
<th className="px-4 py-2 text-left font-medium">Last activity</th>
|
|
276
|
+
</tr>
|
|
277
|
+
</thead>
|
|
278
|
+
<tbody>
|
|
279
|
+
{orgs.map((org) => (
|
|
280
|
+
<tr
|
|
281
|
+
key={org.id}
|
|
282
|
+
className="cursor-pointer border-b border-border transition hover:bg-surface-raised"
|
|
283
|
+
onClick={() => openOrg(org.id)}
|
|
284
|
+
>
|
|
285
|
+
<td className="px-4 py-3">
|
|
286
|
+
<div className="text-body font-medium text-text-primary">{org.name}</div>
|
|
287
|
+
<div className="text-label text-text-tertiary">{org.owner_email || 'No owner email'}</div>
|
|
288
|
+
</td>
|
|
289
|
+
<td className="px-4 py-3">
|
|
290
|
+
<Badge variant={planVariant(org.plan)}>{org.plan}</Badge>
|
|
291
|
+
<div className="mt-1 text-label text-text-tertiary">
|
|
292
|
+
{org.subscription_status}{org.cancel_at_period_end ? ' · canceling' : ''}
|
|
293
|
+
</div>
|
|
294
|
+
</td>
|
|
295
|
+
<td className="px-4 py-3 text-body text-text-secondary">
|
|
296
|
+
{org.agent_count}/{org.agent_limit}
|
|
297
|
+
<span className="ml-2 text-text-tertiary">· {org.connected_agent_count} online</span>
|
|
298
|
+
</td>
|
|
299
|
+
<td className="px-4 py-3 text-body text-text-secondary">
|
|
300
|
+
{org.api_calls_24h}
|
|
301
|
+
<span className="ml-2 text-text-tertiary">· {org.api_calls_month} month</span>
|
|
302
|
+
</td>
|
|
303
|
+
<td className="px-4 py-3 text-body text-text-secondary">{org.errors_24h}</td>
|
|
304
|
+
<td className="px-4 py-3 text-body text-text-secondary">
|
|
305
|
+
{org.active_work_claims} active
|
|
306
|
+
<span className="ml-2 text-text-tertiary">· {org.protected_resources} resources</span>
|
|
307
|
+
</td>
|
|
308
|
+
<td className="px-4 py-3 text-body text-text-tertiary">{formatDate(org.last_activity_at)}</td>
|
|
309
|
+
</tr>
|
|
310
|
+
))}
|
|
311
|
+
</tbody>
|
|
312
|
+
</table>
|
|
313
|
+
</div>
|
|
314
|
+
)}
|
|
315
|
+
</section>
|
|
316
|
+
</div>
|
|
317
|
+
|
|
318
|
+
{selectedOrg && (
|
|
319
|
+
<div className="fixed inset-0 z-50 flex justify-end">
|
|
320
|
+
<div className="absolute inset-0 bg-black/50" onClick={() => setSelectedOrg(null)} />
|
|
321
|
+
<aside className="relative h-full w-full max-w-2xl overflow-y-auto border-l border-border bg-surface p-6">
|
|
322
|
+
<div className="mb-5 flex items-start justify-between gap-4">
|
|
323
|
+
<div>
|
|
324
|
+
<h2 className="text-title font-semibold">{selectedOrg.org.name}</h2>
|
|
325
|
+
<p className="mt-1 font-mono text-[12px] text-text-tertiary">{selectedOrg.org.id}</p>
|
|
326
|
+
</div>
|
|
327
|
+
<Badge variant={planVariant(selectedOrg.org.plan)}>{selectedOrg.org.plan}</Badge>
|
|
328
|
+
</div>
|
|
329
|
+
|
|
330
|
+
{detailLoading ? (
|
|
331
|
+
<p className="text-body text-text-tertiary">Loading detail...</p>
|
|
332
|
+
) : (
|
|
333
|
+
<OrgDetail detail={selectedOrg} />
|
|
334
|
+
)}
|
|
335
|
+
</aside>
|
|
336
|
+
</div>
|
|
337
|
+
)}
|
|
338
|
+
|
|
339
|
+
{selectedTicket && (
|
|
340
|
+
<div className="fixed inset-0 z-50 flex justify-end">
|
|
341
|
+
<div className="absolute inset-0 bg-black/50" onClick={() => setSelectedTicket(null)} />
|
|
342
|
+
<aside className="relative h-full w-full max-w-xl overflow-y-auto border-l border-border bg-surface p-6">
|
|
343
|
+
<div className="mb-5 flex items-start justify-between gap-4">
|
|
344
|
+
<div>
|
|
345
|
+
<h2 className="text-title font-semibold">{selectedTicket.subject}</h2>
|
|
346
|
+
<p className="mt-1 text-body text-text-tertiary">{selectedTicket.org_name || selectedTicket.org_id}</p>
|
|
347
|
+
</div>
|
|
348
|
+
<Badge variant={selectedTicket.status === 'open' ? 'error' : selectedTicket.status === 'in_review' ? 'warning' : 'success'}>
|
|
349
|
+
{selectedTicket.status}
|
|
350
|
+
</Badge>
|
|
351
|
+
</div>
|
|
352
|
+
<div className="grid grid-cols-2 gap-2">
|
|
353
|
+
<Info label="Email" value={selectedTicket.email} />
|
|
354
|
+
<Info label="Plan" value={selectedTicket.plan} />
|
|
355
|
+
<Info label="Category" value={selectedTicket.category.replaceAll('_', ' ')} />
|
|
356
|
+
<Info label="Created" value={formatDate(selectedTicket.created_at)} />
|
|
357
|
+
</div>
|
|
358
|
+
<section className="mt-5 rounded border border-border bg-bg p-4">
|
|
359
|
+
<h3 className="text-body font-medium text-text-primary">Message</h3>
|
|
360
|
+
<p className="mt-2 whitespace-pre-wrap text-body leading-6 text-text-secondary">{selectedTicket.message}</p>
|
|
361
|
+
</section>
|
|
362
|
+
<section className="mt-5 rounded border border-border bg-bg p-4">
|
|
363
|
+
<h3 className="text-body font-medium text-text-primary">Safe context</h3>
|
|
364
|
+
<pre className="mt-2 overflow-auto rounded border border-border bg-surface p-3 text-[11px] text-text-secondary">
|
|
365
|
+
{JSON.stringify(selectedTicket.context, null, 2)}
|
|
366
|
+
</pre>
|
|
367
|
+
</section>
|
|
368
|
+
<div className="mt-5 flex flex-wrap gap-2">
|
|
369
|
+
<Button variant="secondary" onClick={() => navigator.clipboard.writeText(selectedTicket.email)}>
|
|
370
|
+
Copy email
|
|
371
|
+
</Button>
|
|
372
|
+
<Button variant="secondary" onClick={() => updateTicket(selectedTicket, 'in_review')}>
|
|
373
|
+
Mark in review
|
|
374
|
+
</Button>
|
|
375
|
+
<Button variant="secondary" onClick={() => updateTicket(selectedTicket, 'closed')}>
|
|
376
|
+
Mark closed
|
|
377
|
+
</Button>
|
|
378
|
+
</div>
|
|
379
|
+
</aside>
|
|
380
|
+
</div>
|
|
381
|
+
)}
|
|
382
|
+
|
|
383
|
+
{selectedLead && (
|
|
384
|
+
<div className="fixed inset-0 z-50 flex justify-end">
|
|
385
|
+
<div className="absolute inset-0 bg-black/50" onClick={() => setSelectedLead(null)} />
|
|
386
|
+
<aside className="relative h-full w-full max-w-xl overflow-y-auto border-l border-border bg-surface p-6">
|
|
387
|
+
<div className="mb-5 flex items-start justify-between gap-4">
|
|
388
|
+
<div>
|
|
389
|
+
<h2 className="text-title font-semibold">{selectedLead.email}</h2>
|
|
390
|
+
<p className="mt-1 text-body text-text-tertiary">{selectedLead.org_name || selectedLead.org_id || 'Public lead'}</p>
|
|
391
|
+
</div>
|
|
392
|
+
<Badge variant={selectedLead.status === 'new' ? 'error' : selectedLead.status === 'contacted' ? 'warning' : 'success'}>
|
|
393
|
+
{selectedLead.status}
|
|
394
|
+
</Badge>
|
|
395
|
+
</div>
|
|
396
|
+
<div className="grid grid-cols-2 gap-2">
|
|
397
|
+
<Info label="Plan" value={selectedLead.plan} />
|
|
398
|
+
<Info label="Source" value={selectedLead.source} />
|
|
399
|
+
<Info label="Created" value={formatDate(selectedLead.created_at)} />
|
|
400
|
+
<Info label="Workspace" value={selectedLead.org_name || selectedLead.org_id || '-'} />
|
|
401
|
+
</div>
|
|
402
|
+
<section className="mt-5 rounded border border-border bg-bg p-4">
|
|
403
|
+
<h3 className="text-body font-medium text-text-primary">Message</h3>
|
|
404
|
+
<p className="mt-2 whitespace-pre-wrap text-body leading-6 text-text-secondary">
|
|
405
|
+
{selectedLead.message || 'No message.'}
|
|
406
|
+
</p>
|
|
407
|
+
</section>
|
|
408
|
+
<section className="mt-5 rounded border border-border bg-bg p-4">
|
|
409
|
+
<h3 className="text-body font-medium text-text-primary">Safe context</h3>
|
|
410
|
+
<pre className="mt-2 overflow-auto rounded border border-border bg-surface p-3 text-[11px] text-text-secondary">
|
|
411
|
+
{JSON.stringify(selectedLead.context, null, 2)}
|
|
412
|
+
</pre>
|
|
413
|
+
</section>
|
|
414
|
+
<div className="mt-5 flex flex-wrap gap-2">
|
|
415
|
+
<Button variant="secondary" onClick={() => navigator.clipboard.writeText(selectedLead.email)}>
|
|
416
|
+
Copy email
|
|
417
|
+
</Button>
|
|
418
|
+
<Button variant="secondary" onClick={() => updateLead(selectedLead, 'contacted')}>
|
|
419
|
+
Mark contacted
|
|
420
|
+
</Button>
|
|
421
|
+
<Button variant="secondary" onClick={() => updateLead(selectedLead, 'closed')}>
|
|
422
|
+
Mark closed
|
|
423
|
+
</Button>
|
|
424
|
+
</div>
|
|
425
|
+
</aside>
|
|
426
|
+
</div>
|
|
427
|
+
)}
|
|
428
|
+
</>
|
|
429
|
+
)}
|
|
430
|
+
</main>
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function WaitlistPanel({
|
|
435
|
+
leads,
|
|
436
|
+
onOpen,
|
|
437
|
+
}: {
|
|
438
|
+
leads: WaitlistLead[];
|
|
439
|
+
onOpen: (lead: WaitlistLead) => void;
|
|
440
|
+
}) {
|
|
441
|
+
const newCount = leads.filter((lead) => lead.status === 'new').length;
|
|
442
|
+
|
|
443
|
+
return (
|
|
444
|
+
<section className="mb-6 rounded border border-border bg-surface">
|
|
445
|
+
<div className="flex items-center justify-between border-b border-border px-4 py-3">
|
|
446
|
+
<div>
|
|
447
|
+
<div className="flex items-center gap-2">
|
|
448
|
+
<Mail className="h-4 w-4 text-accent" />
|
|
449
|
+
<h2 className="text-heading font-medium">Paid plan waitlist</h2>
|
|
450
|
+
</div>
|
|
451
|
+
<p className="text-body text-text-tertiary">People who asked for Individual or Team access.</p>
|
|
452
|
+
</div>
|
|
453
|
+
<Badge variant={newCount > 0 ? 'error' : 'neutral'}>{newCount} new</Badge>
|
|
454
|
+
</div>
|
|
455
|
+
{leads.length === 0 ? (
|
|
456
|
+
<p className="p-4 text-body text-text-tertiary">No waitlist leads yet.</p>
|
|
457
|
+
) : (
|
|
458
|
+
<div className="overflow-x-auto">
|
|
459
|
+
<table className="w-full">
|
|
460
|
+
<thead>
|
|
461
|
+
<tr className="border-b border-border text-label uppercase text-text-tertiary">
|
|
462
|
+
<th className="px-4 py-2 text-left font-medium">Time</th>
|
|
463
|
+
<th className="px-4 py-2 text-left font-medium">Email</th>
|
|
464
|
+
<th className="px-4 py-2 text-left font-medium">Plan</th>
|
|
465
|
+
<th className="px-4 py-2 text-left font-medium">Workspace</th>
|
|
466
|
+
<th className="px-4 py-2 text-left font-medium">Source</th>
|
|
467
|
+
<th className="px-4 py-2 text-left font-medium">Status</th>
|
|
468
|
+
</tr>
|
|
469
|
+
</thead>
|
|
470
|
+
<tbody>
|
|
471
|
+
{leads.slice(0, 8).map((lead) => (
|
|
472
|
+
<tr
|
|
473
|
+
key={lead.id}
|
|
474
|
+
className="cursor-pointer border-b border-border transition hover:bg-surface-raised"
|
|
475
|
+
onClick={() => onOpen(lead)}
|
|
476
|
+
>
|
|
477
|
+
<td className="px-4 py-3 text-body text-text-tertiary">{formatDate(lead.created_at)}</td>
|
|
478
|
+
<td className="px-4 py-3 text-body text-text-primary">{lead.email}</td>
|
|
479
|
+
<td className="px-4 py-3">
|
|
480
|
+
<Badge variant={planVariant(lead.plan)}>{lead.plan}</Badge>
|
|
481
|
+
</td>
|
|
482
|
+
<td className="px-4 py-3 text-body text-text-secondary">{lead.org_name || lead.org_id || 'Public'}</td>
|
|
483
|
+
<td className="px-4 py-3 text-body text-text-secondary">{lead.source}</td>
|
|
484
|
+
<td className="px-4 py-3">
|
|
485
|
+
<Badge variant={lead.status === 'new' ? 'error' : lead.status === 'contacted' ? 'warning' : 'success'}>
|
|
486
|
+
{lead.status}
|
|
487
|
+
</Badge>
|
|
488
|
+
</td>
|
|
489
|
+
</tr>
|
|
490
|
+
))}
|
|
491
|
+
</tbody>
|
|
492
|
+
</table>
|
|
493
|
+
</div>
|
|
494
|
+
)}
|
|
495
|
+
</section>
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function SupportPanel({
|
|
500
|
+
tickets,
|
|
501
|
+
onOpen,
|
|
502
|
+
}: {
|
|
503
|
+
tickets: SupportTicket[];
|
|
504
|
+
onOpen: (ticket: SupportTicket) => void;
|
|
505
|
+
}) {
|
|
506
|
+
const openCount = tickets.filter((ticket) => ticket.status === 'open').length;
|
|
507
|
+
|
|
508
|
+
return (
|
|
509
|
+
<section className="mb-6 rounded border border-border bg-surface">
|
|
510
|
+
<div className="flex items-center justify-between border-b border-border px-4 py-3">
|
|
511
|
+
<div>
|
|
512
|
+
<div className="flex items-center gap-2">
|
|
513
|
+
<MessageSquare className="h-4 w-4 text-accent" />
|
|
514
|
+
<h2 className="text-heading font-medium">Support tickets</h2>
|
|
515
|
+
</div>
|
|
516
|
+
<p className="text-body text-text-tertiary">Paid customer requests. Reply manually by email.</p>
|
|
517
|
+
</div>
|
|
518
|
+
<Badge variant={openCount > 0 ? 'error' : 'neutral'}>{openCount} open</Badge>
|
|
519
|
+
</div>
|
|
520
|
+
{tickets.length === 0 ? (
|
|
521
|
+
<p className="p-4 text-body text-text-tertiary">No support tickets yet.</p>
|
|
522
|
+
) : (
|
|
523
|
+
<div className="overflow-x-auto">
|
|
524
|
+
<table className="w-full">
|
|
525
|
+
<thead>
|
|
526
|
+
<tr className="border-b border-border text-label uppercase text-text-tertiary">
|
|
527
|
+
<th className="px-4 py-2 text-left font-medium">Time</th>
|
|
528
|
+
<th className="px-4 py-2 text-left font-medium">Workspace</th>
|
|
529
|
+
<th className="px-4 py-2 text-left font-medium">Email</th>
|
|
530
|
+
<th className="px-4 py-2 text-left font-medium">Subject</th>
|
|
531
|
+
<th className="px-4 py-2 text-left font-medium">Status</th>
|
|
532
|
+
</tr>
|
|
533
|
+
</thead>
|
|
534
|
+
<tbody>
|
|
535
|
+
{tickets.slice(0, 8).map((ticket) => (
|
|
536
|
+
<tr
|
|
537
|
+
key={ticket.id}
|
|
538
|
+
className="cursor-pointer border-b border-border transition hover:bg-surface-raised"
|
|
539
|
+
onClick={() => onOpen(ticket)}
|
|
540
|
+
>
|
|
541
|
+
<td className="px-4 py-3 text-body text-text-tertiary">{formatDate(ticket.created_at)}</td>
|
|
542
|
+
<td className="px-4 py-3 text-body text-text-primary">{ticket.org_name || ticket.org_id}</td>
|
|
543
|
+
<td className="px-4 py-3 text-body text-text-secondary">{ticket.email}</td>
|
|
544
|
+
<td className="px-4 py-3">
|
|
545
|
+
<div className="text-body text-text-primary">{ticket.subject}</div>
|
|
546
|
+
<div className="text-label text-text-tertiary">{ticket.category.replaceAll('_', ' ')} · {ticket.plan}</div>
|
|
547
|
+
</td>
|
|
548
|
+
<td className="px-4 py-3">
|
|
549
|
+
<Badge variant={ticket.status === 'open' ? 'error' : ticket.status === 'in_review' ? 'warning' : 'success'}>
|
|
550
|
+
{ticket.status}
|
|
551
|
+
</Badge>
|
|
552
|
+
</td>
|
|
553
|
+
</tr>
|
|
554
|
+
))}
|
|
555
|
+
</tbody>
|
|
556
|
+
</table>
|
|
557
|
+
</div>
|
|
558
|
+
)}
|
|
559
|
+
</section>
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function statusVariant(status: string): 'success' | 'warning' | 'error' | 'neutral' {
|
|
564
|
+
if (status === 'ok' || status === 'applied') return 'success';
|
|
565
|
+
if (status === 'degraded' || status === 'running') return 'warning';
|
|
566
|
+
if (status === 'error' || status === 'failed') return 'error';
|
|
567
|
+
return 'neutral';
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function SystemPanel({ system }: { system: AdminSystem | null }) {
|
|
571
|
+
const missing = system?.env.missing ?? [];
|
|
572
|
+
const readiness = [
|
|
573
|
+
{ label: 'Database', ok: Boolean(system?.database.connected) },
|
|
574
|
+
{ label: 'Migrations', ok: Boolean(system && system.migrations.failed === 0 && system.migrations.pending === 0) },
|
|
575
|
+
{ label: 'Stripe key', ok: Boolean(system?.stripe.configured) },
|
|
576
|
+
{ label: 'Stripe webhook', ok: Boolean(system?.stripe.webhook_configured) },
|
|
577
|
+
{ label: 'Individual price', ok: Boolean(system?.stripe.individual_price_configured) },
|
|
578
|
+
{ label: 'Team price', ok: Boolean(system?.stripe.team_price_configured) },
|
|
579
|
+
{ label: 'Required env', ok: Boolean(system?.env.ok) },
|
|
580
|
+
];
|
|
581
|
+
|
|
582
|
+
return (
|
|
583
|
+
<section className="mb-6 rounded border border-border bg-surface">
|
|
584
|
+
<div className="flex flex-col gap-3 border-b border-border px-4 py-3 md:flex-row md:items-start md:justify-between">
|
|
585
|
+
<div>
|
|
586
|
+
<div className="flex items-center gap-2">
|
|
587
|
+
<h2 className="text-heading font-medium">System</h2>
|
|
588
|
+
<Badge variant={statusVariant(system?.status || 'neutral')}>{system?.status || 'loading'}</Badge>
|
|
589
|
+
</div>
|
|
590
|
+
<p className="mt-1 text-body text-text-tertiary">
|
|
591
|
+
Runtime, deployment and environment readiness. Values are redacted.
|
|
592
|
+
</p>
|
|
593
|
+
</div>
|
|
594
|
+
<div className="text-right text-label text-text-tertiary">
|
|
595
|
+
<div>v{system?.app.version || '-'}</div>
|
|
596
|
+
<div>{system?.app.git_commit?.slice(0, 12) || 'unknown'}</div>
|
|
597
|
+
<div>{system?.app.node_version || '-'}</div>
|
|
598
|
+
</div>
|
|
599
|
+
</div>
|
|
600
|
+
<div className="grid gap-3 p-4 lg:grid-cols-[1.2fr_1fr]">
|
|
601
|
+
<div className="grid gap-2 sm:grid-cols-2 xl:grid-cols-4">
|
|
602
|
+
<Info label="Migrations" value={system ? `${system.migrations.applied}/${system.migrations.total} applied` : '-'} />
|
|
603
|
+
<Info label="Pending" value={String(system?.migrations.pending ?? 0)} />
|
|
604
|
+
<Info label="Failed" value={String(system?.migrations.failed ?? 0)} />
|
|
605
|
+
<Info label="Latest" value={system?.migrations.latest?.filename || '-'} />
|
|
606
|
+
</div>
|
|
607
|
+
<div className="rounded border border-border bg-bg p-3">
|
|
608
|
+
<div className="mb-2 text-label uppercase text-text-tertiary">Readiness</div>
|
|
609
|
+
<div className="flex flex-wrap gap-2">
|
|
610
|
+
{readiness.map((item) => (
|
|
611
|
+
<Badge key={item.label} variant={item.ok ? 'success' : 'warning'}>
|
|
612
|
+
{item.label}
|
|
613
|
+
</Badge>
|
|
614
|
+
))}
|
|
615
|
+
</div>
|
|
616
|
+
{missing.length > 0 && (
|
|
617
|
+
<div className="mt-3 text-body text-warning">
|
|
618
|
+
Missing env: {missing.join(', ')}
|
|
619
|
+
</div>
|
|
620
|
+
)}
|
|
621
|
+
</div>
|
|
622
|
+
</div>
|
|
623
|
+
</section>
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function Metric({
|
|
628
|
+
icon: Icon,
|
|
629
|
+
label,
|
|
630
|
+
value,
|
|
631
|
+
sub,
|
|
632
|
+
}: {
|
|
633
|
+
icon: typeof Building2;
|
|
634
|
+
label: string;
|
|
635
|
+
value: number | string;
|
|
636
|
+
sub?: string;
|
|
637
|
+
}) {
|
|
638
|
+
return (
|
|
639
|
+
<div className="rounded border border-border bg-surface p-4">
|
|
640
|
+
<div className="mb-2 flex items-center gap-2 text-label uppercase text-text-tertiary">
|
|
641
|
+
<Icon className="h-3.5 w-3.5" />
|
|
642
|
+
{label}
|
|
643
|
+
</div>
|
|
644
|
+
<div className="text-title font-semibold text-text-primary">{value}</div>
|
|
645
|
+
{sub && <div className="mt-0.5 text-body text-text-tertiary">{sub}</div>}
|
|
646
|
+
</div>
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function OrgDetail({ detail }: { detail: AdminOrgDetail }) {
|
|
651
|
+
return (
|
|
652
|
+
<div className="space-y-5">
|
|
653
|
+
<section className="grid grid-cols-2 gap-2">
|
|
654
|
+
<Info label="Owner" value={detail.users.find((user) => user.role === 'owner')?.email || detail.users[0]?.email || '-'} />
|
|
655
|
+
<Info label="Created" value={formatDate(detail.org.created_at)} />
|
|
656
|
+
<Info label="Stripe customer" value={detail.org.has_stripe_customer ? 'Yes' : 'No'} />
|
|
657
|
+
<Info label="Stripe subscription" value={detail.org.has_stripe_subscription ? 'Yes' : 'No'} />
|
|
658
|
+
<Info label="Subscription status" value={`${detail.org.subscription_status}${detail.org.cancel_at_period_end ? ' · canceling' : ''}`} />
|
|
659
|
+
<Info label="Renewal" value={formatDate(detail.org.current_period_end)} />
|
|
660
|
+
<Info label="API calls 24h" value={String(detail.usage.api_calls_24h)} />
|
|
661
|
+
<Info label="API calls month" value={String(detail.usage.api_calls_month)} />
|
|
662
|
+
<Info label="Errors 24h" value={String(detail.usage.errors_24h)} />
|
|
663
|
+
<Info label="Connected agents" value={`${detail.usage.connected_agents}/${detail.usage.agents}`} />
|
|
664
|
+
<Info label="Quiet agents" value={String(detail.usage.quiet_agents)} />
|
|
665
|
+
<Info label="Never connected" value={String(detail.usage.never_connected_agents)} />
|
|
666
|
+
<Info label="Prevented 7d" value={String(detail.usage.conflicts_prevented_7d)} />
|
|
667
|
+
<Info label="Resources" value={String(detail.usage.protected_resources)} />
|
|
668
|
+
<Info label="Blocked runs" value={String(detail.usage.blocked_agent_runs)} />
|
|
669
|
+
</section>
|
|
670
|
+
|
|
671
|
+
<Panel title="Users">
|
|
672
|
+
{detail.users.map((user) => (
|
|
673
|
+
<Row key={user.id} primary={user.email} secondary={`${user.role} · ${formatDate(user.created_at)}`} />
|
|
674
|
+
))}
|
|
675
|
+
</Panel>
|
|
676
|
+
|
|
677
|
+
<Panel title="Agents">
|
|
678
|
+
{detail.agents.length === 0 ? (
|
|
679
|
+
<p className="text-body text-text-tertiary">No agents.</p>
|
|
680
|
+
) : (
|
|
681
|
+
detail.agents.map((agent) => (
|
|
682
|
+
<Row
|
|
683
|
+
key={agent.id}
|
|
684
|
+
primary={agent.name}
|
|
685
|
+
secondary={`${agent.agent_type} · priority ${agent.priority} · last ${formatDate(agent.last_activity_at)}`}
|
|
686
|
+
/>
|
|
687
|
+
))
|
|
688
|
+
)}
|
|
689
|
+
</Panel>
|
|
690
|
+
|
|
691
|
+
<Panel title="Recent activity">
|
|
692
|
+
{detail.recent_activity.length === 0 ? (
|
|
693
|
+
<p className="text-body text-text-tertiary">No activity.</p>
|
|
694
|
+
) : (
|
|
695
|
+
detail.recent_activity.map((event) => (
|
|
696
|
+
<Row
|
|
697
|
+
key={event.id}
|
|
698
|
+
primary={`${event.activity_type} · ${event.status}`}
|
|
699
|
+
secondary={`${event.agent_name || 'Unknown'} · ${event.error_code || event.endpoint || '-'} · ${formatDate(event.created_at)}`}
|
|
700
|
+
/>
|
|
701
|
+
))
|
|
702
|
+
)}
|
|
703
|
+
</Panel>
|
|
704
|
+
|
|
705
|
+
<Panel title="Recent work events">
|
|
706
|
+
{detail.recent_work_events.length === 0 ? (
|
|
707
|
+
<p className="text-body text-text-tertiary">No work events.</p>
|
|
708
|
+
) : (
|
|
709
|
+
detail.recent_work_events.map((event) => (
|
|
710
|
+
<Row
|
|
711
|
+
key={event.id}
|
|
712
|
+
primary={`${event.event_type} · ${event.resource_key || '-'}`}
|
|
713
|
+
secondary={`${event.agent_name || 'Unknown'} · ${event.actor_label || event.actor_type || '-'} · ${formatDate(event.created_at)}`}
|
|
714
|
+
/>
|
|
715
|
+
))
|
|
716
|
+
)}
|
|
717
|
+
</Panel>
|
|
718
|
+
</div>
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function Info({ label, value }: { label: string; value: string }) {
|
|
723
|
+
return (
|
|
724
|
+
<div className="rounded border border-border bg-bg p-3">
|
|
725
|
+
<div className="text-label uppercase text-text-tertiary">{label}</div>
|
|
726
|
+
<div className="mt-1 break-words text-body text-text-primary">{value}</div>
|
|
727
|
+
</div>
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function Panel({ title, children }: { title: string; children: React.ReactNode }) {
|
|
732
|
+
return (
|
|
733
|
+
<section className="rounded border border-border bg-bg">
|
|
734
|
+
<h3 className="border-b border-border px-3 py-2 text-body font-medium text-text-primary">{title}</h3>
|
|
735
|
+
<div className="divide-y divide-border p-3">{children}</div>
|
|
736
|
+
</section>
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function Row({ primary, secondary }: { primary: string; secondary: string }) {
|
|
741
|
+
return (
|
|
742
|
+
<div className="py-2 first:pt-0 last:pb-0">
|
|
743
|
+
<div className="text-body text-text-primary">{primary}</div>
|
|
744
|
+
<div className="text-label text-text-tertiary">{secondary}</div>
|
|
745
|
+
</div>
|
|
746
|
+
);
|
|
747
|
+
}
|