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
package/plan.md
ADDED
|
@@ -0,0 +1,923 @@
|
|
|
1
|
+
# PLAN.md — Availsync MVP
|
|
2
|
+
|
|
3
|
+
## Goal contract (læs dette først)
|
|
4
|
+
|
|
5
|
+
Du bygger Availsync: en REST API der fungerer som scheduling source-of-truth for AI-agenter.
|
|
6
|
+
Kerneproblemet: flere AI-agenter (salgs-bot, rekrutteringsbot, kalenderagent) booker møder
|
|
7
|
+
uafhængigt og laver double-bookings. Availsync løser det ved at alle agenter tjekker ét
|
|
8
|
+
fælles lag af regler og holds, inden de booker.
|
|
9
|
+
|
|
10
|
+
**Done-betingelse:**
|
|
11
|
+
`npm test` kører grønt (alle test passes), `npm run build` producerer fejlfri TypeScript-build,
|
|
12
|
+
og `docker compose up` starter systemet lokalt. Alle tre endpoints svarer korrekt på
|
|
13
|
+
de smoke tests beskrevet i `/tests/smoke.sh`.
|
|
14
|
+
|
|
15
|
+
**Stop hvis:**
|
|
16
|
+
- En migration fejler og kan ikke rettes automatisk — log fejlen og stop
|
|
17
|
+
- En afhængighed ikke kan installeres — prøv alternativ, ellers stop og skriv til BLOCKERS.md
|
|
18
|
+
- Du er i tvivl om forretningslogik — se kommentarer i denne fil, lav ikke antagelser
|
|
19
|
+
|
|
20
|
+
**Rør ikke:**
|
|
21
|
+
- Denne PLAN.md fil
|
|
22
|
+
- .env.example (kun tilføj, aldrig slet eksisterende variabler)
|
|
23
|
+
- Eventuelle filer brugeren har lagt i /existing/ mappen
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Stack
|
|
28
|
+
|
|
29
|
+
| Lag | Teknologi | Begrundelse |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| Runtime | Node.js 20 + TypeScript 5 | Async-first, god type-støtte til DB |
|
|
32
|
+
| Web framework | Express.js 4 | Simpelt, veldokumenteret |
|
|
33
|
+
| Database | PostgreSQL 16 via `pg` driver | Row-level locking til konfliktresolution |
|
|
34
|
+
| Validation | Zod | Skema-validation + TypeScript-inferens |
|
|
35
|
+
| Testing | Jest + Supertest | Enkel setup, god Express-integration |
|
|
36
|
+
| Containerisering | Docker + docker-compose | Reproducerbar lokal kørsel |
|
|
37
|
+
| Linting | ESLint + Prettier | Konsistent kode |
|
|
38
|
+
|
|
39
|
+
**Brug ikke** Prisma, TypeORM, Sequelize eller andre ORMs — brug rå `pg`-queries.
|
|
40
|
+
Det giver fuld kontrol over `SELECT FOR UPDATE SKIP LOCKED` som konfliktmotoren kræver.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Mappestruktur (opret præcis dette)
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
availsync/
|
|
48
|
+
├── PLAN.md ← denne fil
|
|
49
|
+
├── BLOCKERS.md ← opret som tom fil, log blokeringer her
|
|
50
|
+
├── package.json
|
|
51
|
+
├── tsconfig.json
|
|
52
|
+
├── .env.example
|
|
53
|
+
├── docker-compose.yml
|
|
54
|
+
├── Dockerfile
|
|
55
|
+
├── jest.config.js
|
|
56
|
+
├── src/
|
|
57
|
+
│ ├── index.ts ← Express app entry point
|
|
58
|
+
│ ├── db/
|
|
59
|
+
│ │ ├── client.ts ← pg Pool singleton
|
|
60
|
+
│ │ └── migrations/
|
|
61
|
+
│ │ └── 001_init.sql ← hele databaseskemaet
|
|
62
|
+
│ ├── core/
|
|
63
|
+
│ │ ├── availability.ts ← getAvailableSlots()
|
|
64
|
+
│ │ └── conflict.ts ← resolveConflict()
|
|
65
|
+
│ ├── middleware/
|
|
66
|
+
│ │ └── auth.ts ← Bearer token validering
|
|
67
|
+
│ ├── routes/
|
|
68
|
+
│ │ ├── availability.ts ← GET /v1/availability
|
|
69
|
+
│ │ ├── holds.ts ← POST /v1/holds, DELETE /v1/holds/:id
|
|
70
|
+
│ │ ├── preferences.ts ← GET + PUT /v1/preferences/:agent_id
|
|
71
|
+
│ │ └── orgs.ts ← POST /v1/orgs, POST /v1/orgs/:id/agents
|
|
72
|
+
│ └── types/
|
|
73
|
+
│ └── index.ts ← delte TypeScript-typer
|
|
74
|
+
└── tests/
|
|
75
|
+
├── unit/
|
|
76
|
+
│ ├── conflict.test.ts
|
|
77
|
+
│ └── availability.test.ts
|
|
78
|
+
├── integration/
|
|
79
|
+
│ ├── availability.route.test.ts
|
|
80
|
+
│ ├── holds.route.test.ts
|
|
81
|
+
│ └── preferences.route.test.ts
|
|
82
|
+
└── smoke.sh ← curl-baserede smoke tests
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Checkpoint 1 — Projektopsætning og databaseskema
|
|
88
|
+
|
|
89
|
+
**Valider med:** `npm run build` producerer ingen fejl. `psql $DATABASE_URL -f src/db/migrations/001_init.sql` kører uden fejl.
|
|
90
|
+
|
|
91
|
+
### 1.1 — Initialiser projektet
|
|
92
|
+
|
|
93
|
+
Opret `package.json` med disse præcise scripts:
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"name": "availsync",
|
|
97
|
+
"version": "0.1.0",
|
|
98
|
+
"scripts": {
|
|
99
|
+
"build": "tsc --noEmit",
|
|
100
|
+
"start": "ts-node src/index.ts",
|
|
101
|
+
"dev": "ts-node-dev --respawn src/index.ts",
|
|
102
|
+
"test": "jest --runInBand",
|
|
103
|
+
"migrate": "psql $DATABASE_URL -f src/db/migrations/001_init.sql",
|
|
104
|
+
"lint": "eslint src --ext .ts"
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Installer disse dependencies (ikke mere):
|
|
110
|
+
```
|
|
111
|
+
npm install express pg zod bcrypt uuid date-fns
|
|
112
|
+
npm install --save-dev typescript ts-node ts-node-dev @types/express @types/pg @types/bcrypt @types/uuid jest @types/jest supertest @types/supertest ts-jest eslint prettier
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### 1.2 — TypeScript-konfiguration
|
|
116
|
+
|
|
117
|
+
`tsconfig.json`:
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"compilerOptions": {
|
|
121
|
+
"target": "ES2022",
|
|
122
|
+
"module": "commonjs",
|
|
123
|
+
"lib": ["ES2022"],
|
|
124
|
+
"outDir": "./dist",
|
|
125
|
+
"rootDir": "./src",
|
|
126
|
+
"strict": true,
|
|
127
|
+
"esModuleInterop": true,
|
|
128
|
+
"skipLibCheck": true,
|
|
129
|
+
"forceConsistentCasingInFileNames": true,
|
|
130
|
+
"resolveJsonModule": true
|
|
131
|
+
},
|
|
132
|
+
"include": ["src/**/*"],
|
|
133
|
+
"exclude": ["node_modules", "dist", "tests"]
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`jest.config.js`:
|
|
138
|
+
```js
|
|
139
|
+
module.exports = {
|
|
140
|
+
preset: 'ts-jest',
|
|
141
|
+
testEnvironment: 'node',
|
|
142
|
+
testMatch: ['**/tests/**/*.test.ts'],
|
|
143
|
+
runInBand: true,
|
|
144
|
+
setupFiles: ['<rootDir>/tests/setup.ts']
|
|
145
|
+
};
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Opret `tests/setup.ts`:
|
|
149
|
+
```typescript
|
|
150
|
+
process.env.DATABASE_URL = process.env.TEST_DATABASE_URL || 'postgresql://postgres:postgres@localhost:5432/availsync_test';
|
|
151
|
+
process.env.NODE_ENV = 'test';
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### 1.3 — Miljøvariabler
|
|
155
|
+
|
|
156
|
+
`.env.example`:
|
|
157
|
+
```
|
|
158
|
+
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/availsync
|
|
159
|
+
TEST_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/availsync_test
|
|
160
|
+
PORT=3000
|
|
161
|
+
NODE_ENV=development
|
|
162
|
+
API_KEY_SALT_ROUNDS=10
|
|
163
|
+
CORS_ORIGINS=http://localhost:3000
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### 1.4 — Databaseskema
|
|
167
|
+
|
|
168
|
+
`src/db/migrations/001_init.sql` — implementér disse tabeller præcis:
|
|
169
|
+
|
|
170
|
+
```sql
|
|
171
|
+
-- Aktiver UUID-extension
|
|
172
|
+
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
|
173
|
+
|
|
174
|
+
-- Organisationer (en org kan have mange agenter)
|
|
175
|
+
CREATE TABLE IF NOT EXISTS orgs (
|
|
176
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
177
|
+
name TEXT NOT NULL,
|
|
178
|
+
plan TEXT NOT NULL DEFAULT 'free' CHECK (plan IN ('free', 'individual', 'team')),
|
|
179
|
+
agent_limit INTEGER NOT NULL DEFAULT 3,
|
|
180
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
-- Agenter (en per AI-tool: sales_bot, recruiting_bot, osv.)
|
|
184
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
185
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
186
|
+
org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
|
|
187
|
+
name TEXT NOT NULL,
|
|
188
|
+
api_key_hash TEXT NOT NULL,
|
|
189
|
+
agent_type TEXT NOT NULL DEFAULT 'generic'
|
|
190
|
+
CHECK (agent_type IN ('external_meeting', 'internal', 'focus', 'generic')),
|
|
191
|
+
priority INTEGER NOT NULL DEFAULT 0,
|
|
192
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
193
|
+
);
|
|
194
|
+
CREATE INDEX IF NOT EXISTS idx_agents_org_id ON agents(org_id);
|
|
195
|
+
|
|
196
|
+
-- Ledige tidsvinduet en agent opererer inden for
|
|
197
|
+
CREATE TABLE IF NOT EXISTS availability_windows (
|
|
198
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
199
|
+
agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
|
|
200
|
+
start_at TIMESTAMPTZ NOT NULL,
|
|
201
|
+
end_at TIMESTAMPTZ NOT NULL,
|
|
202
|
+
window_type TEXT NOT NULL DEFAULT 'available'
|
|
203
|
+
CHECK (window_type IN ('available', 'focus', 'blocked')),
|
|
204
|
+
recurrence TEXT, -- null = engangshændelse, 'daily'/'weekly' = gentagen
|
|
205
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
206
|
+
CONSTRAINT chk_window_order CHECK (end_at > start_at)
|
|
207
|
+
);
|
|
208
|
+
CREATE INDEX IF NOT EXISTS idx_windows_agent_time
|
|
209
|
+
ON availability_windows(agent_id, start_at, end_at);
|
|
210
|
+
|
|
211
|
+
-- En agent reserverer et tidsrum (hold) inden commit
|
|
212
|
+
CREATE TABLE IF NOT EXISTS holds (
|
|
213
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
214
|
+
agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
|
|
215
|
+
org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
|
|
216
|
+
start_at TIMESTAMPTZ NOT NULL,
|
|
217
|
+
end_at TIMESTAMPTZ NOT NULL,
|
|
218
|
+
reason TEXT,
|
|
219
|
+
status TEXT NOT NULL DEFAULT 'confirmed'
|
|
220
|
+
CHECK (status IN ('confirmed', 'superseded', 'released')),
|
|
221
|
+
booked_by_agent_id UUID REFERENCES agents(id),
|
|
222
|
+
metadata JSONB,
|
|
223
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
224
|
+
CONSTRAINT chk_hold_order CHECK (end_at > start_at)
|
|
225
|
+
);
|
|
226
|
+
CREATE INDEX IF NOT EXISTS idx_holds_org_time
|
|
227
|
+
ON holds(org_id, start_at, end_at) WHERE status = 'confirmed';
|
|
228
|
+
CREATE INDEX IF NOT EXISTS idx_holds_agent_id ON holds(agent_id);
|
|
229
|
+
|
|
230
|
+
-- Konfliktregler pr. org
|
|
231
|
+
CREATE TABLE IF NOT EXISTS conflict_rules (
|
|
232
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
233
|
+
org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
|
|
234
|
+
rule_type TEXT NOT NULL CHECK (rule_type IN ('priority', 'buffer', 'focus')),
|
|
235
|
+
config JSONB NOT NULL DEFAULT '{}',
|
|
236
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
-- Agent-præferencer (buffer, focus-blokke, osv.)
|
|
240
|
+
CREATE TABLE IF NOT EXISTS agent_preferences (
|
|
241
|
+
agent_id UUID PRIMARY KEY REFERENCES agents(id) ON DELETE CASCADE,
|
|
242
|
+
buffer_minutes_after INTEGER NOT NULL DEFAULT 15,
|
|
243
|
+
buffer_minutes_before INTEGER NOT NULL DEFAULT 0,
|
|
244
|
+
focus_blocks JSONB NOT NULL DEFAULT '[]',
|
|
245
|
+
priority_over_agents JSONB NOT NULL DEFAULT '[]',
|
|
246
|
+
booking_window_days INTEGER NOT NULL DEFAULT 30,
|
|
247
|
+
allow_back_to_back BOOLEAN NOT NULL DEFAULT false,
|
|
248
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
-- Audit log — ét event per hændelse
|
|
252
|
+
CREATE TABLE IF NOT EXISTS hold_events (
|
|
253
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
254
|
+
hold_id UUID NOT NULL REFERENCES holds(id) ON DELETE CASCADE,
|
|
255
|
+
event_type TEXT NOT NULL
|
|
256
|
+
CHECK (event_type IN ('created', 'superseded', 'released', 'conflict_won', 'conflict_lost')),
|
|
257
|
+
agent_id UUID NOT NULL REFERENCES agents(id),
|
|
258
|
+
org_id UUID NOT NULL REFERENCES orgs(id),
|
|
259
|
+
conflicting_hold_id UUID REFERENCES holds(id),
|
|
260
|
+
rule_applied TEXT,
|
|
261
|
+
metadata JSONB,
|
|
262
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
263
|
+
);
|
|
264
|
+
CREATE INDEX IF NOT EXISTS idx_hold_events_org ON hold_events(org_id, created_at DESC);
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### 1.5 — Database-klient
|
|
268
|
+
|
|
269
|
+
`src/db/client.ts`:
|
|
270
|
+
```typescript
|
|
271
|
+
import { Pool } from 'pg';
|
|
272
|
+
|
|
273
|
+
const pool = new Pool({
|
|
274
|
+
connectionString: process.env.DATABASE_URL,
|
|
275
|
+
max: 10,
|
|
276
|
+
idleTimeoutMillis: 30000,
|
|
277
|
+
connectionTimeoutMillis: 2000,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
export default pool;
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### 1.6 — Delte typer
|
|
284
|
+
|
|
285
|
+
`src/types/index.ts` — definer disse interfaces:
|
|
286
|
+
```typescript
|
|
287
|
+
export interface Org {
|
|
288
|
+
id: string;
|
|
289
|
+
name: string;
|
|
290
|
+
plan: 'free' | 'individual' | 'team';
|
|
291
|
+
agent_limit: number;
|
|
292
|
+
created_at: Date;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export interface Agent {
|
|
296
|
+
id: string;
|
|
297
|
+
org_id: string;
|
|
298
|
+
name: string;
|
|
299
|
+
api_key_hash: string;
|
|
300
|
+
agent_type: 'external_meeting' | 'internal' | 'focus' | 'generic';
|
|
301
|
+
priority: number;
|
|
302
|
+
created_at: Date;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export interface Hold {
|
|
306
|
+
id: string;
|
|
307
|
+
agent_id: string;
|
|
308
|
+
org_id: string;
|
|
309
|
+
start_at: Date;
|
|
310
|
+
end_at: Date;
|
|
311
|
+
reason?: string;
|
|
312
|
+
status: 'confirmed' | 'superseded' | 'released';
|
|
313
|
+
booked_by_agent_id?: string;
|
|
314
|
+
metadata?: Record<string, unknown>;
|
|
315
|
+
created_at: Date;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export interface AvailabilityWindow {
|
|
319
|
+
id: string;
|
|
320
|
+
agent_id: string;
|
|
321
|
+
start_at: Date;
|
|
322
|
+
end_at: Date;
|
|
323
|
+
window_type: 'available' | 'focus' | 'blocked';
|
|
324
|
+
recurrence?: string;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export interface AgentPreferences {
|
|
328
|
+
agent_id: string;
|
|
329
|
+
buffer_minutes_after: number;
|
|
330
|
+
buffer_minutes_before: number;
|
|
331
|
+
focus_blocks: FocusBlock[];
|
|
332
|
+
priority_over_agents: string[];
|
|
333
|
+
booking_window_days: number;
|
|
334
|
+
allow_back_to_back: boolean;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export interface FocusBlock {
|
|
338
|
+
weekday: number; // 0 = søndag, 6 = lørdag
|
|
339
|
+
start_time: string; // "HH:MM"
|
|
340
|
+
end_time: string; // "HH:MM"
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export interface AvailableSlot {
|
|
344
|
+
start: Date;
|
|
345
|
+
end: Date;
|
|
346
|
+
confidence: number;
|
|
347
|
+
competing_holds_count: number;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export interface RequestWithAgent extends Express.Request {
|
|
351
|
+
agent: Agent;
|
|
352
|
+
org: Org;
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## Checkpoint 2 — Kernelogik
|
|
359
|
+
|
|
360
|
+
**Valider med:** `npm test -- tests/unit/` kører grønt (alle unit tests passes).
|
|
361
|
+
|
|
362
|
+
### 2.1 — Auth-middleware
|
|
363
|
+
|
|
364
|
+
`src/middleware/auth.ts`:
|
|
365
|
+
|
|
366
|
+
Implementér en Express middleware der:
|
|
367
|
+
1. Læser `Authorization: Bearer <token>` headeren — returnér 401 `{"error": "missing_auth", "message": "Authorization header required"}` hvis den mangler
|
|
368
|
+
2. Slår token op i `agents`-tabellen med `SELECT agents.*, orgs.* FROM agents JOIN orgs ON agents.org_id = orgs.id WHERE agents.api_key_hash = $1` — brug bcrypt.compare(token, row.api_key_hash)
|
|
369
|
+
3. Returnér 401 `{"error": "invalid_key"}` hvis ingen match
|
|
370
|
+
4. Sætter `req.agent` og `req.org` på request-objektet
|
|
371
|
+
5. Kalder `next()`
|
|
372
|
+
|
|
373
|
+
Eksportér typen `AuthenticatedRequest extends Request` med `agent: Agent` og `org: Org`.
|
|
374
|
+
|
|
375
|
+
### 2.2 — Konfliktresolution-motor
|
|
376
|
+
|
|
377
|
+
`src/core/conflict.ts`:
|
|
378
|
+
|
|
379
|
+
Implementér disse to funktioner:
|
|
380
|
+
|
|
381
|
+
**`determineWinner(holdA: Hold, agentA: Agent, holdB: Hold, agentB: Agent, prefs: AgentPreferences[]): Hold`**
|
|
382
|
+
|
|
383
|
+
Prioritetslogik i denne rækkefølge (returnér vinderen):
|
|
384
|
+
1. Eksplicit `priority_over_agents`-liste: hvis agentA har agentB's ID i sin `priority_over_agents`-liste → agentA vinder
|
|
385
|
+
2. `agent_type`-hierarki: `external_meeting` (4) > `internal` (3) > `focus` (2) > `generic` (1)
|
|
386
|
+
3. Eksplicit `priority`-felt på agent: højere vinder
|
|
387
|
+
4. Tie-breaker: ældste `created_at` på hold vinder (first-come-first-served)
|
|
388
|
+
|
|
389
|
+
**`checkAndResolveConflict(newHold: Omit<Hold, 'id'|'created_at'>, client: PoolClient): Promise<{winner: 'new'|'existing', conflictingHold?: Hold}>`**
|
|
390
|
+
|
|
391
|
+
Flowet:
|
|
392
|
+
1. Kør `SELECT * FROM holds WHERE org_id = $1 AND status = 'confirmed' AND start_at < $3 AND end_at > $2 FOR UPDATE SKIP LOCKED` — brug $1=org_id, $2=start_at, $3=end_at for at finde overlappende confirmed holds
|
|
393
|
+
2. Hvis ingen overlap → returnér `{winner: 'new'}`
|
|
394
|
+
3. For hvert overlappende hold: hent begge agenter, kald `determineWinner()`
|
|
395
|
+
4. Hvis new hold vinder alle sammenstød → returnér `{winner: 'new', conflictingHold: losingHold}`
|
|
396
|
+
5. Hvis new hold taber ét sammenstød → returnér `{winner: 'existing', conflictingHold: winningHold}`
|
|
397
|
+
|
|
398
|
+
### 2.3 — Availability-beregning
|
|
399
|
+
|
|
400
|
+
`src/core/availability.ts`:
|
|
401
|
+
|
|
402
|
+
**`getAvailableSlots(params: GetSlotsParams, client?: PoolClient): Promise<AvailableSlot[]>`**
|
|
403
|
+
|
|
404
|
+
```typescript
|
|
405
|
+
interface GetSlotsParams {
|
|
406
|
+
agent_id: string;
|
|
407
|
+
org_id: string;
|
|
408
|
+
from: Date;
|
|
409
|
+
to: Date;
|
|
410
|
+
duration_minutes: number;
|
|
411
|
+
include_competing_agents?: boolean;
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Algoritme:
|
|
416
|
+
1. Hent `availability_windows` for agenten hvor `window_type = 'available'` og perioden overlapper `from`–`to`
|
|
417
|
+
2. Hent `agent_preferences` for agenten
|
|
418
|
+
3. Hent alle confirmed holds i org i perioden (hvis `include_competing_agents = true`) eller kun agentens egne holds
|
|
419
|
+
4. For hvert availability-vindue: generer slots med `duration_minutes` skridt (tætpakket, ikke hvert minut)
|
|
420
|
+
5. Filtrer slots der overlapper med:
|
|
421
|
+
- Existing holds (direkte overlap)
|
|
422
|
+
- Focus blocks fra preferences (omregn weekday+tidspunkt til konkrete datoer i perioden)
|
|
423
|
+
- Buffer-tid rundt om eksisterende holds (brug `buffer_minutes_after` og `buffer_minutes_before`)
|
|
424
|
+
6. For hvert tilbageværende slot: tæl `competing_holds_count` (antal holds fra andre agenter tæt på slottet, indenfor 30 min)
|
|
425
|
+
7. Sæt `confidence = 1.0 - (competing_holds_count * 0.15)`, minimum 0.1
|
|
426
|
+
8. Returnér sorteret liste af `AvailableSlot`
|
|
427
|
+
|
|
428
|
+
### 2.4 — Unit tests
|
|
429
|
+
|
|
430
|
+
`tests/unit/conflict.test.ts` — skriv tests der dækker:
|
|
431
|
+
- To agenter med ingen priority_over_agents → agent_type-hierarki afgør
|
|
432
|
+
- external_meeting vinder over internal
|
|
433
|
+
- Tie på agent_type → priority-felt afgør
|
|
434
|
+
- Komplet tie → ældste hold vinder (first-come-first-served)
|
|
435
|
+
- Eksplicit priority_over_agents-liste tilsidesætter alt andet
|
|
436
|
+
|
|
437
|
+
`tests/unit/availability.test.ts` — skriv tests der dækker:
|
|
438
|
+
- Slot inden for vindue returneres
|
|
439
|
+
- Slot overlappende med hold filtreres
|
|
440
|
+
- Buffer-tid fjerner slots tæt på hold
|
|
441
|
+
- Focus-blok fjerner slots der overlapper
|
|
442
|
+
- confidence beregnes korrekt ved competing holds
|
|
443
|
+
- Tom liste returneres hvis ingen ledige slots
|
|
444
|
+
|
|
445
|
+
---
|
|
446
|
+
|
|
447
|
+
## Checkpoint 3 — REST API endpoints
|
|
448
|
+
|
|
449
|
+
**Valider med:** `npm test -- tests/integration/` kører grønt.
|
|
450
|
+
|
|
451
|
+
For alle endpoints gælder:
|
|
452
|
+
- Content-Type: application/json på alle responses
|
|
453
|
+
- Fejl returneres altid som `{"error": "<machine_readable_code>", "message": "<human_readable>", "details"?: {...}}`
|
|
454
|
+
- Alle timestamps i ISO 8601 / UTC
|
|
455
|
+
- Auth-middleware skal køre på alle endpoints undtagen `POST /v1/orgs` og `GET /health`
|
|
456
|
+
|
|
457
|
+
### 3.1 — Health check og app entry point
|
|
458
|
+
|
|
459
|
+
`src/index.ts`:
|
|
460
|
+
```typescript
|
|
461
|
+
import express from 'express';
|
|
462
|
+
import availabilityRouter from './routes/availability';
|
|
463
|
+
import holdsRouter from './routes/holds';
|
|
464
|
+
import preferencesRouter from './routes/preferences';
|
|
465
|
+
import orgsRouter from './routes/orgs';
|
|
466
|
+
|
|
467
|
+
const app = express();
|
|
468
|
+
app.use(express.json());
|
|
469
|
+
|
|
470
|
+
app.get('/health', async (req, res) => {
|
|
471
|
+
try {
|
|
472
|
+
await pool.query('SELECT 1');
|
|
473
|
+
res.json({ status: 'ok', db: 'connected', timestamp: new Date().toISOString() });
|
|
474
|
+
} catch {
|
|
475
|
+
res.status(503).json({ status: 'error', db: 'disconnected' });
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
app.use('/v1', orgsRouter);
|
|
480
|
+
app.use('/v1', availabilityRouter);
|
|
481
|
+
app.use('/v1', holdsRouter);
|
|
482
|
+
app.use('/v1', preferencesRouter);
|
|
483
|
+
|
|
484
|
+
const PORT = process.env.PORT || 3000;
|
|
485
|
+
app.listen(PORT, () => console.log(`Availsync running on :${PORT}`));
|
|
486
|
+
|
|
487
|
+
export default app;
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### 3.2 — Org og agent provisioning
|
|
491
|
+
|
|
492
|
+
`src/routes/orgs.ts` — implementér:
|
|
493
|
+
|
|
494
|
+
**`POST /v1/orgs`** (ingen auth krævet)
|
|
495
|
+
- Body (Zod-valideret): `{ name: string }`
|
|
496
|
+
- Opret org med `plan = 'free'` og `agent_limit = 3`
|
|
497
|
+
- Returnér 201: `{ org: Org }`
|
|
498
|
+
|
|
499
|
+
**`POST /v1/orgs/:org_id/agents`** (ingen auth krævet til MVP)
|
|
500
|
+
- Body (Zod-valideret): `{ name: string, agent_type?: AgentType, priority?: number }`
|
|
501
|
+
- Check at org ikke overskrider `agent_limit` — returnér 402 `{"error": "agent_limit_reached"}` hvis den gør
|
|
502
|
+
- Generer API-nøgle: `crypto.randomBytes(32).toString('hex')`
|
|
503
|
+
- Hash nøglen: `bcrypt.hash(apiKey, parseInt(process.env.API_KEY_SALT_ROUNDS || '10'))`
|
|
504
|
+
- Gem agent med hash
|
|
505
|
+
- Opret `agent_preferences`-række med defaults
|
|
506
|
+
- Returnér 201: `{ agent: Agent, api_key: string }` — vis `api_key` kun i dette svar
|
|
507
|
+
|
|
508
|
+
**`GET /v1/orgs/:org_id/agents`** (ingen auth krævet til MVP)
|
|
509
|
+
- Returnér alle agenter i org (uden api_key_hash)
|
|
510
|
+
|
|
511
|
+
### 3.3 — Availability-endpoint
|
|
512
|
+
|
|
513
|
+
`src/routes/availability.ts` — implementér:
|
|
514
|
+
|
|
515
|
+
**`GET /v1/availability`** (auth krævet)
|
|
516
|
+
|
|
517
|
+
Query params — valider med Zod:
|
|
518
|
+
```
|
|
519
|
+
agent_id: string (UUID)
|
|
520
|
+
from: string (ISO8601, skal være fremtidig)
|
|
521
|
+
to: string (ISO8601, skal være efter from)
|
|
522
|
+
duration_minutes: number (1–480)
|
|
523
|
+
include_competing_agents?: boolean (default: false)
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
Flow:
|
|
527
|
+
1. Valider at `req.agent.id === agent_id` ELLER `req.agent.org_id === org_id_for_agent` — en agent må kun se sin egen org's availability
|
|
528
|
+
2. Kald `getAvailableSlots(params)`
|
|
529
|
+
3. Returnér 200:
|
|
530
|
+
```json
|
|
531
|
+
{
|
|
532
|
+
"slots": [
|
|
533
|
+
{ "start": "ISO8601", "end": "ISO8601", "confidence": 0.85, "competing_holds_count": 1 }
|
|
534
|
+
],
|
|
535
|
+
"rules_applied": ["buffer_15min", "focus_block_mornings"],
|
|
536
|
+
"generated_at": "ISO8601",
|
|
537
|
+
"total_slots": 12
|
|
538
|
+
}
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
Fejlcases:
|
|
542
|
+
- Manglende params → 400 med Zod-fejldetaljer
|
|
543
|
+
- `to` mere end `booking_window_days` ude i fremtiden → 400 `{"error": "outside_booking_window"}`
|
|
544
|
+
- `from` i fortiden → 400 `{"error": "past_datetime"}`
|
|
545
|
+
|
|
546
|
+
### 3.4 — Holds-endpoints
|
|
547
|
+
|
|
548
|
+
`src/routes/holds.ts` — implementér:
|
|
549
|
+
|
|
550
|
+
**`POST /v1/holds`** (auth krævet)
|
|
551
|
+
|
|
552
|
+
Body (Zod-valideret):
|
|
553
|
+
```
|
|
554
|
+
agent_id: string (UUID)
|
|
555
|
+
start_at: string (ISO8601)
|
|
556
|
+
end_at: string (ISO8601)
|
|
557
|
+
reason?: string (max 500 chars)
|
|
558
|
+
metadata?: object
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
Flow — ALT SKAL SKE I ÉN POSTGRESQL-TRANSAKTION:
|
|
562
|
+
1. Valider agent tilhører req.org
|
|
563
|
+
2. Check at start_at/end_at falder inden for et availability_window for agenten
|
|
564
|
+
3. Kald `checkAndResolveConflict()`
|
|
565
|
+
4. **Hvis winner = 'new':**
|
|
566
|
+
- Indsæt hold med `status = 'confirmed'`
|
|
567
|
+
- Opret `hold_events` event: `type = 'created'`
|
|
568
|
+
- Hvis der var et superseded hold: opdatér det til `status = 'superseded'`, opret event `type = 'superseded'`
|
|
569
|
+
- COMMIT
|
|
570
|
+
- Returnér 201: `{ hold: Hold, conflict_resolved: boolean, superseded_hold_id?: string }`
|
|
571
|
+
5. **Hvis winner = 'existing':**
|
|
572
|
+
- ROLLBACK
|
|
573
|
+
- Hent 3 alternative slots via `getAvailableSlots()`
|
|
574
|
+
- Returnér 409:
|
|
575
|
+
```json
|
|
576
|
+
{
|
|
577
|
+
"error": "slot_taken",
|
|
578
|
+
"message": "Another agent has priority for this time slot",
|
|
579
|
+
"winning_hold_id": "uuid",
|
|
580
|
+
"suggested_alternatives": [ { "start": "...", "end": "...", "confidence": 0.9 } ]
|
|
581
|
+
}
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
**`DELETE /v1/holds/:hold_id`** (auth krævet)
|
|
585
|
+
- Valider at hold tilhører req.agent
|
|
586
|
+
- Opdatér `status = 'released'`
|
|
587
|
+
- Opret `hold_events` event: `type = 'released'`
|
|
588
|
+
- Returnér 200: `{ hold: Hold }`
|
|
589
|
+
|
|
590
|
+
**`GET /v1/holds`** (auth krævet)
|
|
591
|
+
- Query params: `agent_id?`, `from?`, `to?`, `status?` (default: 'confirmed')
|
|
592
|
+
- Returnér holds filtreret af params, sorteret by start_at ASC
|
|
593
|
+
- Kun holds inden for req.org
|
|
594
|
+
|
|
595
|
+
### 3.5 — Preferences-endpoints
|
|
596
|
+
|
|
597
|
+
`src/routes/preferences.ts` — implementér:
|
|
598
|
+
|
|
599
|
+
**`GET /v1/preferences/:agent_id`** (auth krævet)
|
|
600
|
+
- Valider agent tilhører req.org
|
|
601
|
+
- Hent fra `agent_preferences`
|
|
602
|
+
- Returnér 200: `{ preferences: AgentPreferences }`
|
|
603
|
+
|
|
604
|
+
**`PUT /v1/preferences/:agent_id`** (auth krævet)
|
|
605
|
+
|
|
606
|
+
Body — validér med Zod præcis:
|
|
607
|
+
```typescript
|
|
608
|
+
const PreferencesSchema = z.object({
|
|
609
|
+
buffer_minutes_after: z.number().int().min(0).max(120).optional(),
|
|
610
|
+
buffer_minutes_before: z.number().int().min(0).max(120).optional(),
|
|
611
|
+
focus_blocks: z.array(z.object({
|
|
612
|
+
weekday: z.number().int().min(0).max(6),
|
|
613
|
+
start_time: z.string().regex(/^\d{2}:\d{2}$/),
|
|
614
|
+
end_time: z.string().regex(/^\d{2}:\d{2}$/)
|
|
615
|
+
})).max(20).optional(),
|
|
616
|
+
priority_over_agents: z.array(z.string().uuid()).max(50).optional(),
|
|
617
|
+
booking_window_days: z.number().int().min(1).max(365).optional(),
|
|
618
|
+
allow_back_to_back: z.boolean().optional()
|
|
619
|
+
});
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
- UPSERT til `agent_preferences` (kun opdatér felter der er tilsendt)
|
|
623
|
+
- Returnér 200: `{ preferences: AgentPreferences }`
|
|
624
|
+
- Returnér 422 med Zod-fejldetaljer ved ugyldige felter
|
|
625
|
+
|
|
626
|
+
### 3.6 — Availability windows-endpoint
|
|
627
|
+
|
|
628
|
+
`src/routes/availability.ts` — tilføj:
|
|
629
|
+
|
|
630
|
+
**`POST /v1/availability-windows`** (auth krævet)
|
|
631
|
+
|
|
632
|
+
Body:
|
|
633
|
+
```
|
|
634
|
+
agent_id: string (UUID)
|
|
635
|
+
start_at: string (ISO8601)
|
|
636
|
+
end_at: string (ISO8601)
|
|
637
|
+
window_type: 'available' | 'focus' | 'blocked'
|
|
638
|
+
recurrence?: 'daily' | 'weekly'
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
- Indsæt i `availability_windows`
|
|
642
|
+
- Returnér 201: `{ window: AvailabilityWindow }`
|
|
643
|
+
|
|
644
|
+
**`GET /v1/availability-windows`** (auth krævet)
|
|
645
|
+
- Query params: `agent_id`, `from?`, `to?`
|
|
646
|
+
- Returnér windows for agenten
|
|
647
|
+
|
|
648
|
+
### 3.7 — Integration tests
|
|
649
|
+
|
|
650
|
+
`tests/integration/availability.route.test.ts`:
|
|
651
|
+
- Opret test-org og test-agent i beforeAll
|
|
652
|
+
- Test: GET /v1/availability med gyldige params returnerer slots
|
|
653
|
+
- Test: GET /v1/availability uden auth returnerer 401
|
|
654
|
+
- Test: GET /v1/availability med ugyldig agent_id returnerer 403
|
|
655
|
+
- Test: GET /v1/availability med `from` i fortiden returnerer 400
|
|
656
|
+
|
|
657
|
+
`tests/integration/holds.route.test.ts`:
|
|
658
|
+
- Test: POST /v1/holds booker succesfuldt og returnerer 201
|
|
659
|
+
- Test: POST /v1/holds med overlappende hold fra lavere-prioritet agent: returnerer 201 med conflict_resolved=true
|
|
660
|
+
- Test: POST /v1/holds med overlappende hold fra højere-prioritet agent: returnerer 409 med suggested_alternatives
|
|
661
|
+
- Test: DELETE /v1/holds/:id frigiver holdet korrekt
|
|
662
|
+
- Test: Concurrent booking — kør 10 simultane POST /v1/holds til samme slot, verificér at præcis 1 lykkes
|
|
663
|
+
|
|
664
|
+
`tests/integration/preferences.route.test.ts`:
|
|
665
|
+
- Test: GET /v1/preferences returnerer defaults
|
|
666
|
+
- Test: PUT /v1/preferences opdaterer buffer_minutes_after
|
|
667
|
+
- Test: PUT /v1/preferences med ugyldig weekday returnerer 422
|
|
668
|
+
|
|
669
|
+
---
|
|
670
|
+
|
|
671
|
+
## Checkpoint 4 — Docker og smoke tests
|
|
672
|
+
|
|
673
|
+
**Valider med:** `docker compose up -d` starter uden fejl, `bash tests/smoke.sh` printer kun OK-linjer.
|
|
674
|
+
|
|
675
|
+
### 4.1 — Dockerfile
|
|
676
|
+
|
|
677
|
+
```dockerfile
|
|
678
|
+
FROM node:20-alpine AS builder
|
|
679
|
+
WORKDIR /app
|
|
680
|
+
COPY package*.json ./
|
|
681
|
+
RUN npm ci
|
|
682
|
+
COPY . .
|
|
683
|
+
RUN npm run build
|
|
684
|
+
|
|
685
|
+
FROM node:20-alpine
|
|
686
|
+
WORKDIR /app
|
|
687
|
+
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
|
|
688
|
+
COPY --from=builder /app/node_modules ./node_modules
|
|
689
|
+
COPY --from=builder /app/src ./src
|
|
690
|
+
COPY --from=builder /app/tsconfig.json ./
|
|
691
|
+
USER nodejs
|
|
692
|
+
ENV NODE_ENV=production
|
|
693
|
+
EXPOSE 3000
|
|
694
|
+
CMD ["node", "-r", "ts-node/register", "src/index.ts"]
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
### 4.2 — docker-compose.yml
|
|
698
|
+
|
|
699
|
+
```yaml
|
|
700
|
+
version: '3.9'
|
|
701
|
+
services:
|
|
702
|
+
api:
|
|
703
|
+
build: .
|
|
704
|
+
ports:
|
|
705
|
+
- "3000:3000"
|
|
706
|
+
environment:
|
|
707
|
+
DATABASE_URL: postgresql://postgres:postgres@db:5432/availsync
|
|
708
|
+
PORT: 3000
|
|
709
|
+
NODE_ENV: production
|
|
710
|
+
API_KEY_SALT_ROUNDS: 10
|
|
711
|
+
depends_on:
|
|
712
|
+
db:
|
|
713
|
+
condition: service_healthy
|
|
714
|
+
restart: unless-stopped
|
|
715
|
+
|
|
716
|
+
db:
|
|
717
|
+
image: postgres:16-alpine
|
|
718
|
+
environment:
|
|
719
|
+
POSTGRES_DB: availsync
|
|
720
|
+
POSTGRES_USER: postgres
|
|
721
|
+
POSTGRES_PASSWORD: postgres
|
|
722
|
+
volumes:
|
|
723
|
+
- postgres_data:/var/lib/postgresql/data
|
|
724
|
+
- ./src/db/migrations/001_init.sql:/docker-entrypoint-initdb.d/001_init.sql
|
|
725
|
+
healthcheck:
|
|
726
|
+
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
727
|
+
interval: 5s
|
|
728
|
+
timeout: 5s
|
|
729
|
+
retries: 5
|
|
730
|
+
ports:
|
|
731
|
+
- "5432:5432"
|
|
732
|
+
|
|
733
|
+
volumes:
|
|
734
|
+
postgres_data:
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
### 4.3 — Smoke tests
|
|
738
|
+
|
|
739
|
+
`tests/smoke.sh` — en serie curl-kald der verificerer hele API'et end-to-end:
|
|
740
|
+
|
|
741
|
+
```bash
|
|
742
|
+
#!/bin/bash
|
|
743
|
+
set -e
|
|
744
|
+
BASE="http://localhost:3000"
|
|
745
|
+
ERRORS=0
|
|
746
|
+
|
|
747
|
+
check() {
|
|
748
|
+
local desc=$1
|
|
749
|
+
local expected=$2
|
|
750
|
+
local actual=$3
|
|
751
|
+
if [ "$actual" = "$expected" ]; then
|
|
752
|
+
echo "OK: $desc"
|
|
753
|
+
else
|
|
754
|
+
echo "FAIL: $desc (expected $expected, got $actual)"
|
|
755
|
+
ERRORS=$((ERRORS + 1))
|
|
756
|
+
fi
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
echo "=== Availsync Smoke Tests ==="
|
|
760
|
+
|
|
761
|
+
# Health check
|
|
762
|
+
STATUS=$(curl -s -o /dev/null -w "%{http_code}" $BASE/health)
|
|
763
|
+
check "Health endpoint" "200" "$STATUS"
|
|
764
|
+
|
|
765
|
+
# Opret org
|
|
766
|
+
ORG_RESPONSE=$(curl -s -X POST $BASE/v1/orgs \
|
|
767
|
+
-H "Content-Type: application/json" \
|
|
768
|
+
-d '{"name": "Test Org"}')
|
|
769
|
+
ORG_ID=$(echo $ORG_RESPONSE | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
|
|
770
|
+
check "Create org returns id" "true" "$([ -n "$ORG_ID" ] && echo true || echo false)"
|
|
771
|
+
|
|
772
|
+
# Opret agent
|
|
773
|
+
AGENT_RESPONSE=$(curl -s -X POST $BASE/v1/orgs/$ORG_ID/agents \
|
|
774
|
+
-H "Content-Type: application/json" \
|
|
775
|
+
-d '{"name": "Sales Bot", "agent_type": "external_meeting"}')
|
|
776
|
+
API_KEY=$(echo $AGENT_RESPONSE | grep -o '"api_key":"[^"]*"' | cut -d'"' -f4)
|
|
777
|
+
AGENT_ID=$(echo $AGENT_RESPONSE | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
|
|
778
|
+
check "Create agent returns api_key" "true" "$([ -n "$API_KEY" ] && echo true || echo false)"
|
|
779
|
+
|
|
780
|
+
# Opret availability window (næste uge, man-fre 9-17)
|
|
781
|
+
TOMORROW=$(date -d "+1 day" +%Y-%m-%dT09:00:00Z 2>/dev/null || date -v+1d +%Y-%m-%dT09:00:00Z)
|
|
782
|
+
TOMORROW_END=$(date -d "+1 day" +%Y-%m-%dT17:00:00Z 2>/dev/null || date -v+1d +%Y-%m-%dT17:00:00Z)
|
|
783
|
+
WIN_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST $BASE/v1/availability-windows \
|
|
784
|
+
-H "Authorization: Bearer $API_KEY" \
|
|
785
|
+
-H "Content-Type: application/json" \
|
|
786
|
+
-d "{\"agent_id\":\"$AGENT_ID\",\"start_at\":\"$TOMORROW\",\"end_at\":\"$TOMORROW_END\",\"window_type\":\"available\"}")
|
|
787
|
+
check "Create availability window" "201" "$WIN_STATUS"
|
|
788
|
+
|
|
789
|
+
# Hent availability
|
|
790
|
+
AVAIL_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
|
|
791
|
+
"$BASE/v1/availability?agent_id=$AGENT_ID&from=$TOMORROW&to=$TOMORROW_END&duration_minutes=30" \
|
|
792
|
+
-H "Authorization: Bearer $API_KEY")
|
|
793
|
+
check "Get availability" "200" "$AVAIL_STATUS"
|
|
794
|
+
|
|
795
|
+
# Book hold
|
|
796
|
+
HOLD_RESPONSE=$(curl -s -X POST $BASE/v1/holds \
|
|
797
|
+
-H "Authorization: Bearer $API_KEY" \
|
|
798
|
+
-H "Content-Type: application/json" \
|
|
799
|
+
-d "{\"agent_id\":\"$AGENT_ID\",\"start_at\":\"$TOMORROW\",\"end_at\":\"$(date -d "+1 day" +%Y-%m-%dT10:00:00Z 2>/dev/null || date -v+1d +%Y-%m-%dT10:00:00Z)\",\"reason\":\"Test booking\"}")
|
|
800
|
+
HOLD_STATUS=$(echo $HOLD_RESPONSE | grep -o '"status":"[^"]*"' | head -1 | cut -d'"' -f4)
|
|
801
|
+
check "Book hold returns confirmed status" "confirmed" "$HOLD_STATUS"
|
|
802
|
+
|
|
803
|
+
# Test auth fejl
|
|
804
|
+
UNAUTH_STATUS=$(curl -s -o /dev/null -w "%{http_code}" $BASE/v1/availability?agent_id=$AGENT_ID)
|
|
805
|
+
check "Unauthenticated request returns 401" "401" "$UNAUTH_STATUS"
|
|
806
|
+
|
|
807
|
+
echo ""
|
|
808
|
+
echo "=== Results: $ERRORS failure(s) ==="
|
|
809
|
+
exit $ERRORS
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
---
|
|
813
|
+
|
|
814
|
+
## Checkpoint 5 — MCP-server (bonus, byg kun hvis alle tests er grønne)
|
|
815
|
+
|
|
816
|
+
**Valider med:** `node src/mcp/server.js --help` kører uden fejl.
|
|
817
|
+
|
|
818
|
+
Kun hvis checkpoints 1–4 er bestået: Byg en minimal MCP-server i `src/mcp/server.ts`
|
|
819
|
+
med `@modelcontextprotocol/sdk`. Installér: `npm install @modelcontextprotocol/sdk`
|
|
820
|
+
|
|
821
|
+
Eksponér præcis tre tools:
|
|
822
|
+
|
|
823
|
+
**`check_availability`**
|
|
824
|
+
```typescript
|
|
825
|
+
{
|
|
826
|
+
name: "check_availability",
|
|
827
|
+
description: "Check available time slots for an AI scheduling agent before booking. Always call this before book_slot.",
|
|
828
|
+
inputSchema: {
|
|
829
|
+
type: "object",
|
|
830
|
+
properties: {
|
|
831
|
+
agent_id: { type: "string", description: "UUID of the agent to check availability for" },
|
|
832
|
+
from: { type: "string", description: "Start of search window (ISO 8601 UTC)" },
|
|
833
|
+
to: { type: "string", description: "End of search window (ISO 8601 UTC)" },
|
|
834
|
+
duration_minutes: { type: "number", description: "Required meeting duration in minutes" }
|
|
835
|
+
},
|
|
836
|
+
required: ["agent_id", "from", "to", "duration_minutes"]
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
**`book_slot`**
|
|
842
|
+
```typescript
|
|
843
|
+
{
|
|
844
|
+
name: "book_slot",
|
|
845
|
+
description: "Reserve a time slot. Returns confirmed hold or 409 with alternative suggestions if slot is taken by higher-priority agent.",
|
|
846
|
+
inputSchema: { /* agent_id, start_at, end_at, reason */ }
|
|
847
|
+
}
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
**`release_slot`**
|
|
851
|
+
```typescript
|
|
852
|
+
{
|
|
853
|
+
name: "release_slot",
|
|
854
|
+
description: "Release a previously booked hold so other agents can use the time.",
|
|
855
|
+
inputSchema: { /* hold_id */ }
|
|
856
|
+
}
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
Server læser `AVAILSYNC_API_URL` og `AVAILSYNC_API_KEY` fra environment.
|
|
860
|
+
Start på stdio transport: `new StdioServerTransport()`.
|
|
861
|
+
|
|
862
|
+
Tilføj til `package.json`:
|
|
863
|
+
```json
|
|
864
|
+
"mcp": "ts-node src/mcp/server.ts"
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
Opret `MCP_SETUP.md` med:
|
|
868
|
+
```markdown
|
|
869
|
+
# Tilslut Availsync til Claude Desktop
|
|
870
|
+
|
|
871
|
+
Tilføj til ~/Library/Application Support/Claude/claude_desktop_config.json:
|
|
872
|
+
|
|
873
|
+
{
|
|
874
|
+
"mcpServers": {
|
|
875
|
+
"availsync": {
|
|
876
|
+
"command": "npx",
|
|
877
|
+
"args": ["ts-node", "/path/to/availsync/src/mcp/server.ts"],
|
|
878
|
+
"env": {
|
|
879
|
+
"AVAILSYNC_API_URL": "http://localhost:3000",
|
|
880
|
+
"AVAILSYNC_API_KEY": "din-agent-api-nøgle-her"
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
---
|
|
888
|
+
|
|
889
|
+
## Vigtige implementeringsnoter
|
|
890
|
+
|
|
891
|
+
**Race conditions:** `SELECT FOR UPDATE SKIP LOCKED` er kritisk i `checkAndResolveConflict()`.
|
|
892
|
+
Uden det kan to agenter booke samme slot simultant. Alle holds-operationer skal køre
|
|
893
|
+
i en eksplicit transaktion: `BEGIN` → conflict check → INSERT/UPDATE → `COMMIT`.
|
|
894
|
+
|
|
895
|
+
**Timezone-håndtering:** Gem ALT i UTC i databasen (TIMESTAMPTZ). Frontend konverterer
|
|
896
|
+
til lokal tid. Brug aldrig `new Date()` uden eksplicit timezone. `date-fns` bruges
|
|
897
|
+
til dato-matematik i `getAvailableSlots()`.
|
|
898
|
+
|
|
899
|
+
**Zod-validering:** Validér altid input med Zod FØR det rammer databasen.
|
|
900
|
+
Returnér Zod-fejl direkte som 422-response:
|
|
901
|
+
```typescript
|
|
902
|
+
const result = Schema.safeParse(req.body);
|
|
903
|
+
if (!result.success) {
|
|
904
|
+
return res.status(422).json({
|
|
905
|
+
error: 'validation_error',
|
|
906
|
+
details: result.error.flatten()
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
**Error handling:** Pak alle route-handlers ind i try/catch.
|
|
912
|
+
Log fejlen til console.error og returnér 500:
|
|
913
|
+
```typescript
|
|
914
|
+
} catch (err) {
|
|
915
|
+
console.error('[holds] unexpected error:', err);
|
|
916
|
+
return res.status(500).json({ error: 'internal_error', message: 'An unexpected error occurred' });
|
|
917
|
+
}
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
**Test-isolation:** Hvert integration-test-suite skal:
|
|
921
|
+
- I `beforeAll`: kør migration på test-DB, opret test-org og test-agent
|
|
922
|
+
- I `afterAll`: `DELETE FROM hold_events; DELETE FROM holds; DELETE FROM agents; DELETE FROM orgs;`
|
|
923
|
+
- Brug `TEST_DATABASE_URL` (separat test-database, aldrig production-DB)
|