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.
Files changed (460) hide show
  1. package/.adal/skills/stripe-best-practices/SKILL.md +42 -0
  2. package/.adal/skills/stripe-best-practices/references/billing.md +36 -0
  3. package/.adal/skills/stripe-best-practices/references/connect.md +48 -0
  4. package/.adal/skills/stripe-best-practices/references/payments.md +79 -0
  5. package/.adal/skills/stripe-best-practices/references/security.md +109 -0
  6. package/.adal/skills/stripe-best-practices/references/treasury.md +16 -0
  7. package/.adal/skills/stripe-projects/SKILL.md +139 -0
  8. package/.adal/skills/upgrade-stripe/SKILL.md +185 -0
  9. package/.agents/skills/stripe-best-practices/SKILL.md +42 -0
  10. package/.agents/skills/stripe-best-practices/references/billing.md +36 -0
  11. package/.agents/skills/stripe-best-practices/references/connect.md +48 -0
  12. package/.agents/skills/stripe-best-practices/references/payments.md +79 -0
  13. package/.agents/skills/stripe-best-practices/references/security.md +109 -0
  14. package/.agents/skills/stripe-best-practices/references/treasury.md +16 -0
  15. package/.agents/skills/stripe-projects/SKILL.md +139 -0
  16. package/.agents/skills/upgrade-stripe/SKILL.md +185 -0
  17. package/.augment/skills/stripe-best-practices/SKILL.md +42 -0
  18. package/.augment/skills/stripe-best-practices/references/billing.md +36 -0
  19. package/.augment/skills/stripe-best-practices/references/connect.md +48 -0
  20. package/.augment/skills/stripe-best-practices/references/payments.md +79 -0
  21. package/.augment/skills/stripe-best-practices/references/security.md +109 -0
  22. package/.augment/skills/stripe-best-practices/references/treasury.md +16 -0
  23. package/.augment/skills/stripe-projects/SKILL.md +139 -0
  24. package/.augment/skills/upgrade-stripe/SKILL.md +185 -0
  25. package/.bob/skills/stripe-best-practices/SKILL.md +42 -0
  26. package/.bob/skills/stripe-best-practices/references/billing.md +36 -0
  27. package/.bob/skills/stripe-best-practices/references/connect.md +48 -0
  28. package/.bob/skills/stripe-best-practices/references/payments.md +79 -0
  29. package/.bob/skills/stripe-best-practices/references/security.md +109 -0
  30. package/.bob/skills/stripe-best-practices/references/treasury.md +16 -0
  31. package/.bob/skills/stripe-projects/SKILL.md +139 -0
  32. package/.bob/skills/upgrade-stripe/SKILL.md +185 -0
  33. package/.claude/settings.local.json +7 -0
  34. package/.claude/skills/stripe-best-practices/SKILL.md +42 -0
  35. package/.claude/skills/stripe-best-practices/references/billing.md +36 -0
  36. package/.claude/skills/stripe-best-practices/references/connect.md +48 -0
  37. package/.claude/skills/stripe-best-practices/references/payments.md +79 -0
  38. package/.claude/skills/stripe-best-practices/references/security.md +109 -0
  39. package/.claude/skills/stripe-best-practices/references/treasury.md +16 -0
  40. package/.claude/skills/stripe-projects/SKILL.md +139 -0
  41. package/.claude/skills/upgrade-stripe/SKILL.md +185 -0
  42. package/.codebuddy/skills/stripe-best-practices/SKILL.md +42 -0
  43. package/.codebuddy/skills/stripe-best-practices/references/billing.md +36 -0
  44. package/.codebuddy/skills/stripe-best-practices/references/connect.md +48 -0
  45. package/.codebuddy/skills/stripe-best-practices/references/payments.md +79 -0
  46. package/.codebuddy/skills/stripe-best-practices/references/security.md +109 -0
  47. package/.codebuddy/skills/stripe-best-practices/references/treasury.md +16 -0
  48. package/.codebuddy/skills/stripe-projects/SKILL.md +139 -0
  49. package/.codebuddy/skills/upgrade-stripe/SKILL.md +185 -0
  50. package/.commandcode/skills/stripe-best-practices/SKILL.md +42 -0
  51. package/.commandcode/skills/stripe-best-practices/references/billing.md +36 -0
  52. package/.commandcode/skills/stripe-best-practices/references/connect.md +48 -0
  53. package/.commandcode/skills/stripe-best-practices/references/payments.md +79 -0
  54. package/.commandcode/skills/stripe-best-practices/references/security.md +109 -0
  55. package/.commandcode/skills/stripe-best-practices/references/treasury.md +16 -0
  56. package/.commandcode/skills/stripe-projects/SKILL.md +139 -0
  57. package/.commandcode/skills/upgrade-stripe/SKILL.md +185 -0
  58. package/.continue/skills/stripe-best-practices/SKILL.md +42 -0
  59. package/.continue/skills/stripe-best-practices/references/billing.md +36 -0
  60. package/.continue/skills/stripe-best-practices/references/connect.md +48 -0
  61. package/.continue/skills/stripe-best-practices/references/payments.md +79 -0
  62. package/.continue/skills/stripe-best-practices/references/security.md +109 -0
  63. package/.continue/skills/stripe-best-practices/references/treasury.md +16 -0
  64. package/.continue/skills/stripe-projects/SKILL.md +139 -0
  65. package/.continue/skills/upgrade-stripe/SKILL.md +185 -0
  66. package/.cortex/skills/stripe-best-practices/SKILL.md +42 -0
  67. package/.cortex/skills/stripe-best-practices/references/billing.md +36 -0
  68. package/.cortex/skills/stripe-best-practices/references/connect.md +48 -0
  69. package/.cortex/skills/stripe-best-practices/references/payments.md +79 -0
  70. package/.cortex/skills/stripe-best-practices/references/security.md +109 -0
  71. package/.cortex/skills/stripe-best-practices/references/treasury.md +16 -0
  72. package/.cortex/skills/stripe-projects/SKILL.md +139 -0
  73. package/.cortex/skills/upgrade-stripe/SKILL.md +185 -0
  74. package/.crush/skills/stripe-best-practices/SKILL.md +42 -0
  75. package/.crush/skills/stripe-best-practices/references/billing.md +36 -0
  76. package/.crush/skills/stripe-best-practices/references/connect.md +48 -0
  77. package/.crush/skills/stripe-best-practices/references/payments.md +79 -0
  78. package/.crush/skills/stripe-best-practices/references/security.md +109 -0
  79. package/.crush/skills/stripe-best-practices/references/treasury.md +16 -0
  80. package/.crush/skills/stripe-projects/SKILL.md +139 -0
  81. package/.crush/skills/upgrade-stripe/SKILL.md +185 -0
  82. package/.env.example +20 -0
  83. package/.factory/skills/stripe-best-practices/SKILL.md +42 -0
  84. package/.factory/skills/stripe-best-practices/references/billing.md +36 -0
  85. package/.factory/skills/stripe-best-practices/references/connect.md +48 -0
  86. package/.factory/skills/stripe-best-practices/references/payments.md +79 -0
  87. package/.factory/skills/stripe-best-practices/references/security.md +109 -0
  88. package/.factory/skills/stripe-best-practices/references/treasury.md +16 -0
  89. package/.factory/skills/stripe-projects/SKILL.md +139 -0
  90. package/.factory/skills/upgrade-stripe/SKILL.md +185 -0
  91. package/.goose/skills/stripe-best-practices/SKILL.md +42 -0
  92. package/.goose/skills/stripe-best-practices/references/billing.md +36 -0
  93. package/.goose/skills/stripe-best-practices/references/connect.md +48 -0
  94. package/.goose/skills/stripe-best-practices/references/payments.md +79 -0
  95. package/.goose/skills/stripe-best-practices/references/security.md +109 -0
  96. package/.goose/skills/stripe-best-practices/references/treasury.md +16 -0
  97. package/.goose/skills/stripe-projects/SKILL.md +139 -0
  98. package/.goose/skills/upgrade-stripe/SKILL.md +185 -0
  99. package/.iflow/skills/stripe-best-practices/SKILL.md +42 -0
  100. package/.iflow/skills/stripe-best-practices/references/billing.md +36 -0
  101. package/.iflow/skills/stripe-best-practices/references/connect.md +48 -0
  102. package/.iflow/skills/stripe-best-practices/references/payments.md +79 -0
  103. package/.iflow/skills/stripe-best-practices/references/security.md +109 -0
  104. package/.iflow/skills/stripe-best-practices/references/treasury.md +16 -0
  105. package/.iflow/skills/stripe-projects/SKILL.md +139 -0
  106. package/.iflow/skills/upgrade-stripe/SKILL.md +185 -0
  107. package/.junie/skills/stripe-best-practices/SKILL.md +42 -0
  108. package/.junie/skills/stripe-best-practices/references/billing.md +36 -0
  109. package/.junie/skills/stripe-best-practices/references/connect.md +48 -0
  110. package/.junie/skills/stripe-best-practices/references/payments.md +79 -0
  111. package/.junie/skills/stripe-best-practices/references/security.md +109 -0
  112. package/.junie/skills/stripe-best-practices/references/treasury.md +16 -0
  113. package/.junie/skills/stripe-projects/SKILL.md +139 -0
  114. package/.junie/skills/upgrade-stripe/SKILL.md +185 -0
  115. package/.kilocode/skills/stripe-best-practices/SKILL.md +42 -0
  116. package/.kilocode/skills/stripe-best-practices/references/billing.md +36 -0
  117. package/.kilocode/skills/stripe-best-practices/references/connect.md +48 -0
  118. package/.kilocode/skills/stripe-best-practices/references/payments.md +79 -0
  119. package/.kilocode/skills/stripe-best-practices/references/security.md +109 -0
  120. package/.kilocode/skills/stripe-best-practices/references/treasury.md +16 -0
  121. package/.kilocode/skills/stripe-projects/SKILL.md +139 -0
  122. package/.kilocode/skills/upgrade-stripe/SKILL.md +185 -0
  123. package/.kiro/skills/stripe-best-practices/SKILL.md +42 -0
  124. package/.kiro/skills/stripe-best-practices/references/billing.md +36 -0
  125. package/.kiro/skills/stripe-best-practices/references/connect.md +48 -0
  126. package/.kiro/skills/stripe-best-practices/references/payments.md +79 -0
  127. package/.kiro/skills/stripe-best-practices/references/security.md +109 -0
  128. package/.kiro/skills/stripe-best-practices/references/treasury.md +16 -0
  129. package/.kiro/skills/stripe-projects/SKILL.md +139 -0
  130. package/.kiro/skills/upgrade-stripe/SKILL.md +185 -0
  131. package/.kode/skills/stripe-best-practices/SKILL.md +42 -0
  132. package/.kode/skills/stripe-best-practices/references/billing.md +36 -0
  133. package/.kode/skills/stripe-best-practices/references/connect.md +48 -0
  134. package/.kode/skills/stripe-best-practices/references/payments.md +79 -0
  135. package/.kode/skills/stripe-best-practices/references/security.md +109 -0
  136. package/.kode/skills/stripe-best-practices/references/treasury.md +16 -0
  137. package/.kode/skills/stripe-projects/SKILL.md +139 -0
  138. package/.kode/skills/upgrade-stripe/SKILL.md +185 -0
  139. package/.mcpjam/skills/stripe-best-practices/SKILL.md +42 -0
  140. package/.mcpjam/skills/stripe-best-practices/references/billing.md +36 -0
  141. package/.mcpjam/skills/stripe-best-practices/references/connect.md +48 -0
  142. package/.mcpjam/skills/stripe-best-practices/references/payments.md +79 -0
  143. package/.mcpjam/skills/stripe-best-practices/references/security.md +109 -0
  144. package/.mcpjam/skills/stripe-best-practices/references/treasury.md +16 -0
  145. package/.mcpjam/skills/stripe-projects/SKILL.md +139 -0
  146. package/.mcpjam/skills/upgrade-stripe/SKILL.md +185 -0
  147. package/.mux/skills/stripe-best-practices/SKILL.md +42 -0
  148. package/.mux/skills/stripe-best-practices/references/billing.md +36 -0
  149. package/.mux/skills/stripe-best-practices/references/connect.md +48 -0
  150. package/.mux/skills/stripe-best-practices/references/payments.md +79 -0
  151. package/.mux/skills/stripe-best-practices/references/security.md +109 -0
  152. package/.mux/skills/stripe-best-practices/references/treasury.md +16 -0
  153. package/.mux/skills/stripe-projects/SKILL.md +139 -0
  154. package/.mux/skills/upgrade-stripe/SKILL.md +185 -0
  155. package/.neovate/skills/stripe-best-practices/SKILL.md +42 -0
  156. package/.neovate/skills/stripe-best-practices/references/billing.md +36 -0
  157. package/.neovate/skills/stripe-best-practices/references/connect.md +48 -0
  158. package/.neovate/skills/stripe-best-practices/references/payments.md +79 -0
  159. package/.neovate/skills/stripe-best-practices/references/security.md +109 -0
  160. package/.neovate/skills/stripe-best-practices/references/treasury.md +16 -0
  161. package/.neovate/skills/stripe-projects/SKILL.md +139 -0
  162. package/.neovate/skills/upgrade-stripe/SKILL.md +185 -0
  163. package/.nixpacksignore +14 -0
  164. package/.openhands/skills/stripe-best-practices/SKILL.md +42 -0
  165. package/.openhands/skills/stripe-best-practices/references/billing.md +36 -0
  166. package/.openhands/skills/stripe-best-practices/references/connect.md +48 -0
  167. package/.openhands/skills/stripe-best-practices/references/payments.md +79 -0
  168. package/.openhands/skills/stripe-best-practices/references/security.md +109 -0
  169. package/.openhands/skills/stripe-best-practices/references/treasury.md +16 -0
  170. package/.openhands/skills/stripe-projects/SKILL.md +139 -0
  171. package/.openhands/skills/upgrade-stripe/SKILL.md +185 -0
  172. package/.pi/skills/stripe-best-practices/SKILL.md +42 -0
  173. package/.pi/skills/stripe-best-practices/references/billing.md +36 -0
  174. package/.pi/skills/stripe-best-practices/references/connect.md +48 -0
  175. package/.pi/skills/stripe-best-practices/references/payments.md +79 -0
  176. package/.pi/skills/stripe-best-practices/references/security.md +109 -0
  177. package/.pi/skills/stripe-best-practices/references/treasury.md +16 -0
  178. package/.pi/skills/stripe-projects/SKILL.md +139 -0
  179. package/.pi/skills/upgrade-stripe/SKILL.md +185 -0
  180. package/.pochi/skills/stripe-best-practices/SKILL.md +42 -0
  181. package/.pochi/skills/stripe-best-practices/references/billing.md +36 -0
  182. package/.pochi/skills/stripe-best-practices/references/connect.md +48 -0
  183. package/.pochi/skills/stripe-best-practices/references/payments.md +79 -0
  184. package/.pochi/skills/stripe-best-practices/references/security.md +109 -0
  185. package/.pochi/skills/stripe-best-practices/references/treasury.md +16 -0
  186. package/.pochi/skills/stripe-projects/SKILL.md +139 -0
  187. package/.pochi/skills/upgrade-stripe/SKILL.md +185 -0
  188. package/.qoder/skills/stripe-best-practices/SKILL.md +42 -0
  189. package/.qoder/skills/stripe-best-practices/references/billing.md +36 -0
  190. package/.qoder/skills/stripe-best-practices/references/connect.md +48 -0
  191. package/.qoder/skills/stripe-best-practices/references/payments.md +79 -0
  192. package/.qoder/skills/stripe-best-practices/references/security.md +109 -0
  193. package/.qoder/skills/stripe-best-practices/references/treasury.md +16 -0
  194. package/.qoder/skills/stripe-projects/SKILL.md +139 -0
  195. package/.qoder/skills/upgrade-stripe/SKILL.md +185 -0
  196. package/.qwen/skills/stripe-best-practices/SKILL.md +42 -0
  197. package/.qwen/skills/stripe-best-practices/references/billing.md +36 -0
  198. package/.qwen/skills/stripe-best-practices/references/connect.md +48 -0
  199. package/.qwen/skills/stripe-best-practices/references/payments.md +79 -0
  200. package/.qwen/skills/stripe-best-practices/references/security.md +109 -0
  201. package/.qwen/skills/stripe-best-practices/references/treasury.md +16 -0
  202. package/.qwen/skills/stripe-projects/SKILL.md +139 -0
  203. package/.qwen/skills/upgrade-stripe/SKILL.md +185 -0
  204. package/.roo/skills/stripe-best-practices/SKILL.md +42 -0
  205. package/.roo/skills/stripe-best-practices/references/billing.md +36 -0
  206. package/.roo/skills/stripe-best-practices/references/connect.md +48 -0
  207. package/.roo/skills/stripe-best-practices/references/payments.md +79 -0
  208. package/.roo/skills/stripe-best-practices/references/security.md +109 -0
  209. package/.roo/skills/stripe-best-practices/references/treasury.md +16 -0
  210. package/.roo/skills/stripe-projects/SKILL.md +139 -0
  211. package/.roo/skills/upgrade-stripe/SKILL.md +185 -0
  212. package/.trae/skills/stripe-best-practices/SKILL.md +42 -0
  213. package/.trae/skills/stripe-best-practices/references/billing.md +36 -0
  214. package/.trae/skills/stripe-best-practices/references/connect.md +48 -0
  215. package/.trae/skills/stripe-best-practices/references/payments.md +79 -0
  216. package/.trae/skills/stripe-best-practices/references/security.md +109 -0
  217. package/.trae/skills/stripe-best-practices/references/treasury.md +16 -0
  218. package/.trae/skills/stripe-projects/SKILL.md +139 -0
  219. package/.trae/skills/upgrade-stripe/SKILL.md +185 -0
  220. package/.vibe/skills/stripe-best-practices/SKILL.md +42 -0
  221. package/.vibe/skills/stripe-best-practices/references/billing.md +36 -0
  222. package/.vibe/skills/stripe-best-practices/references/connect.md +48 -0
  223. package/.vibe/skills/stripe-best-practices/references/payments.md +79 -0
  224. package/.vibe/skills/stripe-best-practices/references/security.md +109 -0
  225. package/.vibe/skills/stripe-best-practices/references/treasury.md +16 -0
  226. package/.vibe/skills/stripe-projects/SKILL.md +139 -0
  227. package/.vibe/skills/upgrade-stripe/SKILL.md +185 -0
  228. package/.windsurf/skills/stripe-best-practices/SKILL.md +42 -0
  229. package/.windsurf/skills/stripe-best-practices/references/billing.md +36 -0
  230. package/.windsurf/skills/stripe-best-practices/references/connect.md +48 -0
  231. package/.windsurf/skills/stripe-best-practices/references/payments.md +79 -0
  232. package/.windsurf/skills/stripe-best-practices/references/security.md +109 -0
  233. package/.windsurf/skills/stripe-best-practices/references/treasury.md +16 -0
  234. package/.windsurf/skills/stripe-projects/SKILL.md +139 -0
  235. package/.windsurf/skills/upgrade-stripe/SKILL.md +185 -0
  236. package/.zencoder/skills/stripe-best-practices/SKILL.md +42 -0
  237. package/.zencoder/skills/stripe-best-practices/references/billing.md +36 -0
  238. package/.zencoder/skills/stripe-best-practices/references/connect.md +48 -0
  239. package/.zencoder/skills/stripe-best-practices/references/payments.md +79 -0
  240. package/.zencoder/skills/stripe-best-practices/references/security.md +109 -0
  241. package/.zencoder/skills/stripe-best-practices/references/treasury.md +16 -0
  242. package/.zencoder/skills/stripe-projects/SKILL.md +139 -0
  243. package/.zencoder/skills/upgrade-stripe/SKILL.md +185 -0
  244. package/AUDIT.md +95 -0
  245. package/BLOCKERS.md +0 -0
  246. package/COOLIFY.md +51 -0
  247. package/MCP_SETUP.md +23 -0
  248. package/PRODUCTION_CHECKLIST.md +246 -0
  249. package/README.md +47 -0
  250. package/ROADMAP.md +91 -0
  251. package/docs/superpowers/plans/2026-05-11-availsync-frontend-sales-flow.md +2445 -0
  252. package/frontend/.env.example +2 -0
  253. package/frontend/app/admin/layout.tsx +13 -0
  254. package/frontend/app/admin/page.tsx +747 -0
  255. package/frontend/app/app/activity/page.tsx +257 -0
  256. package/frontend/app/app/agents/[agentId]/page.tsx +21 -0
  257. package/frontend/app/app/agents/page.tsx +1155 -0
  258. package/frontend/app/app/audit/page.tsx +225 -0
  259. package/frontend/app/app/availability/page.tsx +840 -0
  260. package/frontend/app/app/holds/page.tsx +262 -0
  261. package/frontend/app/app/layout.tsx +19 -0
  262. package/frontend/app/app/onboarding/page.tsx +10 -0
  263. package/frontend/app/app/onboarding/verify/page.tsx +309 -0
  264. package/frontend/app/app/page.tsx +508 -0
  265. package/frontend/app/app/settings/page.tsx +399 -0
  266. package/frontend/app/app/work/page.tsx +426 -0
  267. package/frontend/app/changelog/page.tsx +93 -0
  268. package/frontend/app/checkout/page.tsx +25 -0
  269. package/frontend/app/docs/api/page.tsx +157 -0
  270. package/frontend/app/docs/page.tsx +296 -0
  271. package/frontend/app/docs/pilot/page.tsx +127 -0
  272. package/frontend/app/docs/quickstart/page.tsx +318 -0
  273. package/frontend/app/docs/reliability/page.tsx +78 -0
  274. package/frontend/app/docs/sdk/node/page.tsx +166 -0
  275. package/frontend/app/globals.css +57 -0
  276. package/frontend/app/icon.png +0 -0
  277. package/frontend/app/layout.tsx +87 -0
  278. package/frontend/app/login/page.tsx +14 -0
  279. package/frontend/app/page.tsx +47 -0
  280. package/frontend/app/pricing/page.tsx +66 -0
  281. package/frontend/app/privacy/page.tsx +52 -0
  282. package/frontend/app/robots.ts +26 -0
  283. package/frontend/app/security/page.tsx +74 -0
  284. package/frontend/app/signup/page.tsx +14 -0
  285. package/frontend/app/sitemap.ts +14 -0
  286. package/frontend/app/terms/page.tsx +51 -0
  287. package/frontend/components/brand/AvailsyncLogo.tsx +56 -0
  288. package/frontend/components/checkout/CheckoutClient.tsx +100 -0
  289. package/frontend/components/dashboard/AgentForm.tsx +59 -0
  290. package/frontend/components/dashboard/AppShell.tsx +291 -0
  291. package/frontend/components/dashboard/AvailabilityChecker.tsx +117 -0
  292. package/frontend/components/dashboard/AvailabilityWindowForm.tsx +40 -0
  293. package/frontend/components/dashboard/HoldForm.tsx +133 -0
  294. package/frontend/components/dashboard/MetricCard.tsx +10 -0
  295. package/frontend/components/login/LoginForm.tsx +95 -0
  296. package/frontend/components/marketing/AgentCoordinationStory.tsx +1530 -0
  297. package/frontend/components/marketing/Faq.tsx +41 -0
  298. package/frontend/components/marketing/Hero.tsx +73 -0
  299. package/frontend/components/marketing/HowItWorks.tsx +28 -0
  300. package/frontend/components/marketing/ObserveModeTeaser.tsx +41 -0
  301. package/frontend/components/marketing/PricingTeaser.tsx +23 -0
  302. package/frontend/components/marketing/ProblemSolution.tsx +36 -0
  303. package/frontend/components/marketing/SiteFooter.tsx +59 -0
  304. package/frontend/components/marketing/SiteHeader.tsx +45 -0
  305. package/frontend/components/marketing/UseCases.tsx +27 -0
  306. package/frontend/components/onboarding/OnboardingClient.tsx +278 -0
  307. package/frontend/components/pricing/PricingCards.tsx +65 -0
  308. package/frontend/components/privacy/CookieConsent.tsx +230 -0
  309. package/frontend/components/privacy/CookieSettingsButton.tsx +15 -0
  310. package/frontend/components/seo/JsonLd.tsx +10 -0
  311. package/frontend/components/signup/SignupForm.tsx +55 -0
  312. package/frontend/components/ui/Badge.tsx +23 -0
  313. package/frontend/components/ui/Button.tsx +37 -0
  314. package/frontend/components/ui/Card.tsx +11 -0
  315. package/frontend/components/ui/ConfirmDialog.tsx +77 -0
  316. package/frontend/components/ui/EmptyState.tsx +24 -0
  317. package/frontend/components/ui/Input.tsx +14 -0
  318. package/frontend/components/ui/KeyDisplay.tsx +49 -0
  319. package/frontend/components/ui/Select.tsx +14 -0
  320. package/frontend/components/ui/Skeleton.tsx +24 -0
  321. package/frontend/components/ui/Tabs.tsx +19 -0
  322. package/frontend/components/ui/Textarea.tsx +14 -0
  323. package/frontend/components/ui/Toast.tsx +78 -0
  324. package/frontend/components/waitlist/WaitlistDialog.tsx +128 -0
  325. package/frontend/lib/api.ts +1282 -0
  326. package/frontend/lib/billing.ts +6 -0
  327. package/frontend/lib/cookieConsent.ts +113 -0
  328. package/frontend/lib/format.ts +16 -0
  329. package/frontend/lib/plans.ts +62 -0
  330. package/frontend/lib/schemas.ts +108 -0
  331. package/frontend/lib/seo.ts +376 -0
  332. package/frontend/lib/setupGuides.ts +630 -0
  333. package/frontend/lib/storage.ts +30 -0
  334. package/frontend/next-env.d.ts +6 -0
  335. package/frontend/next.config.mjs +13 -0
  336. package/frontend/package-lock.json +14409 -0
  337. package/frontend/package.json +41 -0
  338. package/frontend/playwright.config.ts +20 -0
  339. package/frontend/postcss.config.mjs +8 -0
  340. package/frontend/public/.gitkeep +0 -0
  341. package/frontend/public/brand/availsync-logo-board.png +0 -0
  342. package/frontend/public/brand/availsync-logo-dark.png +0 -0
  343. package/frontend/public/brand/availsync-mark-dark.png +0 -0
  344. package/frontend/public/brand/availsync-wordmark-dark.png +0 -0
  345. package/frontend/public/marketing/hero-agent-coordination.png +0 -0
  346. package/frontend/tailwind.config.ts +53 -0
  347. package/frontend/tests/smoke.spec.ts +89 -0
  348. package/frontend/tsconfig.json +23 -0
  349. package/jest.config.js +7 -0
  350. package/nixpacks.toml +11 -0
  351. package/package.json +53 -0
  352. package/packages/mcp/LICENSE +21 -0
  353. package/packages/mcp/README.md +60 -0
  354. package/packages/mcp/jest.config.cjs +8 -0
  355. package/packages/mcp/package.json +54 -0
  356. package/packages/mcp/src/helpers.ts +38 -0
  357. package/packages/mcp/src/index.test.ts +60 -0
  358. package/packages/mcp/src/index.ts +387 -0
  359. package/packages/mcp/tsconfig.json +20 -0
  360. package/packages/mcp/tsconfig.test.json +12 -0
  361. package/packages/node/LICENSE +21 -0
  362. package/packages/node/README.md +120 -0
  363. package/packages/node/jest.config.cjs +8 -0
  364. package/packages/node/package.json +46 -0
  365. package/packages/node/src/index.test.ts +360 -0
  366. package/packages/node/src/index.ts +402 -0
  367. package/packages/node/tsconfig.json +20 -0
  368. package/packages/node/tsconfig.test.json +12 -0
  369. package/plan.md +923 -0
  370. package/skills/stripe-best-practices/SKILL.md +42 -0
  371. package/skills/stripe-best-practices/references/billing.md +36 -0
  372. package/skills/stripe-best-practices/references/connect.md +48 -0
  373. package/skills/stripe-best-practices/references/payments.md +79 -0
  374. package/skills/stripe-best-practices/references/security.md +109 -0
  375. package/skills/stripe-best-practices/references/treasury.md +16 -0
  376. package/skills/stripe-projects/SKILL.md +139 -0
  377. package/skills/upgrade-stripe/SKILL.md +185 -0
  378. package/skills-lock.json +20 -0
  379. package/src/core/availability.ts +178 -0
  380. package/src/core/conflict.ts +209 -0
  381. package/src/core/work.ts +490 -0
  382. package/src/db/client.ts +17 -0
  383. package/src/db/migrations/001_init.sql +88 -0
  384. package/src/db/migrations/002_stripe.sql +2 -0
  385. package/src/db/migrations/003_workspace_auth.sql +19 -0
  386. package/src/db/migrations/004_agent_mcp_status.sql +2 -0
  387. package/src/db/migrations/005_hold_event_actor.sql +4 -0
  388. package/src/db/migrations/006_agent_activity.sql +35 -0
  389. package/src/db/migrations/007_work_coordination.sql +60 -0
  390. package/src/db/migrations/008_work_claim_leases.sql +20 -0
  391. package/src/db/migrations/009_billing_subscription_state.sql +23 -0
  392. package/src/db/migrations/010_agent_api_key_prefix.sql +10 -0
  393. package/src/db/migrations/011_org_verified_and_work_event_retention.sql +11 -0
  394. package/src/db/migrations/012_agent_enforcement_mode.sql +12 -0
  395. package/src/db/migrations/013_support_tickets.sql +21 -0
  396. package/src/db/migrations/014_paid_plan_waitlist.sql +23 -0
  397. package/src/db/migrations/015_agent_last_seen.sql +2 -0
  398. package/src/db/migrations.ts +164 -0
  399. package/src/db/run-migrations.ts +13 -0
  400. package/src/index.ts +183 -0
  401. package/src/lib/activity.ts +137 -0
  402. package/src/lib/apiKeys.ts +32 -0
  403. package/src/lib/appInfo.ts +26 -0
  404. package/src/lib/billingConfig.ts +3 -0
  405. package/src/lib/env.ts +75 -0
  406. package/src/lib/logger.ts +8 -0
  407. package/src/lib/plans.ts +204 -0
  408. package/src/mcp/server.js +5 -0
  409. package/src/mcp/server.ts +350 -0
  410. package/src/middleware/auth.ts +342 -0
  411. package/src/middleware/requestId.ts +16 -0
  412. package/src/routes/account.ts +168 -0
  413. package/src/routes/activity.ts +126 -0
  414. package/src/routes/admin.ts +514 -0
  415. package/src/routes/audit.ts +68 -0
  416. package/src/routes/auth.ts +203 -0
  417. package/src/routes/availability.ts +325 -0
  418. package/src/routes/billing.ts +406 -0
  419. package/src/routes/conflicts.ts +131 -0
  420. package/src/routes/holds.ts +437 -0
  421. package/src/routes/mcp.ts +57 -0
  422. package/src/routes/metrics.ts +39 -0
  423. package/src/routes/onboarding.ts +273 -0
  424. package/src/routes/orgs.ts +981 -0
  425. package/src/routes/preferences.ts +132 -0
  426. package/src/routes/session.ts +16 -0
  427. package/src/routes/support.ts +77 -0
  428. package/src/routes/value.ts +186 -0
  429. package/src/routes/waitlist.ts +63 -0
  430. package/src/routes/work.ts +1578 -0
  431. package/src/server.ts +36 -0
  432. package/src/types/index.ts +109 -0
  433. package/tests/integration/activity.route.test.ts +103 -0
  434. package/tests/integration/admin.route.test.ts +143 -0
  435. package/tests/integration/agent-keys.route.test.ts +237 -0
  436. package/tests/integration/availability.route.test.ts +125 -0
  437. package/tests/integration/billing.route.test.ts +393 -0
  438. package/tests/integration/conflicts.route.test.ts +131 -0
  439. package/tests/integration/flows.test.ts +154 -0
  440. package/tests/integration/helpers.ts +134 -0
  441. package/tests/integration/holds.route.test.ts +185 -0
  442. package/tests/integration/metrics.route.test.ts +100 -0
  443. package/tests/integration/onboarding.verify.route.test.ts +163 -0
  444. package/tests/integration/preferences.route.test.ts +53 -0
  445. package/tests/integration/session.route.test.ts +97 -0
  446. package/tests/integration/system.route.test.ts +92 -0
  447. package/tests/integration/value.route.test.ts +235 -0
  448. package/tests/integration/work.route.test.ts +745 -0
  449. package/tests/setup.ts +4 -0
  450. package/tests/smoke.sh +62 -0
  451. package/tests/unit/auth.test.ts +114 -0
  452. package/tests/unit/availability.test.ts +149 -0
  453. package/tests/unit/conflict.test.ts +118 -0
  454. package/tests/unit/env.test.ts +69 -0
  455. package/tests/unit/migrations.test.ts +135 -0
  456. package/tests/unit/request-id.test.ts +37 -0
  457. package/tmp-mobile-agents.png +0 -0
  458. package/tmp-next-mobile.err.log +10 -0
  459. package/tmp-next-mobile.log +5 -0
  460. package/tsconfig.json +16 -0
@@ -0,0 +1,2445 @@
1
+ # Availsync Frontend Sales Flow Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Build a complete frontend that can explain Availsync, sell the product, onboard a buyer, create their first org/agent, show the API key, and let them verify the backend works through the UI.
6
+
7
+ **Architecture:** Add a Next.js frontend in `frontend/` that talks to the existing Express API at `http://localhost:3000`. Keep the first version purchase-ready but pragmatic: Stripe checkout can be wired through a small backend billing extension, while the core no-payment path lets a buyer create an org, create an agent, configure availability, check slots, and book/release holds.
8
+
9
+ **Tech Stack:** Next.js App Router, TypeScript, Tailwind CSS, shadcn-style UI primitives, React Hook Form, Zod, TanStack Query, Playwright, Stripe Checkout for paid plans, existing Availsync REST API.
10
+
11
+ ---
12
+
13
+ ## Product Summary
14
+
15
+ Availsync is the scheduling source of truth for AI agents.
16
+
17
+ The frontend must make this immediately clear:
18
+
19
+ - AI agents can create double-bookings when sales bots, recruiting bots, and calendar assistants act independently.
20
+ - Availsync gives all agents one shared rules and holds layer before anything is booked.
21
+ - The buyer gets a REST API, agent API keys, conflict resolution, availability windows, preferences, holds, audit events, Docker local deployment, and optional MCP access.
22
+
23
+ Primary homepage line:
24
+
25
+ ```text
26
+ One scheduling source of truth for AI agents
27
+ ```
28
+
29
+ Primary subheadline:
30
+
31
+ ```text
32
+ Prevent sales bots, recruiting agents, and calendar automations from double-booking the same time slot.
33
+ ```
34
+
35
+ Primary CTA:
36
+
37
+ ```text
38
+ Start free
39
+ ```
40
+
41
+ Secondary CTA:
42
+
43
+ ```text
44
+ View API docs
45
+ ```
46
+
47
+ ---
48
+
49
+ ## Required Buyer Journey
50
+
51
+ The frontend is complete only when a new buyer can do this:
52
+
53
+ 1. Visit the homepage and understand what Availsync sells.
54
+ 2. Compare plans on pricing.
55
+ 3. Start free or choose a paid plan.
56
+ 4. Create an organization.
57
+ 5. Create their first AI scheduling agent.
58
+ 6. See and copy the generated API key once.
59
+ 7. Create an availability window.
60
+ 8. Check availability from the UI.
61
+ 9. Book a hold from the UI.
62
+ 10. Release the hold from the UI.
63
+ 11. Read a quickstart doc that shows how to use the same API from curl.
64
+
65
+ ---
66
+
67
+ ## File Structure
68
+
69
+ Create these files:
70
+
71
+ ```text
72
+ frontend/
73
+ package.json
74
+ next.config.mjs
75
+ tsconfig.json
76
+ tailwind.config.ts
77
+ postcss.config.mjs
78
+ playwright.config.ts
79
+ .env.example
80
+ app/
81
+ globals.css
82
+ layout.tsx
83
+ page.tsx
84
+ pricing/page.tsx
85
+ signup/page.tsx
86
+ checkout/page.tsx
87
+ docs/page.tsx
88
+ docs/quickstart/page.tsx
89
+ app/layout.tsx
90
+ app/page.tsx
91
+ app/onboarding/page.tsx
92
+ app/agents/page.tsx
93
+ app/agents/[agentId]/page.tsx
94
+ app/availability/page.tsx
95
+ app/holds/page.tsx
96
+ components/
97
+ marketing/Hero.tsx
98
+ marketing/ProblemSolution.tsx
99
+ marketing/HowItWorks.tsx
100
+ marketing/UseCases.tsx
101
+ marketing/PricingTeaser.tsx
102
+ marketing/Faq.tsx
103
+ marketing/SiteHeader.tsx
104
+ marketing/SiteFooter.tsx
105
+ pricing/PricingCards.tsx
106
+ dashboard/AppShell.tsx
107
+ dashboard/MetricCard.tsx
108
+ dashboard/AgentForm.tsx
109
+ dashboard/AvailabilityWindowForm.tsx
110
+ dashboard/AvailabilityChecker.tsx
111
+ dashboard/HoldForm.tsx
112
+ ui/Button.tsx
113
+ ui/Card.tsx
114
+ ui/Input.tsx
115
+ ui/Select.tsx
116
+ ui/Textarea.tsx
117
+ ui/Tabs.tsx
118
+ lib/
119
+ api.ts
120
+ schemas.ts
121
+ storage.ts
122
+ format.ts
123
+ plans.ts
124
+ tests/
125
+ smoke.spec.ts
126
+ ```
127
+
128
+ Modify these backend files:
129
+
130
+ ```text
131
+ src/index.ts
132
+ src/routes/billing.ts
133
+ package.json
134
+ .env.example
135
+ ```
136
+
137
+ Do not modify:
138
+
139
+ ```text
140
+ plan.md
141
+ ```
142
+
143
+ ---
144
+
145
+ ## API Contract To Use
146
+
147
+ The frontend must call the existing API:
148
+
149
+ ```text
150
+ GET /health
151
+ POST /v1/orgs
152
+ POST /v1/orgs/:org_id/agents
153
+ GET /v1/orgs/:org_id/agents
154
+ POST /v1/availability-windows
155
+ GET /v1/availability-windows
156
+ GET /v1/availability
157
+ POST /v1/holds
158
+ GET /v1/holds
159
+ DELETE /v1/holds/:hold_id
160
+ GET /v1/preferences/:agent_id
161
+ PUT /v1/preferences/:agent_id
162
+ ```
163
+
164
+ For paid purchase, add these backend endpoints:
165
+
166
+ ```text
167
+ POST /v1/billing/checkout
168
+ POST /v1/billing/webhook
169
+ GET /v1/billing/subscription/:org_id
170
+ ```
171
+
172
+ Initial purchase implementation can use Stripe test mode. If Stripe env vars are absent, the checkout endpoint must return `501 billing_not_configured` with a clear message, while the free plan remains usable.
173
+
174
+ ---
175
+
176
+ ## Task 1: Create Frontend Project Shell
177
+
178
+ **Files:**
179
+ - Create: `frontend/package.json`
180
+ - Create: `frontend/next.config.mjs`
181
+ - Create: `frontend/tsconfig.json`
182
+ - Create: `frontend/tailwind.config.ts`
183
+ - Create: `frontend/postcss.config.mjs`
184
+ - Create: `frontend/.env.example`
185
+ - Create: `frontend/app/globals.css`
186
+ - Create: `frontend/app/layout.tsx`
187
+
188
+ - [ ] **Step 1: Create `frontend/package.json`**
189
+
190
+ ```json
191
+ {
192
+ "name": "availsync-frontend",
193
+ "version": "0.1.0",
194
+ "private": true,
195
+ "scripts": {
196
+ "dev": "next dev -p 3001",
197
+ "build": "next build",
198
+ "start": "next start -p 3001",
199
+ "lint": "next lint",
200
+ "test:e2e": "playwright test"
201
+ },
202
+ "dependencies": {
203
+ "@tanstack/react-query": "^5.90.0",
204
+ "clsx": "^2.1.1",
205
+ "lucide-react": "^0.468.0",
206
+ "next": "^15.0.0",
207
+ "react": "^19.0.0",
208
+ "react-dom": "^19.0.0",
209
+ "react-hook-form": "^7.53.0",
210
+ "tailwind-merge": "^2.5.4",
211
+ "zod": "^4.4.3"
212
+ },
213
+ "devDependencies": {
214
+ "@playwright/test": "^1.49.0",
215
+ "@types/node": "^22.0.0",
216
+ "@types/react": "^19.0.0",
217
+ "@types/react-dom": "^19.0.0",
218
+ "autoprefixer": "^10.4.20",
219
+ "eslint": "^9.0.0",
220
+ "eslint-config-next": "^15.0.0",
221
+ "postcss": "^8.4.49",
222
+ "tailwindcss": "^3.4.17",
223
+ "typescript": "^5.9.3"
224
+ }
225
+ }
226
+ ```
227
+
228
+ - [ ] **Step 2: Create config files**
229
+
230
+ `frontend/next.config.mjs`:
231
+
232
+ ```js
233
+ /** @type {import('next').NextConfig} */
234
+ const nextConfig = {
235
+ output: 'standalone',
236
+ };
237
+
238
+ export default nextConfig;
239
+ ```
240
+
241
+ `frontend/tsconfig.json`:
242
+
243
+ ```json
244
+ {
245
+ "compilerOptions": {
246
+ "target": "ES2022",
247
+ "lib": ["dom", "dom.iterable", "ES2022"],
248
+ "allowJs": false,
249
+ "skipLibCheck": true,
250
+ "strict": true,
251
+ "noEmit": true,
252
+ "esModuleInterop": true,
253
+ "module": "esnext",
254
+ "moduleResolution": "bundler",
255
+ "resolveJsonModule": true,
256
+ "isolatedModules": true,
257
+ "jsx": "preserve",
258
+ "incremental": true,
259
+ "plugins": [{ "name": "next" }],
260
+ "paths": {
261
+ "@/*": ["./*"]
262
+ }
263
+ },
264
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
265
+ "exclude": ["node_modules"]
266
+ }
267
+ ```
268
+
269
+ `frontend/tailwind.config.ts`:
270
+
271
+ ```ts
272
+ import type { Config } from 'tailwindcss';
273
+
274
+ const config: Config = {
275
+ content: ['./app/**/*.{ts,tsx}', './components/**/*.{ts,tsx}', './lib/**/*.{ts,tsx}'],
276
+ theme: {
277
+ extend: {
278
+ colors: {
279
+ ink: '#101418',
280
+ paper: '#f7f5ef',
281
+ line: '#d8d2c4',
282
+ moss: '#556b4e',
283
+ rust: '#b75c2b',
284
+ blue: '#315f8a',
285
+ },
286
+ boxShadow: {
287
+ panel: '0 16px 48px rgba(16, 20, 24, 0.10)',
288
+ },
289
+ },
290
+ },
291
+ plugins: [],
292
+ };
293
+
294
+ export default config;
295
+ ```
296
+
297
+ `frontend/postcss.config.mjs`:
298
+
299
+ ```js
300
+ const config = {
301
+ plugins: {
302
+ tailwindcss: {},
303
+ autoprefixer: {},
304
+ },
305
+ };
306
+
307
+ export default config;
308
+ ```
309
+
310
+ `frontend/.env.example`:
311
+
312
+ ```text
313
+ NEXT_PUBLIC_API_URL=http://localhost:3000
314
+ NEXT_PUBLIC_APP_URL=http://localhost:3001
315
+ ```
316
+
317
+ - [ ] **Step 3: Create global layout**
318
+
319
+ `frontend/app/globals.css`:
320
+
321
+ ```css
322
+ @tailwind base;
323
+ @tailwind components;
324
+ @tailwind utilities;
325
+
326
+ :root {
327
+ color-scheme: light;
328
+ }
329
+
330
+ * {
331
+ box-sizing: border-box;
332
+ }
333
+
334
+ body {
335
+ margin: 0;
336
+ background: #f7f5ef;
337
+ color: #101418;
338
+ }
339
+
340
+ a {
341
+ color: inherit;
342
+ text-decoration: none;
343
+ }
344
+
345
+ button,
346
+ input,
347
+ select,
348
+ textarea {
349
+ font: inherit;
350
+ }
351
+ ```
352
+
353
+ `frontend/app/layout.tsx`:
354
+
355
+ ```tsx
356
+ import type { Metadata } from 'next';
357
+ import './globals.css';
358
+
359
+ export const metadata: Metadata = {
360
+ title: 'Availsync - Scheduling source of truth for AI agents',
361
+ description:
362
+ 'Prevent sales bots, recruiting agents, and calendar automations from double-booking the same time slot.',
363
+ };
364
+
365
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
366
+ return (
367
+ <html lang="en">
368
+ <body>{children}</body>
369
+ </html>
370
+ );
371
+ }
372
+ ```
373
+
374
+ - [ ] **Step 4: Install and verify**
375
+
376
+ Run:
377
+
378
+ ```bash
379
+ cd frontend
380
+ npm install
381
+ npm run build
382
+ ```
383
+
384
+ Expected:
385
+
386
+ ```text
387
+ Compiled successfully
388
+ ```
389
+
390
+ ---
391
+
392
+ ## Task 2: Shared API Client And Local Session Storage
393
+
394
+ **Files:**
395
+ - Create: `frontend/lib/schemas.ts`
396
+ - Create: `frontend/lib/api.ts`
397
+ - Create: `frontend/lib/storage.ts`
398
+ - Create: `frontend/lib/format.ts`
399
+
400
+ - [ ] **Step 1: Create schemas**
401
+
402
+ `frontend/lib/schemas.ts`:
403
+
404
+ ```ts
405
+ import { z } from 'zod';
406
+
407
+ export const agentTypeSchema = z.enum(['external_meeting', 'internal', 'focus', 'generic']);
408
+
409
+ export const orgSchema = z.object({
410
+ id: z.string().uuid(),
411
+ name: z.string(),
412
+ plan: z.enum(['free', 'individual', 'team']),
413
+ agent_limit: z.number(),
414
+ created_at: z.string(),
415
+ });
416
+
417
+ export const agentSchema = z.object({
418
+ id: z.string().uuid(),
419
+ org_id: z.string().uuid(),
420
+ name: z.string(),
421
+ agent_type: agentTypeSchema,
422
+ priority: z.number(),
423
+ created_at: z.string(),
424
+ });
425
+
426
+ export const holdSchema = z.object({
427
+ id: z.string().uuid(),
428
+ agent_id: z.string().uuid(),
429
+ org_id: z.string().uuid(),
430
+ start_at: z.string(),
431
+ end_at: z.string(),
432
+ reason: z.string().nullable().optional(),
433
+ status: z.enum(['confirmed', 'superseded', 'released']),
434
+ booked_by_agent_id: z.string().uuid().nullable().optional(),
435
+ metadata: z.record(z.string(), z.unknown()).nullable().optional(),
436
+ created_at: z.string(),
437
+ });
438
+
439
+ export const slotSchema = z.object({
440
+ start: z.string(),
441
+ end: z.string(),
442
+ confidence: z.number(),
443
+ competing_holds_count: z.number(),
444
+ });
445
+
446
+ export const availabilityWindowSchema = z.object({
447
+ id: z.string().uuid(),
448
+ agent_id: z.string().uuid(),
449
+ start_at: z.string(),
450
+ end_at: z.string(),
451
+ window_type: z.enum(['available', 'focus', 'blocked']),
452
+ recurrence: z.string().nullable().optional(),
453
+ });
454
+
455
+ export type AgentType = z.infer<typeof agentTypeSchema>;
456
+ export type Org = z.infer<typeof orgSchema>;
457
+ export type Agent = z.infer<typeof agentSchema>;
458
+ export type Hold = z.infer<typeof holdSchema>;
459
+ export type Slot = z.infer<typeof slotSchema>;
460
+ export type AvailabilityWindow = z.infer<typeof availabilityWindowSchema>;
461
+ ```
462
+
463
+ - [ ] **Step 2: Create API client**
464
+
465
+ `frontend/lib/api.ts`:
466
+
467
+ ```ts
468
+ import {
469
+ agentSchema,
470
+ availabilityWindowSchema,
471
+ holdSchema,
472
+ orgSchema,
473
+ slotSchema,
474
+ type AgentType,
475
+ } from './schemas';
476
+
477
+ const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
478
+
479
+ async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
480
+ const response = await fetch(`${API_URL}${path}`, {
481
+ ...options,
482
+ headers: {
483
+ 'Content-Type': 'application/json',
484
+ ...(options.headers || {}),
485
+ },
486
+ cache: 'no-store',
487
+ });
488
+ const body = await response.json().catch(() => ({}));
489
+ if (!response.ok) {
490
+ throw new Error(body.message || body.error || `Request failed with ${response.status}`);
491
+ }
492
+ return body as T;
493
+ }
494
+
495
+ export async function createOrg(name: string) {
496
+ const response = await request<{ org: unknown }>('/v1/orgs', {
497
+ method: 'POST',
498
+ body: JSON.stringify({ name }),
499
+ });
500
+ return orgSchema.parse(response.org);
501
+ }
502
+
503
+ export async function createAgent(input: {
504
+ orgId: string;
505
+ name: string;
506
+ agent_type: AgentType;
507
+ priority: number;
508
+ }) {
509
+ const response = await request<{ agent: unknown; api_key: string }>(
510
+ `/v1/orgs/${input.orgId}/agents`,
511
+ {
512
+ method: 'POST',
513
+ body: JSON.stringify({
514
+ name: input.name,
515
+ agent_type: input.agent_type,
516
+ priority: input.priority,
517
+ }),
518
+ },
519
+ );
520
+ return {
521
+ agent: agentSchema.parse(response.agent),
522
+ apiKey: response.api_key,
523
+ };
524
+ }
525
+
526
+ export async function listAgents(orgId: string) {
527
+ const response = await request<{ agents: unknown[] }>(`/v1/orgs/${orgId}/agents`);
528
+ return response.agents.map((agent) => agentSchema.parse(agent));
529
+ }
530
+
531
+ export async function createAvailabilityWindow(input: {
532
+ apiKey: string;
533
+ agent_id: string;
534
+ start_at: string;
535
+ end_at: string;
536
+ window_type: 'available' | 'focus' | 'blocked';
537
+ }) {
538
+ const response = await request<{ window: unknown }>('/v1/availability-windows', {
539
+ method: 'POST',
540
+ headers: { Authorization: `Bearer ${input.apiKey}` },
541
+ body: JSON.stringify({
542
+ agent_id: input.agent_id,
543
+ start_at: input.start_at,
544
+ end_at: input.end_at,
545
+ window_type: input.window_type,
546
+ }),
547
+ });
548
+ return availabilityWindowSchema.parse(response.window);
549
+ }
550
+
551
+ export async function checkAvailability(input: {
552
+ apiKey: string;
553
+ agent_id: string;
554
+ from: string;
555
+ to: string;
556
+ duration_minutes: number;
557
+ }) {
558
+ const query = new URLSearchParams({
559
+ agent_id: input.agent_id,
560
+ from: input.from,
561
+ to: input.to,
562
+ duration_minutes: String(input.duration_minutes),
563
+ });
564
+ const response = await request<{ slots: unknown[]; total_slots: number }>(
565
+ `/v1/availability?${query.toString()}`,
566
+ {
567
+ headers: { Authorization: `Bearer ${input.apiKey}` },
568
+ },
569
+ );
570
+ return {
571
+ slots: response.slots.map((slot) => slotSchema.parse(slot)),
572
+ totalSlots: response.total_slots,
573
+ };
574
+ }
575
+
576
+ export async function createHold(input: {
577
+ apiKey: string;
578
+ agent_id: string;
579
+ start_at: string;
580
+ end_at: string;
581
+ reason?: string;
582
+ }) {
583
+ const response = await request<{ hold: unknown }>('/v1/holds', {
584
+ method: 'POST',
585
+ headers: { Authorization: `Bearer ${input.apiKey}` },
586
+ body: JSON.stringify({
587
+ agent_id: input.agent_id,
588
+ start_at: input.start_at,
589
+ end_at: input.end_at,
590
+ reason: input.reason,
591
+ }),
592
+ });
593
+ return holdSchema.parse(response.hold);
594
+ }
595
+
596
+ export async function listHolds(apiKey: string) {
597
+ const response = await request<{ holds: unknown[] }>('/v1/holds', {
598
+ headers: { Authorization: `Bearer ${apiKey}` },
599
+ });
600
+ return response.holds.map((hold) => holdSchema.parse(hold));
601
+ }
602
+
603
+ export async function releaseHold(apiKey: string, holdId: string) {
604
+ const response = await request<{ hold: unknown }>(`/v1/holds/${holdId}`, {
605
+ method: 'DELETE',
606
+ headers: { Authorization: `Bearer ${apiKey}` },
607
+ });
608
+ return holdSchema.parse(response.hold);
609
+ }
610
+ ```
611
+
612
+ - [ ] **Step 3: Create local storage helpers**
613
+
614
+ `frontend/lib/storage.ts`:
615
+
616
+ ```ts
617
+ 'use client';
618
+
619
+ export type StoredSession = {
620
+ orgId: string;
621
+ agentId: string;
622
+ apiKey: string;
623
+ };
624
+
625
+ const KEY = 'availsync_session';
626
+
627
+ export function saveSession(session: StoredSession): void {
628
+ window.localStorage.setItem(KEY, JSON.stringify(session));
629
+ }
630
+
631
+ export function loadSession(): StoredSession | null {
632
+ const raw = window.localStorage.getItem(KEY);
633
+ if (!raw) return null;
634
+ try {
635
+ return JSON.parse(raw) as StoredSession;
636
+ } catch {
637
+ return null;
638
+ }
639
+ }
640
+
641
+ export function clearSession(): void {
642
+ window.localStorage.removeItem(KEY);
643
+ }
644
+ ```
645
+
646
+ `frontend/lib/format.ts`:
647
+
648
+ ```ts
649
+ export function toLocalInputValue(date: Date): string {
650
+ const offset = date.getTimezoneOffset();
651
+ const local = new Date(date.getTime() - offset * 60_000);
652
+ return local.toISOString().slice(0, 16);
653
+ }
654
+
655
+ export function fromLocalInputValue(value: string): string {
656
+ return new Date(value).toISOString();
657
+ }
658
+
659
+ export function formatDateTime(value: string): string {
660
+ return new Intl.DateTimeFormat(undefined, {
661
+ dateStyle: 'medium',
662
+ timeStyle: 'short',
663
+ }).format(new Date(value));
664
+ }
665
+ ```
666
+
667
+ - [ ] **Step 4: Verify build**
668
+
669
+ Run:
670
+
671
+ ```bash
672
+ cd frontend
673
+ npm run build
674
+ ```
675
+
676
+ Expected:
677
+
678
+ ```text
679
+ Compiled successfully
680
+ ```
681
+
682
+ ---
683
+
684
+ ## Task 3: UI Primitives
685
+
686
+ **Files:**
687
+ - Create: `frontend/components/ui/Button.tsx`
688
+ - Create: `frontend/components/ui/Card.tsx`
689
+ - Create: `frontend/components/ui/Input.tsx`
690
+ - Create: `frontend/components/ui/Select.tsx`
691
+ - Create: `frontend/components/ui/Textarea.tsx`
692
+ - Create: `frontend/components/ui/Tabs.tsx`
693
+
694
+ - [ ] **Step 1: Create button and card primitives**
695
+
696
+ `frontend/components/ui/Button.tsx`:
697
+
698
+ ```tsx
699
+ import Link from 'next/link';
700
+ import { type ButtonHTMLAttributes, type ReactNode } from 'react';
701
+ import { twMerge } from 'tailwind-merge';
702
+
703
+ type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
704
+ variant?: 'primary' | 'secondary' | 'ghost';
705
+ href?: string;
706
+ children: ReactNode;
707
+ };
708
+
709
+ const variants = {
710
+ primary: 'bg-ink text-white hover:bg-moss',
711
+ secondary: 'bg-white text-ink border border-line hover:border-ink',
712
+ ghost: 'bg-transparent text-ink hover:bg-white',
713
+ };
714
+
715
+ export function Button({ variant = 'primary', href, className, children, ...props }: ButtonProps) {
716
+ const classes = twMerge(
717
+ 'inline-flex min-h-12 items-center justify-center gap-2 rounded-md px-5 text-sm font-semibold transition',
718
+ variants[variant],
719
+ className,
720
+ );
721
+
722
+ if (href) {
723
+ return (
724
+ <Link className={classes} href={href}>
725
+ {children}
726
+ </Link>
727
+ );
728
+ }
729
+
730
+ return (
731
+ <button className={classes} {...props}>
732
+ {children}
733
+ </button>
734
+ );
735
+ }
736
+ ```
737
+
738
+ `frontend/components/ui/Card.tsx`:
739
+
740
+ ```tsx
741
+ import { type HTMLAttributes } from 'react';
742
+ import { twMerge } from 'tailwind-merge';
743
+
744
+ export function Card({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
745
+ return (
746
+ <div
747
+ className={twMerge('rounded-md border border-line bg-white p-5 shadow-panel', className)}
748
+ {...props}
749
+ />
750
+ );
751
+ }
752
+ ```
753
+
754
+ - [ ] **Step 2: Create form primitives**
755
+
756
+ `frontend/components/ui/Input.tsx`:
757
+
758
+ ```tsx
759
+ import { type InputHTMLAttributes } from 'react';
760
+ import { twMerge } from 'tailwind-merge';
761
+
762
+ export function Input({ className, ...props }: InputHTMLAttributes<HTMLInputElement>) {
763
+ return (
764
+ <input
765
+ className={twMerge(
766
+ 'min-h-12 w-full rounded-md border border-line bg-white px-3 text-base outline-none focus:border-blue',
767
+ className,
768
+ )}
769
+ {...props}
770
+ />
771
+ );
772
+ }
773
+ ```
774
+
775
+ `frontend/components/ui/Select.tsx`:
776
+
777
+ ```tsx
778
+ import { type SelectHTMLAttributes } from 'react';
779
+ import { twMerge } from 'tailwind-merge';
780
+
781
+ export function Select({ className, ...props }: SelectHTMLAttributes<HTMLSelectElement>) {
782
+ return (
783
+ <select
784
+ className={twMerge(
785
+ 'min-h-12 w-full rounded-md border border-line bg-white px-3 text-base outline-none focus:border-blue',
786
+ className,
787
+ )}
788
+ {...props}
789
+ />
790
+ );
791
+ }
792
+ ```
793
+
794
+ `frontend/components/ui/Textarea.tsx`:
795
+
796
+ ```tsx
797
+ import { type TextareaHTMLAttributes } from 'react';
798
+ import { twMerge } from 'tailwind-merge';
799
+
800
+ export function Textarea({ className, ...props }: TextareaHTMLAttributes<HTMLTextAreaElement>) {
801
+ return (
802
+ <textarea
803
+ className={twMerge(
804
+ 'min-h-28 w-full rounded-md border border-line bg-white px-3 py-2 text-base outline-none focus:border-blue',
805
+ className,
806
+ )}
807
+ {...props}
808
+ />
809
+ );
810
+ }
811
+ ```
812
+
813
+ - [ ] **Step 3: Create simple tabs helper**
814
+
815
+ `frontend/components/ui/Tabs.tsx`:
816
+
817
+ ```tsx
818
+ import { type ReactNode } from 'react';
819
+ import { twMerge } from 'tailwind-merge';
820
+
821
+ export function TabList({ children, className }: { children: ReactNode; className?: string }) {
822
+ return <div className={twMerge('flex gap-2 border-b border-line', className)}>{children}</div>;
823
+ }
824
+
825
+ export function TabButton({
826
+ active,
827
+ children,
828
+ }: {
829
+ active?: boolean;
830
+ children: ReactNode;
831
+ }) {
832
+ return (
833
+ <span
834
+ className={twMerge(
835
+ 'px-3 py-2 text-sm font-semibold',
836
+ active ? 'border-b-2 border-ink text-ink' : 'text-ink/60',
837
+ )}
838
+ >
839
+ {children}
840
+ </span>
841
+ );
842
+ }
843
+ ```
844
+
845
+ - [ ] **Step 4: Verify build**
846
+
847
+ Run:
848
+
849
+ ```bash
850
+ cd frontend
851
+ npm run build
852
+ ```
853
+
854
+ Expected:
855
+
856
+ ```text
857
+ Compiled successfully
858
+ ```
859
+
860
+ ---
861
+
862
+ ## Task 4: Marketing Homepage
863
+
864
+ **Files:**
865
+ - Create: `frontend/components/marketing/SiteHeader.tsx`
866
+ - Create: `frontend/components/marketing/SiteFooter.tsx`
867
+ - Create: `frontend/components/marketing/Hero.tsx`
868
+ - Create: `frontend/components/marketing/ProblemSolution.tsx`
869
+ - Create: `frontend/components/marketing/HowItWorks.tsx`
870
+ - Create: `frontend/components/marketing/UseCases.tsx`
871
+ - Create: `frontend/components/marketing/PricingTeaser.tsx`
872
+ - Create: `frontend/components/marketing/Faq.tsx`
873
+ - Create: `frontend/app/page.tsx`
874
+
875
+ - [ ] **Step 1: Create header and footer**
876
+
877
+ `frontend/components/marketing/SiteHeader.tsx`:
878
+
879
+ ```tsx
880
+ import { CalendarCheck } from 'lucide-react';
881
+ import { Button } from '@/components/ui/Button';
882
+
883
+ export function SiteHeader() {
884
+ return (
885
+ <header className="border-b border-line bg-paper/95">
886
+ <div className="mx-auto flex max-w-6xl items-center justify-between px-4 py-4">
887
+ <a className="flex items-center gap-2 font-semibold" href="/">
888
+ <CalendarCheck className="h-5 w-5" />
889
+ Availsync
890
+ </a>
891
+ <nav className="hidden items-center gap-6 text-sm md:flex">
892
+ <a href="/pricing">Pricing</a>
893
+ <a href="/docs">Docs</a>
894
+ <a href="/docs/quickstart">Quickstart</a>
895
+ </nav>
896
+ <Button href="/signup">Start free</Button>
897
+ </div>
898
+ </header>
899
+ );
900
+ }
901
+ ```
902
+
903
+ `frontend/components/marketing/SiteFooter.tsx`:
904
+
905
+ ```tsx
906
+ export function SiteFooter() {
907
+ return (
908
+ <footer className="border-t border-line bg-ink text-white">
909
+ <div className="mx-auto grid max-w-6xl gap-6 px-4 py-10 md:grid-cols-3">
910
+ <div>
911
+ <p className="font-semibold">Availsync</p>
912
+ <p className="mt-2 text-sm text-white/70">Scheduling source of truth for AI agents.</p>
913
+ </div>
914
+ <div className="text-sm text-white/70">
915
+ <p className="font-semibold text-white">Product</p>
916
+ <a className="mt-2 block" href="/pricing">Pricing</a>
917
+ <a className="mt-2 block" href="/docs">Docs</a>
918
+ <a className="mt-2 block" href="/signup">Start free</a>
919
+ </div>
920
+ <div className="text-sm text-white/70">
921
+ <p className="font-semibold text-white">Trust</p>
922
+ <p className="mt-2">API keys are hashed. Holds are backed by PostgreSQL transactions.</p>
923
+ </div>
924
+ </div>
925
+ </footer>
926
+ );
927
+ }
928
+ ```
929
+
930
+ - [ ] **Step 2: Create homepage sections**
931
+
932
+ `frontend/components/marketing/Hero.tsx`:
933
+
934
+ ```tsx
935
+ import { ArrowRight, ShieldCheck } from 'lucide-react';
936
+ import { Button } from '@/components/ui/Button';
937
+
938
+ export function Hero() {
939
+ return (
940
+ <section className="bg-paper">
941
+ <div className="mx-auto grid min-h-[680px] max-w-6xl items-center gap-10 px-4 py-16 md:grid-cols-[1.05fr_0.95fr]">
942
+ <div>
943
+ <p className="mb-4 inline-flex items-center gap-2 rounded-md border border-line bg-white px-3 py-2 text-sm font-semibold">
944
+ <ShieldCheck className="h-4 w-4" />
945
+ Built for AI scheduling agents
946
+ </p>
947
+ <h1 className="max-w-3xl text-5xl font-semibold leading-tight md:text-6xl">
948
+ One scheduling source of truth for AI agents
949
+ </h1>
950
+ <p className="mt-6 max-w-2xl text-xl leading-8 text-ink/70">
951
+ Prevent sales bots, recruiting agents, and calendar automations from double-booking the same time slot.
952
+ </p>
953
+ <div className="mt-8 flex flex-col gap-3 sm:flex-row">
954
+ <Button href="/signup">
955
+ Start free <ArrowRight className="h-4 w-4" />
956
+ </Button>
957
+ <Button href="/docs/quickstart" variant="secondary">
958
+ View API docs
959
+ </Button>
960
+ </div>
961
+ <p className="mt-5 text-sm text-ink/60">Free plan includes 3 agents. No card required.</p>
962
+ </div>
963
+ <div className="rounded-md border border-line bg-white p-4 shadow-panel">
964
+ <div className="rounded-md bg-ink p-5 text-white">
965
+ <p className="text-sm text-white/60">Conflict resolved</p>
966
+ <p className="mt-3 text-2xl font-semibold">Sales Bot kept 10:00</p>
967
+ <p className="mt-2 text-sm text-white/70">Recruiting Bot received 3 alternative slots with confidence scores.</p>
968
+ <div className="mt-6 grid gap-3">
969
+ {['10:30 - 11:00 · 0.95', '11:00 - 11:30 · 0.90', '14:00 - 14:30 · 0.85'].map((slot) => (
970
+ <div className="rounded-md bg-white/10 px-4 py-3 text-sm" key={slot}>{slot}</div>
971
+ ))}
972
+ </div>
973
+ </div>
974
+ </div>
975
+ </div>
976
+ </section>
977
+ );
978
+ }
979
+ ```
980
+
981
+ `frontend/components/marketing/ProblemSolution.tsx`:
982
+
983
+ ```tsx
984
+ export function ProblemSolution() {
985
+ return (
986
+ <section className="bg-white">
987
+ <div className="mx-auto grid max-w-6xl gap-8 px-4 py-20 md:grid-cols-2">
988
+ <div>
989
+ <h2 className="text-3xl font-semibold">Your agents are booking independently.</h2>
990
+ <p className="mt-4 text-lg leading-8 text-ink/70">
991
+ Sales, recruiting, and calendar agents can all see availability, but they do not share the same rules, holds, buffers, and priorities.
992
+ </p>
993
+ </div>
994
+ <div>
995
+ <h2 className="text-3xl font-semibold">Availsync becomes the shared decision layer.</h2>
996
+ <p className="mt-4 text-lg leading-8 text-ink/70">
997
+ Every agent checks one API before booking. Availsync applies availability windows, buffers, focus blocks, and priority rules before a hold is confirmed.
998
+ </p>
999
+ </div>
1000
+ </div>
1001
+ </section>
1002
+ );
1003
+ }
1004
+ ```
1005
+
1006
+ `frontend/components/marketing/HowItWorks.tsx`:
1007
+
1008
+ ```tsx
1009
+ const steps = [
1010
+ ['Create agents', 'Add each AI tool as an agent with its own type, priority, and API key.'],
1011
+ ['Define rules', 'Set availability windows, buffers, focus blocks, and priority preferences.'],
1012
+ ['Book through Availsync', 'Agents check availability, create holds, and receive alternatives when conflicts happen.'],
1013
+ ];
1014
+
1015
+ export function HowItWorks() {
1016
+ return (
1017
+ <section className="bg-paper">
1018
+ <div className="mx-auto max-w-6xl px-4 py-20">
1019
+ <h2 className="text-3xl font-semibold">How it works</h2>
1020
+ <div className="mt-8 grid gap-4 md:grid-cols-3">
1021
+ {steps.map(([title, body], index) => (
1022
+ <div className="rounded-md border border-line bg-white p-5" key={title}>
1023
+ <p className="text-sm font-semibold text-rust">0{index + 1}</p>
1024
+ <h3 className="mt-3 text-xl font-semibold">{title}</h3>
1025
+ <p className="mt-3 leading-7 text-ink/70">{body}</p>
1026
+ </div>
1027
+ ))}
1028
+ </div>
1029
+ </div>
1030
+ </section>
1031
+ );
1032
+ }
1033
+ ```
1034
+
1035
+ `frontend/components/marketing/UseCases.tsx`:
1036
+
1037
+ ```tsx
1038
+ const cases = [
1039
+ ['Sales bots', 'Protect demo slots while outbound agents book meetings automatically.'],
1040
+ ['Recruiting bots', 'Coordinate candidate screens without stealing priority customer calls.'],
1041
+ ['Internal assistants', 'Reserve focus time and internal meetings around external commitments.'],
1042
+ ];
1043
+
1044
+ export function UseCases() {
1045
+ return (
1046
+ <section className="bg-white">
1047
+ <div className="mx-auto max-w-6xl px-4 py-20">
1048
+ <h2 className="text-3xl font-semibold">Built for multi-agent scheduling</h2>
1049
+ <div className="mt-8 grid gap-4 md:grid-cols-3">
1050
+ {cases.map(([title, body]) => (
1051
+ <div className="rounded-md border border-line p-5" key={title}>
1052
+ <h3 className="text-xl font-semibold">{title}</h3>
1053
+ <p className="mt-3 leading-7 text-ink/70">{body}</p>
1054
+ </div>
1055
+ ))}
1056
+ </div>
1057
+ </div>
1058
+ </section>
1059
+ );
1060
+ }
1061
+ ```
1062
+
1063
+ `frontend/components/marketing/PricingTeaser.tsx`:
1064
+
1065
+ ```tsx
1066
+ import { Button } from '@/components/ui/Button';
1067
+
1068
+ export function PricingTeaser() {
1069
+ return (
1070
+ <section className="bg-ink text-white">
1071
+ <div className="mx-auto flex max-w-6xl flex-col items-start justify-between gap-6 px-4 py-16 md:flex-row md:items-center">
1072
+ <div>
1073
+ <h2 className="text-3xl font-semibold">Start with 3 agents for free.</h2>
1074
+ <p className="mt-3 max-w-2xl text-white/70">Upgrade when you need more agents, longer booking windows, and team-level coordination.</p>
1075
+ </div>
1076
+ <Button href="/pricing" variant="secondary">View pricing</Button>
1077
+ </div>
1078
+ </section>
1079
+ );
1080
+ }
1081
+ ```
1082
+
1083
+ `frontend/components/marketing/Faq.tsx`:
1084
+
1085
+ ```tsx
1086
+ const faqs = [
1087
+ ['Is Availsync a calendar app?', 'No. It is a scheduling rules and holds API for AI agents that already book into calendars.'],
1088
+ ['Can I use it locally?', 'Yes. The MVP runs with Docker Compose and PostgreSQL.'],
1089
+ ['Do API keys appear again?', 'No. The agent API key is shown once when the agent is created.'],
1090
+ ['What happens when two agents want the same slot?', 'Availsync applies explicit priority rules, agent type hierarchy, agent priority, then first-come-first-served.'],
1091
+ ];
1092
+
1093
+ export function Faq() {
1094
+ return (
1095
+ <section className="bg-paper">
1096
+ <div className="mx-auto max-w-4xl px-4 py-20">
1097
+ <h2 className="text-3xl font-semibold">Questions buyers ask</h2>
1098
+ <div className="mt-8 divide-y divide-line rounded-md border border-line bg-white">
1099
+ {faqs.map(([question, answer]) => (
1100
+ <div className="p-5" key={question}>
1101
+ <h3 className="font-semibold">{question}</h3>
1102
+ <p className="mt-2 leading-7 text-ink/70">{answer}</p>
1103
+ </div>
1104
+ ))}
1105
+ </div>
1106
+ </div>
1107
+ </section>
1108
+ );
1109
+ }
1110
+ ```
1111
+
1112
+ - [ ] **Step 3: Compose homepage**
1113
+
1114
+ `frontend/app/page.tsx`:
1115
+
1116
+ ```tsx
1117
+ import { Faq } from '@/components/marketing/Faq';
1118
+ import { Hero } from '@/components/marketing/Hero';
1119
+ import { HowItWorks } from '@/components/marketing/HowItWorks';
1120
+ import { PricingTeaser } from '@/components/marketing/PricingTeaser';
1121
+ import { ProblemSolution } from '@/components/marketing/ProblemSolution';
1122
+ import { SiteFooter } from '@/components/marketing/SiteFooter';
1123
+ import { SiteHeader } from '@/components/marketing/SiteHeader';
1124
+ import { UseCases } from '@/components/marketing/UseCases';
1125
+
1126
+ export default function HomePage() {
1127
+ return (
1128
+ <>
1129
+ <SiteHeader />
1130
+ <main>
1131
+ <Hero />
1132
+ <ProblemSolution />
1133
+ <HowItWorks />
1134
+ <UseCases />
1135
+ <PricingTeaser />
1136
+ <Faq />
1137
+ </main>
1138
+ <SiteFooter />
1139
+ </>
1140
+ );
1141
+ }
1142
+ ```
1143
+
1144
+ - [ ] **Step 4: Verify**
1145
+
1146
+ Run:
1147
+
1148
+ ```bash
1149
+ cd frontend
1150
+ npm run build
1151
+ npm run dev
1152
+ ```
1153
+
1154
+ Open:
1155
+
1156
+ ```text
1157
+ http://localhost:3001
1158
+ ```
1159
+
1160
+ Expected:
1161
+
1162
+ ```text
1163
+ Hero headline, Start free CTA, API docs CTA, pricing teaser, and FAQ render without overlap on desktop and mobile.
1164
+ ```
1165
+
1166
+ ---
1167
+
1168
+ ## Task 5: Pricing And Checkout Entry
1169
+
1170
+ **Files:**
1171
+ - Create: `frontend/lib/plans.ts`
1172
+ - Create: `frontend/components/pricing/PricingCards.tsx`
1173
+ - Create: `frontend/app/pricing/page.tsx`
1174
+ - Create: `frontend/app/checkout/page.tsx`
1175
+
1176
+ - [ ] **Step 1: Create plan data**
1177
+
1178
+ `frontend/lib/plans.ts`:
1179
+
1180
+ ```ts
1181
+ export const plans = [
1182
+ {
1183
+ id: 'free',
1184
+ name: 'Free',
1185
+ price: '$0',
1186
+ description: 'For testing Availsync with up to 3 agents.',
1187
+ cta: 'Start free',
1188
+ href: '/signup?plan=free',
1189
+ features: ['3 agents', 'Availability windows', 'Confirmed holds', 'Basic conflict rules'],
1190
+ },
1191
+ {
1192
+ id: 'individual',
1193
+ name: 'Individual',
1194
+ price: '$19',
1195
+ description: 'For solo operators running multiple AI agents.',
1196
+ cta: 'Start individual',
1197
+ href: '/checkout?plan=individual',
1198
+ features: ['More agents', 'Priority rules', 'Longer booking windows', 'API quickstart support'],
1199
+ },
1200
+ {
1201
+ id: 'team',
1202
+ name: 'Team',
1203
+ price: '$49',
1204
+ description: 'For teams coordinating AI-driven scheduling.',
1205
+ cta: 'Start team',
1206
+ href: '/checkout?plan=team',
1207
+ features: ['Team usage', 'Audit history', 'Advanced conflict workflows', 'Priority support'],
1208
+ },
1209
+ ] as const;
1210
+ ```
1211
+
1212
+ - [ ] **Step 2: Create pricing cards**
1213
+
1214
+ `frontend/components/pricing/PricingCards.tsx`:
1215
+
1216
+ ```tsx
1217
+ import { Check } from 'lucide-react';
1218
+ import { Button } from '@/components/ui/Button';
1219
+ import { Card } from '@/components/ui/Card';
1220
+ import { plans } from '@/lib/plans';
1221
+
1222
+ export function PricingCards() {
1223
+ return (
1224
+ <div className="grid gap-4 md:grid-cols-3">
1225
+ {plans.map((plan) => (
1226
+ <Card className={plan.id === 'individual' ? 'border-ink' : ''} key={plan.id}>
1227
+ <p className="text-sm font-semibold uppercase tracking-wide text-ink/60">{plan.name}</p>
1228
+ <p className="mt-4 text-4xl font-semibold">{plan.price}<span className="text-base text-ink/60">/mo</span></p>
1229
+ <p className="mt-3 min-h-14 text-ink/70">{plan.description}</p>
1230
+ <Button className="mt-6 w-full" href={plan.href}>{plan.cta}</Button>
1231
+ <div className="mt-6 space-y-3">
1232
+ {plan.features.map((feature) => (
1233
+ <p className="flex gap-2 text-sm" key={feature}>
1234
+ <Check className="h-4 w-4 text-moss" />
1235
+ {feature}
1236
+ </p>
1237
+ ))}
1238
+ </div>
1239
+ </Card>
1240
+ ))}
1241
+ </div>
1242
+ );
1243
+ }
1244
+ ```
1245
+
1246
+ - [ ] **Step 3: Create pricing and checkout pages**
1247
+
1248
+ `frontend/app/pricing/page.tsx`:
1249
+
1250
+ ```tsx
1251
+ import { SiteFooter } from '@/components/marketing/SiteFooter';
1252
+ import { SiteHeader } from '@/components/marketing/SiteHeader';
1253
+ import { PricingCards } from '@/components/pricing/PricingCards';
1254
+
1255
+ export default function PricingPage() {
1256
+ return (
1257
+ <>
1258
+ <SiteHeader />
1259
+ <main className="bg-paper">
1260
+ <section className="mx-auto max-w-6xl px-4 py-20">
1261
+ <h1 className="max-w-3xl text-5xl font-semibold">Simple pricing for AI-agent scheduling.</h1>
1262
+ <p className="mt-5 max-w-2xl text-xl leading-8 text-ink/70">
1263
+ Start free, then upgrade when more agents need shared rules, holds, and conflict resolution.
1264
+ </p>
1265
+ <div className="mt-10">
1266
+ <PricingCards />
1267
+ </div>
1268
+ </section>
1269
+ </main>
1270
+ <SiteFooter />
1271
+ </>
1272
+ );
1273
+ }
1274
+ ```
1275
+
1276
+ `frontend/app/checkout/page.tsx`:
1277
+
1278
+ ```tsx
1279
+ import { SiteHeader } from '@/components/marketing/SiteHeader';
1280
+ import { Button } from '@/components/ui/Button';
1281
+
1282
+ export default function CheckoutPage({ searchParams }: { searchParams: { plan?: string } }) {
1283
+ const plan = searchParams.plan || 'individual';
1284
+
1285
+ return (
1286
+ <>
1287
+ <SiteHeader />
1288
+ <main className="bg-paper">
1289
+ <section className="mx-auto max-w-3xl px-4 py-20">
1290
+ <h1 className="text-4xl font-semibold">Checkout for {plan}</h1>
1291
+ <p className="mt-4 text-lg leading-8 text-ink/70">
1292
+ Paid checkout will connect to Stripe. You can still start with the free onboarding flow now.
1293
+ </p>
1294
+ <div className="mt-8 flex gap-3">
1295
+ <Button href={`/signup?plan=${plan}`}>Continue to setup</Button>
1296
+ <Button href="/pricing" variant="secondary">Back to pricing</Button>
1297
+ </div>
1298
+ </section>
1299
+ </main>
1300
+ </>
1301
+ );
1302
+ }
1303
+ ```
1304
+
1305
+ - [ ] **Step 4: Verify**
1306
+
1307
+ Run:
1308
+
1309
+ ```bash
1310
+ cd frontend
1311
+ npm run build
1312
+ ```
1313
+
1314
+ Expected:
1315
+
1316
+ ```text
1317
+ Pricing and checkout routes compile.
1318
+ ```
1319
+
1320
+ ---
1321
+
1322
+ ## Task 6: Signup And Onboarding
1323
+
1324
+ **Files:**
1325
+ - Create: `frontend/app/signup/page.tsx`
1326
+ - Create: `frontend/app/app/onboarding/page.tsx`
1327
+ - Create: `frontend/components/dashboard/AgentForm.tsx`
1328
+ - Create: `frontend/components/dashboard/AvailabilityWindowForm.tsx`
1329
+
1330
+ - [ ] **Step 1: Create signup page**
1331
+
1332
+ `frontend/app/signup/page.tsx`:
1333
+
1334
+ ```tsx
1335
+ 'use client';
1336
+
1337
+ import { useState } from 'react';
1338
+ import { useRouter, useSearchParams } from 'next/navigation';
1339
+ import { createOrg } from '@/lib/api';
1340
+ import { Button } from '@/components/ui/Button';
1341
+ import { Input } from '@/components/ui/Input';
1342
+ import { SiteHeader } from '@/components/marketing/SiteHeader';
1343
+
1344
+ export default function SignupPage() {
1345
+ const router = useRouter();
1346
+ const searchParams = useSearchParams();
1347
+ const [name, setName] = useState('');
1348
+ const [error, setError] = useState('');
1349
+ const [loading, setLoading] = useState(false);
1350
+
1351
+ async function onSubmit(event: React.FormEvent) {
1352
+ event.preventDefault();
1353
+ setError('');
1354
+ setLoading(true);
1355
+ try {
1356
+ const org = await createOrg(name);
1357
+ router.push(`/app/onboarding?orgId=${org.id}&plan=${searchParams.get('plan') || 'free'}`);
1358
+ } catch (err) {
1359
+ setError(err instanceof Error ? err.message : 'Could not create organization');
1360
+ } finally {
1361
+ setLoading(false);
1362
+ }
1363
+ }
1364
+
1365
+ return (
1366
+ <>
1367
+ <SiteHeader />
1368
+ <main className="bg-paper">
1369
+ <section className="mx-auto max-w-xl px-4 py-20">
1370
+ <h1 className="text-4xl font-semibold">Create your Availsync workspace</h1>
1371
+ <p className="mt-3 text-ink/70">Start by creating the organization your AI agents will belong to.</p>
1372
+ <form className="mt-8 rounded-md border border-line bg-white p-5" onSubmit={onSubmit}>
1373
+ <label className="text-sm font-semibold" htmlFor="org-name">Organization name</label>
1374
+ <Input id="org-name" required value={name} onChange={(event) => setName(event.target.value)} />
1375
+ {error && <p className="mt-3 text-sm text-rust">{error}</p>}
1376
+ <Button className="mt-5 w-full" disabled={loading} type="submit">
1377
+ {loading ? 'Creating...' : 'Create organization'}
1378
+ </Button>
1379
+ </form>
1380
+ </section>
1381
+ </main>
1382
+ </>
1383
+ );
1384
+ }
1385
+ ```
1386
+
1387
+ - [ ] **Step 2: Create onboarding page**
1388
+
1389
+ `frontend/app/app/onboarding/page.tsx`:
1390
+
1391
+ ```tsx
1392
+ 'use client';
1393
+
1394
+ import { useState } from 'react';
1395
+ import { useRouter, useSearchParams } from 'next/navigation';
1396
+ import { createAgent, createAvailabilityWindow } from '@/lib/api';
1397
+ import { saveSession } from '@/lib/storage';
1398
+ import { fromLocalInputValue, toLocalInputValue } from '@/lib/format';
1399
+ import { Button } from '@/components/ui/Button';
1400
+ import { Input } from '@/components/ui/Input';
1401
+ import { Select } from '@/components/ui/Select';
1402
+
1403
+ export default function OnboardingPage() {
1404
+ const router = useRouter();
1405
+ const searchParams = useSearchParams();
1406
+ const orgId = searchParams.get('orgId') || '';
1407
+ const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000);
1408
+ tomorrow.setHours(9, 0, 0, 0);
1409
+ const tomorrowEnd = new Date(tomorrow);
1410
+ tomorrowEnd.setHours(17, 0, 0, 0);
1411
+
1412
+ const [name, setName] = useState('Sales Bot');
1413
+ const [agentType, setAgentType] = useState('external_meeting');
1414
+ const [priority, setPriority] = useState(10);
1415
+ const [startAt, setStartAt] = useState(toLocalInputValue(tomorrow));
1416
+ const [endAt, setEndAt] = useState(toLocalInputValue(tomorrowEnd));
1417
+ const [apiKey, setApiKey] = useState('');
1418
+ const [error, setError] = useState('');
1419
+
1420
+ async function onSubmit(event: React.FormEvent) {
1421
+ event.preventDefault();
1422
+ setError('');
1423
+ try {
1424
+ const created = await createAgent({
1425
+ orgId,
1426
+ name,
1427
+ agent_type: agentType as 'external_meeting',
1428
+ priority,
1429
+ });
1430
+ await createAvailabilityWindow({
1431
+ apiKey: created.apiKey,
1432
+ agent_id: created.agent.id,
1433
+ start_at: fromLocalInputValue(startAt),
1434
+ end_at: fromLocalInputValue(endAt),
1435
+ window_type: 'available',
1436
+ });
1437
+ saveSession({ orgId, agentId: created.agent.id, apiKey: created.apiKey });
1438
+ setApiKey(created.apiKey);
1439
+ } catch (err) {
1440
+ setError(err instanceof Error ? err.message : 'Onboarding failed');
1441
+ }
1442
+ }
1443
+
1444
+ return (
1445
+ <main className="bg-paper">
1446
+ <section className="mx-auto max-w-3xl px-4 py-12">
1447
+ <h1 className="text-4xl font-semibold">Create your first AI agent</h1>
1448
+ <p className="mt-3 text-ink/70">This creates an agent API key and a first availability window.</p>
1449
+ <form className="mt-8 grid gap-4 rounded-md border border-line bg-white p-5" onSubmit={onSubmit}>
1450
+ <label className="text-sm font-semibold">Agent name</label>
1451
+ <Input required value={name} onChange={(event) => setName(event.target.value)} />
1452
+ <label className="text-sm font-semibold">Agent type</label>
1453
+ <Select value={agentType} onChange={(event) => setAgentType(event.target.value)}>
1454
+ <option value="external_meeting">Sales or external meeting bot</option>
1455
+ <option value="internal">Internal assistant</option>
1456
+ <option value="focus">Focus blocker</option>
1457
+ <option value="generic">Generic agent</option>
1458
+ </Select>
1459
+ <label className="text-sm font-semibold">Priority</label>
1460
+ <Input type="number" value={priority} onChange={(event) => setPriority(Number(event.target.value))} />
1461
+ <label className="text-sm font-semibold">Availability starts</label>
1462
+ <Input type="datetime-local" value={startAt} onChange={(event) => setStartAt(event.target.value)} />
1463
+ <label className="text-sm font-semibold">Availability ends</label>
1464
+ <Input type="datetime-local" value={endAt} onChange={(event) => setEndAt(event.target.value)} />
1465
+ {error && <p className="text-sm text-rust">{error}</p>}
1466
+ <Button type="submit">Create agent and window</Button>
1467
+ </form>
1468
+ {apiKey && (
1469
+ <div className="mt-6 rounded-md border border-line bg-white p-5">
1470
+ <h2 className="text-xl font-semibold">Copy this API key now</h2>
1471
+ <p className="mt-2 text-sm text-ink/60">It is only shown once.</p>
1472
+ <code className="mt-4 block overflow-x-auto rounded-md bg-ink p-4 text-sm text-white">{apiKey}</code>
1473
+ <Button className="mt-5" href="/app/availability">Check availability</Button>
1474
+ </div>
1475
+ )}
1476
+ </section>
1477
+ </main>
1478
+ );
1479
+ }
1480
+ ```
1481
+
1482
+ - [ ] **Step 3: Verify onboarding**
1483
+
1484
+ Run backend first:
1485
+
1486
+ ```bash
1487
+ docker compose up -d
1488
+ ```
1489
+
1490
+ Run frontend:
1491
+
1492
+ ```bash
1493
+ cd frontend
1494
+ npm run dev
1495
+ ```
1496
+
1497
+ Open:
1498
+
1499
+ ```text
1500
+ http://localhost:3001/signup
1501
+ ```
1502
+
1503
+ Expected:
1504
+
1505
+ ```text
1506
+ Creating an org redirects to onboarding. Submitting onboarding creates an agent, creates an availability window, shows an API key, and stores session data in localStorage.
1507
+ ```
1508
+
1509
+ ---
1510
+
1511
+ ## Task 7: Dashboard Shell And Overview
1512
+
1513
+ **Files:**
1514
+ - Create: `frontend/components/dashboard/AppShell.tsx`
1515
+ - Create: `frontend/components/dashboard/MetricCard.tsx`
1516
+ - Create: `frontend/app/app/layout.tsx`
1517
+ - Create: `frontend/app/app/page.tsx`
1518
+
1519
+ - [ ] **Step 1: Create app shell**
1520
+
1521
+ `frontend/components/dashboard/AppShell.tsx`:
1522
+
1523
+ ```tsx
1524
+ import { CalendarClock, Home, KeyRound, Users } from 'lucide-react';
1525
+
1526
+ const links = [
1527
+ ['/app', 'Overview', Home],
1528
+ ['/app/agents', 'Agents', Users],
1529
+ ['/app/availability', 'Availability', CalendarClock],
1530
+ ['/app/holds', 'Holds', KeyRound],
1531
+ ] as const;
1532
+
1533
+ export function AppShell({ children }: { children: React.ReactNode }) {
1534
+ return (
1535
+ <div className="min-h-screen bg-paper md:grid md:grid-cols-[240px_1fr]">
1536
+ <aside className="border-b border-line bg-white p-4 md:border-b-0 md:border-r">
1537
+ <a className="font-semibold" href="/app">Availsync</a>
1538
+ <nav className="mt-6 grid gap-2">
1539
+ {links.map(([href, label, Icon]) => (
1540
+ <a className="flex items-center gap-2 rounded-md px-3 py-2 text-sm hover:bg-paper" href={href} key={href}>
1541
+ <Icon className="h-4 w-4" />
1542
+ {label}
1543
+ </a>
1544
+ ))}
1545
+ </nav>
1546
+ </aside>
1547
+ <main>{children}</main>
1548
+ </div>
1549
+ );
1550
+ }
1551
+ ```
1552
+
1553
+ `frontend/components/dashboard/MetricCard.tsx`:
1554
+
1555
+ ```tsx
1556
+ import { Card } from '@/components/ui/Card';
1557
+
1558
+ export function MetricCard({ label, value }: { label: string; value: string }) {
1559
+ return (
1560
+ <Card>
1561
+ <p className="text-sm text-ink/60">{label}</p>
1562
+ <p className="mt-2 text-3xl font-semibold">{value}</p>
1563
+ </Card>
1564
+ );
1565
+ }
1566
+ ```
1567
+
1568
+ - [ ] **Step 2: Create dashboard layout and overview**
1569
+
1570
+ `frontend/app/app/layout.tsx`:
1571
+
1572
+ ```tsx
1573
+ import { AppShell } from '@/components/dashboard/AppShell';
1574
+
1575
+ export default function DashboardLayout({ children }: { children: React.ReactNode }) {
1576
+ return <AppShell>{children}</AppShell>;
1577
+ }
1578
+ ```
1579
+
1580
+ `frontend/app/app/page.tsx`:
1581
+
1582
+ ```tsx
1583
+ 'use client';
1584
+
1585
+ import { useEffect, useState } from 'react';
1586
+ import { MetricCard } from '@/components/dashboard/MetricCard';
1587
+ import { Button } from '@/components/ui/Button';
1588
+ import { loadSession, type StoredSession } from '@/lib/storage';
1589
+
1590
+ export default function DashboardPage() {
1591
+ const [session, setSession] = useState<StoredSession | null>(null);
1592
+
1593
+ useEffect(() => {
1594
+ setSession(loadSession());
1595
+ }, []);
1596
+
1597
+ return (
1598
+ <section className="mx-auto max-w-6xl px-4 py-10">
1599
+ <h1 className="text-4xl font-semibold">Dashboard</h1>
1600
+ <p className="mt-3 text-ink/70">Manage the scheduling rules your AI agents use before booking.</p>
1601
+ <div className="mt-8 grid gap-4 md:grid-cols-3">
1602
+ <MetricCard label="Workspace" value={session?.orgId ? 'Connected' : 'Missing'} />
1603
+ <MetricCard label="Agent" value={session?.agentId ? '1 active' : '0 active'} />
1604
+ <MetricCard label="API key" value={session?.apiKey ? 'Stored locally' : 'Missing'} />
1605
+ </div>
1606
+ <div className="mt-8 flex gap-3">
1607
+ <Button href="/app/availability">Check availability</Button>
1608
+ <Button href="/app/holds" variant="secondary">Manage holds</Button>
1609
+ </div>
1610
+ </section>
1611
+ );
1612
+ }
1613
+ ```
1614
+
1615
+ - [ ] **Step 3: Verify**
1616
+
1617
+ Run:
1618
+
1619
+ ```bash
1620
+ cd frontend
1621
+ npm run build
1622
+ ```
1623
+
1624
+ Expected:
1625
+
1626
+ ```text
1627
+ Dashboard pages compile.
1628
+ ```
1629
+
1630
+ ---
1631
+
1632
+ ## Task 8: Availability Checker UI
1633
+
1634
+ **Files:**
1635
+ - Create: `frontend/components/dashboard/AvailabilityChecker.tsx`
1636
+ - Create: `frontend/app/app/availability/page.tsx`
1637
+
1638
+ - [ ] **Step 1: Create availability checker component**
1639
+
1640
+ `frontend/components/dashboard/AvailabilityChecker.tsx`:
1641
+
1642
+ ```tsx
1643
+ 'use client';
1644
+
1645
+ import { useEffect, useState } from 'react';
1646
+ import { checkAvailability } from '@/lib/api';
1647
+ import { fromLocalInputValue, formatDateTime, toLocalInputValue } from '@/lib/format';
1648
+ import { loadSession, type StoredSession } from '@/lib/storage';
1649
+ import { Button } from '@/components/ui/Button';
1650
+ import { Input } from '@/components/ui/Input';
1651
+ import type { Slot } from '@/lib/schemas';
1652
+
1653
+ export function AvailabilityChecker() {
1654
+ const [session, setSession] = useState<StoredSession | null>(null);
1655
+ const [from, setFrom] = useState(toLocalInputValue(new Date(Date.now() + 24 * 60 * 60 * 1000)));
1656
+ const [to, setTo] = useState(toLocalInputValue(new Date(Date.now() + 25 * 60 * 60 * 1000)));
1657
+ const [duration, setDuration] = useState(30);
1658
+ const [slots, setSlots] = useState<Slot[]>([]);
1659
+ const [error, setError] = useState('');
1660
+
1661
+ useEffect(() => {
1662
+ setSession(loadSession());
1663
+ }, []);
1664
+
1665
+ async function onSubmit(event: React.FormEvent) {
1666
+ event.preventDefault();
1667
+ if (!session) return;
1668
+ setError('');
1669
+ try {
1670
+ const result = await checkAvailability({
1671
+ apiKey: session.apiKey,
1672
+ agent_id: session.agentId,
1673
+ from: fromLocalInputValue(from),
1674
+ to: fromLocalInputValue(to),
1675
+ duration_minutes: duration,
1676
+ });
1677
+ setSlots(result.slots);
1678
+ } catch (err) {
1679
+ setError(err instanceof Error ? err.message : 'Could not check availability');
1680
+ }
1681
+ }
1682
+
1683
+ if (!session) {
1684
+ return <p>Create an agent in onboarding before checking availability.</p>;
1685
+ }
1686
+
1687
+ return (
1688
+ <div className="grid gap-6">
1689
+ <form className="grid gap-4 rounded-md border border-line bg-white p-5" onSubmit={onSubmit}>
1690
+ <label className="text-sm font-semibold">From</label>
1691
+ <Input type="datetime-local" value={from} onChange={(event) => setFrom(event.target.value)} />
1692
+ <label className="text-sm font-semibold">To</label>
1693
+ <Input type="datetime-local" value={to} onChange={(event) => setTo(event.target.value)} />
1694
+ <label className="text-sm font-semibold">Duration minutes</label>
1695
+ <Input type="number" min={1} max={480} value={duration} onChange={(event) => setDuration(Number(event.target.value))} />
1696
+ {error && <p className="text-sm text-rust">{error}</p>}
1697
+ <Button type="submit">Check availability</Button>
1698
+ </form>
1699
+ <div className="grid gap-3">
1700
+ {slots.map((slot) => (
1701
+ <div className="rounded-md border border-line bg-white p-4" key={slot.start}>
1702
+ <p className="font-semibold">{formatDateTime(slot.start)} - {formatDateTime(slot.end)}</p>
1703
+ <p className="mt-1 text-sm text-ink/60">Confidence {slot.confidence} · Competing holds {slot.competing_holds_count}</p>
1704
+ </div>
1705
+ ))}
1706
+ </div>
1707
+ </div>
1708
+ );
1709
+ }
1710
+ ```
1711
+
1712
+ - [ ] **Step 2: Create page**
1713
+
1714
+ `frontend/app/app/availability/page.tsx`:
1715
+
1716
+ ```tsx
1717
+ import { AvailabilityChecker } from '@/components/dashboard/AvailabilityChecker';
1718
+
1719
+ export default function AvailabilityPage() {
1720
+ return (
1721
+ <section className="mx-auto max-w-4xl px-4 py-10">
1722
+ <h1 className="text-4xl font-semibold">Availability</h1>
1723
+ <p className="mt-3 text-ink/70">Check the slots your agent can safely book.</p>
1724
+ <div className="mt-8">
1725
+ <AvailabilityChecker />
1726
+ </div>
1727
+ </section>
1728
+ );
1729
+ }
1730
+ ```
1731
+
1732
+ - [ ] **Step 3: Verify manually**
1733
+
1734
+ Run:
1735
+
1736
+ ```bash
1737
+ docker compose up -d
1738
+ cd frontend
1739
+ npm run dev
1740
+ ```
1741
+
1742
+ Open:
1743
+
1744
+ ```text
1745
+ http://localhost:3001/app/availability
1746
+ ```
1747
+
1748
+ Expected:
1749
+
1750
+ ```text
1751
+ Availability form submits and renders slot cards when a session exists in localStorage.
1752
+ ```
1753
+
1754
+ ---
1755
+
1756
+ ## Task 9: Holds UI
1757
+
1758
+ **Files:**
1759
+ - Create: `frontend/components/dashboard/HoldForm.tsx`
1760
+ - Create: `frontend/app/app/holds/page.tsx`
1761
+
1762
+ - [ ] **Step 1: Create hold form**
1763
+
1764
+ `frontend/components/dashboard/HoldForm.tsx`:
1765
+
1766
+ ```tsx
1767
+ 'use client';
1768
+
1769
+ import { useEffect, useState } from 'react';
1770
+ import { createHold, listHolds, releaseHold } from '@/lib/api';
1771
+ import { fromLocalInputValue, formatDateTime, toLocalInputValue } from '@/lib/format';
1772
+ import { loadSession, type StoredSession } from '@/lib/storage';
1773
+ import { Button } from '@/components/ui/Button';
1774
+ import { Input } from '@/components/ui/Input';
1775
+ import type { Hold } from '@/lib/schemas';
1776
+
1777
+ export function HoldForm() {
1778
+ const [session, setSession] = useState<StoredSession | null>(null);
1779
+ const [holds, setHolds] = useState<Hold[]>([]);
1780
+ const [startAt, setStartAt] = useState(toLocalInputValue(new Date(Date.now() + 24 * 60 * 60 * 1000)));
1781
+ const [endAt, setEndAt] = useState(toLocalInputValue(new Date(Date.now() + 24.5 * 60 * 60 * 1000)));
1782
+ const [reason, setReason] = useState('Manual dashboard booking');
1783
+ const [error, setError] = useState('');
1784
+
1785
+ async function refresh(current: StoredSession) {
1786
+ setHolds(await listHolds(current.apiKey));
1787
+ }
1788
+
1789
+ useEffect(() => {
1790
+ const loaded = loadSession();
1791
+ setSession(loaded);
1792
+ if (loaded) {
1793
+ refresh(loaded).catch(() => setError('Could not load holds'));
1794
+ }
1795
+ }, []);
1796
+
1797
+ async function onSubmit(event: React.FormEvent) {
1798
+ event.preventDefault();
1799
+ if (!session) return;
1800
+ setError('');
1801
+ try {
1802
+ await createHold({
1803
+ apiKey: session.apiKey,
1804
+ agent_id: session.agentId,
1805
+ start_at: fromLocalInputValue(startAt),
1806
+ end_at: fromLocalInputValue(endAt),
1807
+ reason,
1808
+ });
1809
+ await refresh(session);
1810
+ } catch (err) {
1811
+ setError(err instanceof Error ? err.message : 'Could not create hold');
1812
+ }
1813
+ }
1814
+
1815
+ async function onRelease(holdId: string) {
1816
+ if (!session) return;
1817
+ await releaseHold(session.apiKey, holdId);
1818
+ await refresh(session);
1819
+ }
1820
+
1821
+ if (!session) {
1822
+ return <p>Create an agent before managing holds.</p>;
1823
+ }
1824
+
1825
+ return (
1826
+ <div className="grid gap-6">
1827
+ <form className="grid gap-4 rounded-md border border-line bg-white p-5" onSubmit={onSubmit}>
1828
+ <label className="text-sm font-semibold">Start</label>
1829
+ <Input type="datetime-local" value={startAt} onChange={(event) => setStartAt(event.target.value)} />
1830
+ <label className="text-sm font-semibold">End</label>
1831
+ <Input type="datetime-local" value={endAt} onChange={(event) => setEndAt(event.target.value)} />
1832
+ <label className="text-sm font-semibold">Reason</label>
1833
+ <Input value={reason} onChange={(event) => setReason(event.target.value)} />
1834
+ {error && <p className="text-sm text-rust">{error}</p>}
1835
+ <Button type="submit">Create hold</Button>
1836
+ </form>
1837
+ <div className="grid gap-3">
1838
+ {holds.map((hold) => (
1839
+ <div className="flex flex-col justify-between gap-3 rounded-md border border-line bg-white p-4 md:flex-row md:items-center" key={hold.id}>
1840
+ <div>
1841
+ <p className="font-semibold">{formatDateTime(hold.start_at)} - {formatDateTime(hold.end_at)}</p>
1842
+ <p className="mt-1 text-sm text-ink/60">{hold.status} · {hold.reason || 'No reason'}</p>
1843
+ </div>
1844
+ {hold.status === 'confirmed' && (
1845
+ <Button onClick={() => onRelease(hold.id)} variant="secondary">Release</Button>
1846
+ )}
1847
+ </div>
1848
+ ))}
1849
+ </div>
1850
+ </div>
1851
+ );
1852
+ }
1853
+ ```
1854
+
1855
+ - [ ] **Step 2: Create page**
1856
+
1857
+ `frontend/app/app/holds/page.tsx`:
1858
+
1859
+ ```tsx
1860
+ import { HoldForm } from '@/components/dashboard/HoldForm';
1861
+
1862
+ export default function HoldsPage() {
1863
+ return (
1864
+ <section className="mx-auto max-w-4xl px-4 py-10">
1865
+ <h1 className="text-4xl font-semibold">Holds</h1>
1866
+ <p className="mt-3 text-ink/70">Create and release confirmed holds.</p>
1867
+ <div className="mt-8">
1868
+ <HoldForm />
1869
+ </div>
1870
+ </section>
1871
+ );
1872
+ }
1873
+ ```
1874
+
1875
+ - [ ] **Step 3: Verify**
1876
+
1877
+ Run:
1878
+
1879
+ ```bash
1880
+ cd frontend
1881
+ npm run build
1882
+ ```
1883
+
1884
+ Expected:
1885
+
1886
+ ```text
1887
+ Holds page compiles and can create/release holds when backend is running.
1888
+ ```
1889
+
1890
+ ---
1891
+
1892
+ ## Task 10: Agents And Agent Detail Pages
1893
+
1894
+ **Files:**
1895
+ - Create: `frontend/app/app/agents/page.tsx`
1896
+ - Create: `frontend/app/app/agents/[agentId]/page.tsx`
1897
+
1898
+ - [ ] **Step 1: Create agents list page**
1899
+
1900
+ `frontend/app/app/agents/page.tsx`:
1901
+
1902
+ ```tsx
1903
+ 'use client';
1904
+
1905
+ import { useEffect, useState } from 'react';
1906
+ import { listAgents } from '@/lib/api';
1907
+ import { loadSession } from '@/lib/storage';
1908
+ import { Button } from '@/components/ui/Button';
1909
+ import type { Agent } from '@/lib/schemas';
1910
+
1911
+ export default function AgentsPage() {
1912
+ const [agents, setAgents] = useState<Agent[]>([]);
1913
+
1914
+ useEffect(() => {
1915
+ const session = loadSession();
1916
+ if (session) {
1917
+ listAgents(session.orgId).then(setAgents).catch(() => setAgents([]));
1918
+ }
1919
+ }, []);
1920
+
1921
+ return (
1922
+ <section className="mx-auto max-w-5xl px-4 py-10">
1923
+ <div className="flex items-center justify-between gap-4">
1924
+ <div>
1925
+ <h1 className="text-4xl font-semibold">Agents</h1>
1926
+ <p className="mt-3 text-ink/70">AI tools allowed to use Availsync scheduling rules.</p>
1927
+ </div>
1928
+ <Button href="/app/onboarding">Add agent</Button>
1929
+ </div>
1930
+ <div className="mt-8 grid gap-3">
1931
+ {agents.map((agent) => (
1932
+ <a className="rounded-md border border-line bg-white p-4 hover:border-ink" href={`/app/agents/${agent.id}`} key={agent.id}>
1933
+ <p className="font-semibold">{agent.name}</p>
1934
+ <p className="mt-1 text-sm text-ink/60">{agent.agent_type} · priority {agent.priority}</p>
1935
+ </a>
1936
+ ))}
1937
+ </div>
1938
+ </section>
1939
+ );
1940
+ }
1941
+ ```
1942
+
1943
+ - [ ] **Step 2: Create agent detail page**
1944
+
1945
+ `frontend/app/app/agents/[agentId]/page.tsx`:
1946
+
1947
+ ```tsx
1948
+ export default function AgentDetailPage({ params }: { params: { agentId: string } }) {
1949
+ return (
1950
+ <section className="mx-auto max-w-4xl px-4 py-10">
1951
+ <h1 className="text-4xl font-semibold">Agent</h1>
1952
+ <p className="mt-3 text-ink/70">Agent ID: {params.agentId}</p>
1953
+ <div className="mt-8 rounded-md border border-line bg-white p-5">
1954
+ <h2 className="text-xl font-semibold">Next controls</h2>
1955
+ <p className="mt-2 leading-7 text-ink/70">
1956
+ Preferences, API key rotation, and audit event views belong here once backend endpoints expose them fully for the dashboard.
1957
+ </p>
1958
+ </div>
1959
+ </section>
1960
+ );
1961
+ }
1962
+ ```
1963
+
1964
+ - [ ] **Step 3: Verify**
1965
+
1966
+ Run:
1967
+
1968
+ ```bash
1969
+ cd frontend
1970
+ npm run build
1971
+ ```
1972
+
1973
+ Expected:
1974
+
1975
+ ```text
1976
+ Agents routes compile and the list loads after onboarding.
1977
+ ```
1978
+
1979
+ ---
1980
+
1981
+ ## Task 11: Documentation Pages
1982
+
1983
+ **Files:**
1984
+ - Create: `frontend/app/docs/page.tsx`
1985
+ - Create: `frontend/app/docs/quickstart/page.tsx`
1986
+
1987
+ - [ ] **Step 1: Create docs overview**
1988
+
1989
+ `frontend/app/docs/page.tsx`:
1990
+
1991
+ ```tsx
1992
+ import { SiteHeader } from '@/components/marketing/SiteHeader';
1993
+ import { SiteFooter } from '@/components/marketing/SiteFooter';
1994
+ import { Button } from '@/components/ui/Button';
1995
+
1996
+ export default function DocsPage() {
1997
+ return (
1998
+ <>
1999
+ <SiteHeader />
2000
+ <main className="bg-paper">
2001
+ <section className="mx-auto max-w-4xl px-4 py-20">
2002
+ <h1 className="text-5xl font-semibold">Availsync API docs</h1>
2003
+ <p className="mt-5 text-xl leading-8 text-ink/70">
2004
+ Use Availsync as the shared scheduling layer before your AI agents create calendar bookings.
2005
+ </p>
2006
+ <div className="mt-8">
2007
+ <Button href="/docs/quickstart">Open quickstart</Button>
2008
+ </div>
2009
+ </section>
2010
+ </main>
2011
+ <SiteFooter />
2012
+ </>
2013
+ );
2014
+ }
2015
+ ```
2016
+
2017
+ - [ ] **Step 2: Create quickstart**
2018
+
2019
+ `frontend/app/docs/quickstart/page.tsx`:
2020
+
2021
+ ```tsx
2022
+ import { SiteHeader } from '@/components/marketing/SiteHeader';
2023
+ import { SiteFooter } from '@/components/marketing/SiteFooter';
2024
+
2025
+ const snippets = [
2026
+ ['Create org', `curl -X POST http://localhost:3000/v1/orgs \\
2027
+ -H "Content-Type: application/json" \\
2028
+ -d '{"name":"Acme"}'`],
2029
+ ['Create agent', `curl -X POST http://localhost:3000/v1/orgs/ORG_ID/agents \\
2030
+ -H "Content-Type: application/json" \\
2031
+ -d '{"name":"Sales Bot","agent_type":"external_meeting"}'`],
2032
+ ['Check availability', `curl "http://localhost:3000/v1/availability?agent_id=AGENT_ID&from=2026-05-12T09:00:00Z&to=2026-05-12T17:00:00Z&duration_minutes=30" \\
2033
+ -H "Authorization: Bearer API_KEY"`],
2034
+ ['Book hold', `curl -X POST http://localhost:3000/v1/holds \\
2035
+ -H "Authorization: Bearer API_KEY" \\
2036
+ -H "Content-Type: application/json" \\
2037
+ -d '{"agent_id":"AGENT_ID","start_at":"2026-05-12T09:00:00Z","end_at":"2026-05-12T09:30:00Z"}'`],
2038
+ ];
2039
+
2040
+ export default function QuickstartPage() {
2041
+ return (
2042
+ <>
2043
+ <SiteHeader />
2044
+ <main className="bg-paper">
2045
+ <section className="mx-auto max-w-4xl px-4 py-20">
2046
+ <h1 className="text-5xl font-semibold">Quickstart</h1>
2047
+ <p className="mt-5 text-xl leading-8 text-ink/70">Create an org, create an agent, check availability, then book a hold.</p>
2048
+ <div className="mt-10 grid gap-5">
2049
+ {snippets.map(([title, code]) => (
2050
+ <div className="rounded-md border border-line bg-white p-5" key={title}>
2051
+ <h2 className="text-xl font-semibold">{title}</h2>
2052
+ <pre className="mt-4 overflow-x-auto rounded-md bg-ink p-4 text-sm text-white"><code>{code}</code></pre>
2053
+ </div>
2054
+ ))}
2055
+ </div>
2056
+ </section>
2057
+ </main>
2058
+ <SiteFooter />
2059
+ </>
2060
+ );
2061
+ }
2062
+ ```
2063
+
2064
+ - [ ] **Step 3: Verify**
2065
+
2066
+ Run:
2067
+
2068
+ ```bash
2069
+ cd frontend
2070
+ npm run build
2071
+ ```
2072
+
2073
+ Expected:
2074
+
2075
+ ```text
2076
+ Docs pages compile and curl examples are readable on mobile.
2077
+ ```
2078
+
2079
+ ---
2080
+
2081
+ ## Task 12: Backend Billing Stub
2082
+
2083
+ **Files:**
2084
+ - Create: `src/routes/billing.ts`
2085
+ - Modify: `src/index.ts`
2086
+ - Modify: `.env.example`
2087
+ - Modify: `package.json`
2088
+
2089
+ - [ ] **Step 1: Add Stripe dependency**
2090
+
2091
+ Run:
2092
+
2093
+ ```bash
2094
+ npm install stripe
2095
+ ```
2096
+
2097
+ Expected:
2098
+
2099
+ ```text
2100
+ added ... stripe
2101
+ ```
2102
+
2103
+ - [ ] **Step 2: Add env vars**
2104
+
2105
+ Append to `.env.example` without removing existing vars:
2106
+
2107
+ ```text
2108
+ STRIPE_SECRET_KEY=
2109
+ STRIPE_WEBHOOK_SECRET=
2110
+ STRIPE_PRICE_INDIVIDUAL=
2111
+ STRIPE_PRICE_TEAM=
2112
+ FRONTEND_URL=http://localhost:3001
2113
+ ```
2114
+
2115
+ - [ ] **Step 3: Create billing route**
2116
+
2117
+ `src/routes/billing.ts`:
2118
+
2119
+ ```ts
2120
+ import express from 'express';
2121
+ import Stripe from 'stripe';
2122
+ import { z } from 'zod';
2123
+
2124
+ const router = express.Router();
2125
+
2126
+ const CheckoutSchema = z.object({
2127
+ plan: z.enum(['individual', 'team']),
2128
+ org_id: z.string().uuid(),
2129
+ email: z.string().email().optional(),
2130
+ });
2131
+
2132
+ router.post('/billing/checkout', async (req, res) => {
2133
+ const parsed = CheckoutSchema.safeParse(req.body);
2134
+ if (!parsed.success) {
2135
+ return res.status(422).json({ error: 'validation_error', details: parsed.error.flatten() });
2136
+ }
2137
+
2138
+ const secret = process.env.STRIPE_SECRET_KEY;
2139
+ const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3001';
2140
+ const price =
2141
+ parsed.data.plan === 'individual'
2142
+ ? process.env.STRIPE_PRICE_INDIVIDUAL
2143
+ : process.env.STRIPE_PRICE_TEAM;
2144
+
2145
+ if (!secret || !price) {
2146
+ return res.status(501).json({
2147
+ error: 'billing_not_configured',
2148
+ message: 'Stripe is not configured for this environment',
2149
+ });
2150
+ }
2151
+
2152
+ const stripe = new Stripe(secret);
2153
+ const session = await stripe.checkout.sessions.create({
2154
+ mode: 'subscription',
2155
+ customer_email: parsed.data.email,
2156
+ line_items: [{ price, quantity: 1 }],
2157
+ success_url: `${frontendUrl}/app?checkout=success`,
2158
+ cancel_url: `${frontendUrl}/pricing?checkout=cancelled`,
2159
+ metadata: {
2160
+ org_id: parsed.data.org_id,
2161
+ plan: parsed.data.plan,
2162
+ },
2163
+ });
2164
+
2165
+ return res.json({ url: session.url });
2166
+ });
2167
+
2168
+ router.post('/billing/webhook', async (_req, res) => {
2169
+ return res.json({ received: true });
2170
+ });
2171
+
2172
+ router.get('/billing/subscription/:org_id', async (req, res) => {
2173
+ return res.json({
2174
+ org_id: req.params.org_id,
2175
+ status: 'free',
2176
+ plan: 'free',
2177
+ });
2178
+ });
2179
+
2180
+ export default router;
2181
+ ```
2182
+
2183
+ - [ ] **Step 4: Register billing route**
2184
+
2185
+ Modify `src/index.ts`:
2186
+
2187
+ ```ts
2188
+ import billingRouter from './routes/billing';
2189
+ ```
2190
+
2191
+ Then add before other `/v1` route registrations:
2192
+
2193
+ ```ts
2194
+ app.use('/v1', billingRouter);
2195
+ ```
2196
+
2197
+ - [ ] **Step 5: Verify**
2198
+
2199
+ Run:
2200
+
2201
+ ```bash
2202
+ npm run build
2203
+ npm test
2204
+ ```
2205
+
2206
+ Expected:
2207
+
2208
+ ```text
2209
+ Build passes and all existing tests pass.
2210
+ ```
2211
+
2212
+ ---
2213
+
2214
+ ## Task 13: Frontend Checkout Integration
2215
+
2216
+ **Files:**
2217
+ - Modify: `frontend/lib/api.ts`
2218
+ - Modify: `frontend/app/checkout/page.tsx`
2219
+
2220
+ - [ ] **Step 1: Add checkout function**
2221
+
2222
+ Append to `frontend/lib/api.ts`:
2223
+
2224
+ ```ts
2225
+ export async function createCheckout(input: {
2226
+ plan: 'individual' | 'team';
2227
+ org_id: string;
2228
+ email?: string;
2229
+ }) {
2230
+ return request<{ url?: string; error?: string; message?: string }>('/v1/billing/checkout', {
2231
+ method: 'POST',
2232
+ body: JSON.stringify(input),
2233
+ });
2234
+ }
2235
+ ```
2236
+
2237
+ - [ ] **Step 2: Replace checkout page with interactive version**
2238
+
2239
+ `frontend/app/checkout/page.tsx`:
2240
+
2241
+ ```tsx
2242
+ 'use client';
2243
+
2244
+ import { useState } from 'react';
2245
+ import { useSearchParams } from 'next/navigation';
2246
+ import { SiteHeader } from '@/components/marketing/SiteHeader';
2247
+ import { Button } from '@/components/ui/Button';
2248
+ import { Input } from '@/components/ui/Input';
2249
+ import { createCheckout } from '@/lib/api';
2250
+
2251
+ export default function CheckoutPage() {
2252
+ const searchParams = useSearchParams();
2253
+ const plan = searchParams.get('plan') === 'team' ? 'team' : 'individual';
2254
+ const [orgId, setOrgId] = useState('');
2255
+ const [email, setEmail] = useState('');
2256
+ const [message, setMessage] = useState('');
2257
+
2258
+ async function onSubmit(event: React.FormEvent) {
2259
+ event.preventDefault();
2260
+ setMessage('');
2261
+ const response = await createCheckout({ plan, org_id: orgId, email: email || undefined });
2262
+ if (response.url) {
2263
+ window.location.href = response.url;
2264
+ } else {
2265
+ setMessage(response.message || 'Billing is not configured. Continue with free setup.');
2266
+ }
2267
+ }
2268
+
2269
+ return (
2270
+ <>
2271
+ <SiteHeader />
2272
+ <main className="bg-paper">
2273
+ <section className="mx-auto max-w-xl px-4 py-20">
2274
+ <h1 className="text-4xl font-semibold">Checkout for {plan}</h1>
2275
+ <form className="mt-8 grid gap-4 rounded-md border border-line bg-white p-5" onSubmit={onSubmit}>
2276
+ <label className="text-sm font-semibold">Organization ID</label>
2277
+ <Input required value={orgId} onChange={(event) => setOrgId(event.target.value)} />
2278
+ <label className="text-sm font-semibold">Billing email</label>
2279
+ <Input type="email" value={email} onChange={(event) => setEmail(event.target.value)} />
2280
+ {message && <p className="text-sm text-rust">{message}</p>}
2281
+ <Button type="submit">Continue to payment</Button>
2282
+ <Button href={`/signup?plan=${plan}`} variant="secondary">Start setup first</Button>
2283
+ </form>
2284
+ </section>
2285
+ </main>
2286
+ </>
2287
+ );
2288
+ }
2289
+ ```
2290
+
2291
+ - [ ] **Step 3: Verify**
2292
+
2293
+ Run:
2294
+
2295
+ ```bash
2296
+ docker compose up -d --build
2297
+ cd frontend
2298
+ npm run build
2299
+ ```
2300
+
2301
+ Expected:
2302
+
2303
+ ```text
2304
+ Checkout compiles. Without Stripe env vars, submitting checkout shows billing_not_configured message and free setup still works.
2305
+ ```
2306
+
2307
+ ---
2308
+
2309
+ ## Task 14: End-To-End Smoke Test
2310
+
2311
+ **Files:**
2312
+ - Create: `frontend/playwright.config.ts`
2313
+ - Create: `frontend/tests/smoke.spec.ts`
2314
+
2315
+ - [ ] **Step 1: Create Playwright config**
2316
+
2317
+ `frontend/playwright.config.ts`:
2318
+
2319
+ ```ts
2320
+ import { defineConfig, devices } from '@playwright/test';
2321
+
2322
+ export default defineConfig({
2323
+ testDir: './tests',
2324
+ timeout: 30_000,
2325
+ use: {
2326
+ baseURL: 'http://localhost:3001',
2327
+ trace: 'on-first-retry',
2328
+ },
2329
+ projects: [
2330
+ { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
2331
+ { name: 'mobile', use: { ...devices['Pixel 5'] } },
2332
+ ],
2333
+ });
2334
+ ```
2335
+
2336
+ - [ ] **Step 2: Create smoke test**
2337
+
2338
+ `frontend/tests/smoke.spec.ts`:
2339
+
2340
+ ```ts
2341
+ import { expect, test } from '@playwright/test';
2342
+
2343
+ test('buyer can understand product and start onboarding', async ({ page }) => {
2344
+ await page.goto('/');
2345
+ await expect(page.getByRole('heading', { name: /one scheduling source of truth/i })).toBeVisible();
2346
+ await page.getByRole('link', { name: /start free/i }).first().click();
2347
+ await expect(page.getByRole('heading', { name: /create your availsync workspace/i })).toBeVisible();
2348
+ });
2349
+
2350
+ test('pricing shows all plans', async ({ page }) => {
2351
+ await page.goto('/pricing');
2352
+ await expect(page.getByText('Free')).toBeVisible();
2353
+ await expect(page.getByText('Individual')).toBeVisible();
2354
+ await expect(page.getByText('Team')).toBeVisible();
2355
+ });
2356
+
2357
+ test('docs quickstart includes API commands', async ({ page }) => {
2358
+ await page.goto('/docs/quickstart');
2359
+ await expect(page.getByText('Create org')).toBeVisible();
2360
+ await expect(page.getByText('Check availability')).toBeVisible();
2361
+ await expect(page.getByText('Book hold')).toBeVisible();
2362
+ });
2363
+ ```
2364
+
2365
+ - [ ] **Step 3: Run full validation**
2366
+
2367
+ Terminal 1:
2368
+
2369
+ ```bash
2370
+ docker compose up -d --build
2371
+ ```
2372
+
2373
+ Terminal 2:
2374
+
2375
+ ```bash
2376
+ cd frontend
2377
+ npm run dev
2378
+ ```
2379
+
2380
+ Terminal 3:
2381
+
2382
+ ```bash
2383
+ cd frontend
2384
+ npm run build
2385
+ npm run test:e2e
2386
+ ```
2387
+
2388
+ Expected:
2389
+
2390
+ ```text
2391
+ Next build passes. Playwright smoke tests pass on desktop and mobile.
2392
+ ```
2393
+
2394
+ ---
2395
+
2396
+ ## Task 15: Final Acceptance Checklist
2397
+
2398
+ Run every command from repository root unless noted:
2399
+
2400
+ ```bash
2401
+ npm run build
2402
+ npm test
2403
+ docker compose up -d --build
2404
+ bash tests/smoke.sh
2405
+ cd frontend && npm run build
2406
+ cd frontend && npm run test:e2e
2407
+ ```
2408
+
2409
+ Manual browser checks:
2410
+
2411
+ ```text
2412
+ http://localhost:3001
2413
+ http://localhost:3001/pricing
2414
+ http://localhost:3001/signup
2415
+ http://localhost:3001/docs/quickstart
2416
+ http://localhost:3001/app
2417
+ http://localhost:3001/app/availability
2418
+ http://localhost:3001/app/holds
2419
+ ```
2420
+
2421
+ Acceptance criteria:
2422
+
2423
+ - [ ] Homepage explains Availsync as the scheduling source of truth for AI agents.
2424
+ - [ ] Pricing page shows Free, Individual, and Team plans.
2425
+ - [ ] Signup creates an org through the backend.
2426
+ - [ ] Onboarding creates an agent and availability window through the backend.
2427
+ - [ ] API key is shown once and stored locally for the MVP dashboard.
2428
+ - [ ] Dashboard shows connected workspace state.
2429
+ - [ ] Availability checker calls `GET /v1/availability`.
2430
+ - [ ] Holds page calls `POST /v1/holds`, `GET /v1/holds`, and `DELETE /v1/holds/:id`.
2431
+ - [ ] Quickstart docs include curl examples for org, agent, availability, and hold creation.
2432
+ - [ ] Stripe checkout path has a configured path and a safe not-configured fallback.
2433
+ - [ ] Desktop and mobile layouts have no overlapping text or clipped buttons.
2434
+ - [ ] Backend build and tests remain green.
2435
+ - [ ] Backend smoke tests remain green.
2436
+ - [ ] Frontend build and Playwright smoke tests are green.
2437
+
2438
+ ---
2439
+
2440
+ ## Execution Notes
2441
+
2442
+ Start by implementing Tasks 1-5 to get the public buying surface live. Then implement Tasks 6-11 so a buyer can experience the product with the existing backend. Add billing only after the free onboarding path works end to end.
2443
+
2444
+ Use the existing backend on `http://localhost:3000`. Run the frontend on `http://localhost:3001` to avoid colliding with the API.
2445
+