@pylonsync/create-pylon 0.11.1 → 0.11.4

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 (142) hide show
  1. package/bin/create-pylon.js +1 -1
  2. package/package.json +1 -1
  3. package/templates/_root/AGENTS.md +1 -1
  4. package/templates/agency/AGENTS.md +1 -1
  5. package/templates/agency/app/(marketing)/contact-form.tsx +22 -32
  6. package/templates/agency/app/(marketing)/layout.tsx +36 -64
  7. package/templates/agency/app/(marketing)/not-found.tsx +6 -8
  8. package/templates/agency/app/(marketing)/page.tsx +93 -144
  9. package/templates/agency/app/(marketing)/work/[slug]/page.tsx +69 -71
  10. package/templates/agency/app/(marketing)/work/page.tsx +20 -17
  11. package/templates/agency/app/globals.css +15 -6
  12. package/templates/agency/app.ts +10 -0
  13. package/templates/agency/components/marketing.tsx +40 -104
  14. package/templates/agency/lib/site.config.ts +34 -52
  15. package/templates/agency/public/images/hero.jpg +0 -0
  16. package/templates/agency/public/images/team/claire-donovan.jpg +0 -0
  17. package/templates/agency/public/images/team/dana-okafor.jpg +0 -0
  18. package/templates/agency/public/images/team/marcus-lee.jpg +0 -0
  19. package/templates/agency/public/images/work/atlas-health.jpg +0 -0
  20. package/templates/agency/public/images/work/cohort.jpg +0 -0
  21. package/templates/agency/public/images/work/ledger.jpg +0 -0
  22. package/templates/agency/public/images/work/vela.jpg +0 -0
  23. package/templates/ai-chat/AGENTS.md +1 -1
  24. package/templates/ai-studio/AGENTS.md +1 -1
  25. package/templates/barebones/AGENTS.md +1 -1
  26. package/templates/chat/AGENTS.md +1 -1
  27. package/templates/consumer/AGENTS.md +1 -1
  28. package/templates/consumer/README.md +39 -23
  29. package/templates/consumer/app/(app)/explore/page.tsx +13 -0
  30. package/templates/consumer/app/(app)/layout.tsx +17 -0
  31. package/templates/consumer/app/(app)/p/[id]/page.tsx +13 -0
  32. package/templates/consumer/app/(app)/page.tsx +15 -0
  33. package/templates/consumer/app/(app)/u/[username]/page.tsx +14 -0
  34. package/templates/consumer/app/error.tsx +1 -1
  35. package/templates/consumer/app/globals.css +25 -0
  36. package/templates/consumer/app/layout.tsx +3 -10
  37. package/templates/consumer/app/llms.ts +10 -10
  38. package/templates/consumer/app/not-found.tsx +9 -22
  39. package/templates/consumer/app/robots.ts +3 -4
  40. package/templates/consumer/app/sitemap.ts +7 -21
  41. package/templates/consumer/app.ts +152 -33
  42. package/templates/consumer/components/social/app-chrome.tsx +109 -0
  43. package/templates/consumer/components/social/avatar.tsx +50 -0
  44. package/templates/consumer/components/social/comment-form.tsx +51 -0
  45. package/templates/consumer/components/social/create-post.tsx +155 -0
  46. package/templates/consumer/components/social/edit-profile.tsx +103 -0
  47. package/templates/consumer/components/social/explore-screen.tsx +23 -0
  48. package/templates/consumer/components/social/home-screen.tsx +161 -0
  49. package/templates/consumer/components/social/post-card.tsx +77 -0
  50. package/templates/consumer/components/social/post-grid.tsx +46 -0
  51. package/templates/consumer/components/social/post-parts.tsx +146 -0
  52. package/templates/consumer/components/social/post-screen.tsx +173 -0
  53. package/templates/consumer/components/social/profile-screen.tsx +146 -0
  54. package/templates/consumer/components/social/rich-text.tsx +22 -0
  55. package/templates/consumer/components/social/session.tsx +43 -0
  56. package/templates/consumer/components/social/use-social.ts +86 -0
  57. package/templates/consumer/functions/addComment.ts +27 -0
  58. package/templates/consumer/functions/createPost.ts +39 -0
  59. package/templates/consumer/functions/ensureProfile.ts +39 -0
  60. package/templates/consumer/functions/seedFeed.ts +38 -0
  61. package/templates/consumer/functions/updateProfile.ts +61 -0
  62. package/templates/consumer/lib/seed.ts +192 -0
  63. package/templates/consumer/lib/site.ts +6 -0
  64. package/templates/consumer/lib/social.ts +245 -0
  65. package/templates/consumer/tests/social.test.ts +157 -0
  66. package/templates/creator/AGENTS.md +1 -1
  67. package/templates/creator/app/(marketing)/layout.tsx +35 -69
  68. package/templates/creator/app/(marketing)/newsletter-signup.tsx +15 -26
  69. package/templates/creator/app/(marketing)/not-found.tsx +8 -6
  70. package/templates/creator/app/(marketing)/page.tsx +90 -106
  71. package/templates/creator/app/globals.css +14 -0
  72. package/templates/creator/app/layout.tsx +1 -0
  73. package/templates/creator/app.ts +10 -0
  74. package/templates/creator/components/marketing.tsx +28 -89
  75. package/templates/creator/lib/site.config.ts +51 -29
  76. package/templates/creator/public/images/headshot.jpg +0 -0
  77. package/templates/crm/AGENTS.md +1 -1
  78. package/templates/crm/lib/seed.ts +153 -21
  79. package/templates/directory/AGENTS.md +1 -1
  80. package/templates/directory/app/(marketing)/directory-browse.tsx +99 -137
  81. package/templates/directory/app/(marketing)/layout.tsx +37 -88
  82. package/templates/directory/app/(marketing)/not-found.tsx +11 -10
  83. package/templates/directory/app/(marketing)/page.tsx +12 -33
  84. package/templates/directory/app/(marketing)/submit/page.tsx +9 -10
  85. package/templates/directory/app/(marketing)/submit-form.tsx +26 -26
  86. package/templates/directory/app/globals.css +15 -4
  87. package/templates/directory/app.ts +10 -0
  88. package/templates/directory/components/marketing.tsx +27 -91
  89. package/templates/directory/lib/site.config.ts +13 -24
  90. package/templates/helpdesk/AGENTS.md +1 -1
  91. package/templates/helpdesk/lib/seed.ts +188 -104
  92. package/templates/inventory/AGENTS.md +1 -1
  93. package/templates/inventory/lib/seed.ts +175 -44
  94. package/templates/invoices/AGENTS.md +1 -1
  95. package/templates/invoices/lib/seed.ts +74 -89
  96. package/templates/local-service/AGENTS.md +1 -1
  97. package/templates/local-service/app/(marketing)/booking-widget.tsx +31 -31
  98. package/templates/local-service/app/(marketing)/layout.tsx +25 -37
  99. package/templates/local-service/app/(marketing)/not-found.tsx +4 -4
  100. package/templates/local-service/app/(marketing)/page.tsx +145 -139
  101. package/templates/local-service/app/globals.css +11 -0
  102. package/templates/local-service/app/layout.tsx +2 -0
  103. package/templates/local-service/app.ts +9 -0
  104. package/templates/local-service/components/marketing.tsx +14 -39
  105. package/templates/local-service/lib/site.config.ts +16 -23
  106. package/templates/local-service/public/images/hero.jpg +0 -0
  107. package/templates/marketplace/AGENTS.md +1 -1
  108. package/templates/marketplace/client/SellForm.tsx +2 -0
  109. package/templates/projects/AGENTS.md +1 -1
  110. package/templates/projects/lib/seed.ts +103 -32
  111. package/templates/restaurant/AGENTS.md +1 -1
  112. package/templates/restaurant/app/(marketing)/layout.tsx +26 -48
  113. package/templates/restaurant/app/(marketing)/page.tsx +146 -123
  114. package/templates/restaurant/app/globals.css +11 -0
  115. package/templates/restaurant/app/layout.tsx +1 -0
  116. package/templates/restaurant/app.ts +10 -0
  117. package/templates/restaurant/components/marketing.tsx +10 -0
  118. package/templates/restaurant/lib/site.config.ts +16 -14
  119. package/templates/restaurant/public/images/bar.jpg +0 -0
  120. package/templates/restaurant/public/images/branzino.jpg +0 -0
  121. package/templates/restaurant/public/images/hearth.jpg +0 -0
  122. package/templates/restaurant/public/images/hero.jpg +0 -0
  123. package/templates/saas/AGENTS.md +1 -1
  124. package/templates/shop/AGENTS.md +1 -1
  125. package/templates/shop/public/images/products/cedar-smoke.jpg +0 -0
  126. package/templates/shop/public/images/products/linen.jpg +0 -0
  127. package/templates/todo/AGENTS.md +1 -1
  128. package/templates/waitlist/AGENTS.md +1 -1
  129. package/templates/waitlist/app/(marketing)/layout.tsx +39 -85
  130. package/templates/waitlist/app/(marketing)/not-found.tsx +8 -9
  131. package/templates/waitlist/app/(marketing)/page.tsx +51 -86
  132. package/templates/waitlist/app/(marketing)/waitlist-hero.tsx +43 -83
  133. package/templates/waitlist/app/globals.css +23 -0
  134. package/templates/waitlist/app/layout.tsx +1 -0
  135. package/templates/waitlist/app.ts +18 -0
  136. package/templates/waitlist/components/marketing.tsx +67 -81
  137. package/templates/waitlist/lib/site.config.ts +52 -59
  138. package/templates/consumer/app/feed-client.tsx +0 -159
  139. package/templates/consumer/app/page.tsx +0 -27
  140. package/templates/consumer/functions/_keep.ts +0 -13
  141. package/templates/consumer/functions/seedPosts.ts +0 -56
  142. package/templates/consumer/tests/example.test.ts +0 -12
@@ -65,7 +65,7 @@ this pass before the user has to ask.
65
65
  - **`ctx.llm.stream(request, onEvent)` streams tokens as they generate** and still resolves with the full response, so `stop_reason === "tool_use"` drives an agent tool loop. Events are `text_delta` / `tool_use_start` / `tool_input_delta` / `done`. Same auth + model-allowlist gating as `ctx.llm.complete`. Streaming does NOT extend the call deadline (`PYLON_FN_CALL_TIMEOUT`, 30s default) — set `timeout: <secs>` on the def for a long run.
66
66
  - **`ctx.stream.write(text)` streams to the caller — and every fn stream is RESUMABLE**: the server buffers frames under a stream id (`streamFn`'s `onStreamId`), so a dropped connection or closed tab catches up via `resumeStream(id)`, including the final result after the handler finished. `ctx.rooms.broadcast(room, topic, data)` fans out to every CURRENTLY-CONNECTED subscriber (second device, second tab) but does not replay missed messages — use the stream id for anything that must survive a gap. Broadcast resolves `{ delivered: false }` when the room has no members — a no-op, not an error. Clients receive with `useRoomMessages(room, cb)` from `@pylonsync/react`; `useRoom` is the send side.
67
67
  - **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
68
- - **Use the supported file and scheduling APIs.** Files go through `<FileUpload>` and `/api/files/*`; there is no `ctx.files`. One-shot work uses `ctx.scheduler.runAfter`, `runAt`, or `cancel`; there is no `defineWorkflow` or `defineJob`. Recurring work uses `cron("0 * * * *", "fnName")` in `buildManifest({ crons: [...] })`, imported from `@pylonsync/sdk`. Make the target function `internal: true`. It runs with anonymous auth, but its own `ctx.db.*` calls are server-side and bypass policies. Use `ctx.auth.elevate({ admin: true, reason: "..." })`, with a mandatory reason, only when chaining another internal function through `ctx.scheduler`.
68
+ - **Use the supported file and scheduling APIs.** Files go through `<FileUpload>` and `/api/files/*`; an upload is readable by its uploader only, so pass `visibility: "public"` to `uploadFile` or `<FileUpload>` for files every visitor sees; there is no `ctx.files`. One-shot work uses `ctx.scheduler.runAfter`, `runAt`, or `cancel`; there is no `defineWorkflow` or `defineJob`. Recurring work uses `cron("0 * * * *", "fnName")` in `buildManifest({ crons: [...] })`, imported from `@pylonsync/sdk`. Make the target function `internal: true`. It runs with anonymous auth, but its own `ctx.db.*` calls are server-side and bypass policies. Use `ctx.auth.elevate({ admin: true, reason: "..." })`, with a mandatory reason, only when chaining another internal function through `ctx.scheduler`.
69
69
 
70
70
  ## Testing
71
71
 
@@ -39,10 +39,12 @@ async function uploadListingPhoto(file: File) {
39
39
  const initResponse = await fetch("/api/files/init", {
40
40
  method: "POST",
41
41
  headers: { "Content-Type": "application/json" },
42
+ // Every buyer sees the listing photo, signed in or not.
42
43
  body: JSON.stringify({
43
44
  filename: file.name,
44
45
  mimeType: file.type,
45
46
  size: file.size,
47
+ visibility: "public",
46
48
  }),
47
49
  });
48
50
  if (!initResponse.ok) throw new Error("Could not prepare that upload.");
@@ -65,7 +65,7 @@ this pass before the user has to ask.
65
65
  - **`ctx.llm.stream(request, onEvent)` streams tokens as they generate** and still resolves with the full response, so `stop_reason === "tool_use"` drives an agent tool loop. Events are `text_delta` / `tool_use_start` / `tool_input_delta` / `done`. Same auth + model-allowlist gating as `ctx.llm.complete`. Streaming does NOT extend the call deadline (`PYLON_FN_CALL_TIMEOUT`, 30s default) — set `timeout: <secs>` on the def for a long run.
66
66
  - **`ctx.stream.write(text)` streams to the caller — and every fn stream is RESUMABLE**: the server buffers frames under a stream id (`streamFn`'s `onStreamId`), so a dropped connection or closed tab catches up via `resumeStream(id)`, including the final result after the handler finished. `ctx.rooms.broadcast(room, topic, data)` fans out to every CURRENTLY-CONNECTED subscriber (second device, second tab) but does not replay missed messages — use the stream id for anything that must survive a gap. Broadcast resolves `{ delivered: false }` when the room has no members — a no-op, not an error. Clients receive with `useRoomMessages(room, cb)` from `@pylonsync/react`; `useRoom` is the send side.
67
67
  - **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
68
- - **Use the supported file and scheduling APIs.** Files go through `<FileUpload>` and `/api/files/*`; there is no `ctx.files`. One-shot work uses `ctx.scheduler.runAfter`, `runAt`, or `cancel`; there is no `defineWorkflow` or `defineJob`. Recurring work uses `cron("0 * * * *", "fnName")` in `buildManifest({ crons: [...] })`, imported from `@pylonsync/sdk`. Make the target function `internal: true`. It runs with anonymous auth, but its own `ctx.db.*` calls are server-side and bypass policies. Use `ctx.auth.elevate({ admin: true, reason: "..." })`, with a mandatory reason, only when chaining another internal function through `ctx.scheduler`.
68
+ - **Use the supported file and scheduling APIs.** Files go through `<FileUpload>` and `/api/files/*`; an upload is readable by its uploader only, so pass `visibility: "public"` to `uploadFile` or `<FileUpload>` for files every visitor sees; there is no `ctx.files`. One-shot work uses `ctx.scheduler.runAfter`, `runAt`, or `cancel`; there is no `defineWorkflow` or `defineJob`. Recurring work uses `cron("0 * * * *", "fnName")` in `buildManifest({ crons: [...] })`, imported from `@pylonsync/sdk`. Make the target function `internal: true`. It runs with anonymous auth, but its own `ctx.db.*` calls are server-side and bypass policies. Use `ctx.auth.elevate({ admin: true, reason: "..." })`, with a mandatory reason, only when chaining another internal function through `ctx.scheduler`.
69
69
 
70
70
  ## Testing
71
71
 
@@ -1,8 +1,9 @@
1
1
  // Demo data for a brand-new delivery workspace.
2
2
  //
3
- // An empty project list shows nothing about boards, budgets, or time. The first
4
- // sign-in seeds two live projects — one comfortably inside its budget, one over
5
- // it — so the budget states are visible rather than theoretical.
3
+ // The first sign-in seeds a working studio: eight clients, ten projects in
4
+ // every status, sixty tasks, and the time logged against them. Two active
5
+ // projects are over budget, so the budget states are visible rather than
6
+ // theoretical.
6
7
 
7
8
  export interface SeedClient {
8
9
  key: string;
@@ -19,6 +20,8 @@ export interface SeedProject {
19
20
  budgetHours: number;
20
21
  hourlyRate: number;
21
22
  dueInDays: number;
23
+ /** Days since the project was created. */
24
+ age: number;
22
25
  }
23
26
 
24
27
  export interface SeedTask {
@@ -27,49 +30,108 @@ export interface SeedTask {
27
30
  title: string;
28
31
  status: string;
29
32
  estimateHours: number;
30
- /** Hours logged against it; each becomes one or more entries. */
33
+ /** Hours logged against it; each becomes one entry. */
31
34
  loggedHours: number[];
32
35
  }
33
36
 
34
37
  export const SEED_CLIENTS: SeedClient[] = [
35
38
  { key: "northwind", name: "Northwind Logistics", email: "dana@northwind.co" },
36
39
  { key: "hallmark", name: "Hallmark Dental", email: "priya@hallmarkdental.com" },
40
+ { key: "larkspur", name: "Larkspur Clinics", email: "kdoyle@larkspurclinics.com" },
41
+ { key: "meridian", name: "Meridian Freight", email: "rstone@meridianfreight.com" },
42
+ { key: "bluefin", name: "Bluefin Analytics", email: "elena@bluefin.io" },
43
+ { key: "harbor", name: "Harbor Point Hotels", email: "rchen@harborpointhotels.com" },
44
+ { key: "kestrel", name: "Kestrel Aviation Services", email: "jwhitaker@kestrelaviation.com" },
45
+ { key: "orchard", name: "Orchard Learning", email: "dpark@orchardlearning.org" },
37
46
  ];
38
47
 
39
48
  export const SEED_PROJECTS: SeedProject[] = [
40
- {
41
- key: "dispatch",
42
- client: "northwind",
43
- name: "Dispatch portal",
44
- status: "active",
45
- budgetHours: 80,
46
- hourlyRate: 165,
47
- dueInDays: 24,
48
- },
49
- // Over budget: 40h agreed, ~46h logged.
50
- {
51
- key: "intake",
52
- client: "hallmark",
53
- name: "Patient intake rebuild",
54
- status: "active",
55
- budgetHours: 40,
56
- hourlyRate: 150,
57
- dueInDays: 9,
58
- },
49
+ { key: "dispatch", client: "northwind", name: "Dispatch portal", status: "active", budgetHours: 120, hourlyRate: 165, dueInDays: 24, age: 45 },
50
+ // Over budget: 40h agreed, about 50h logged.
51
+ { key: "intake", client: "hallmark", name: "Patient intake rebuild", status: "active", budgetHours: 40, hourlyRate: 150, dueInDays: 9, age: 38 },
52
+ { key: "scheduling", client: "larkspur", name: "Multi-site scheduling", status: "active", budgetHours: 200, hourlyRate: 165, dueInDays: 60, age: 30 },
53
+ { key: "onboarding", client: "meridian", name: "Carrier onboarding workflow", status: "active", budgetHours: 160, hourlyRate: 165, dueInDays: 41, age: 52 },
54
+ // Over budget: 60h agreed, about 68h logged.
55
+ { key: "reporting", client: "bluefin", name: "Embedded reporting", status: "active", budgetHours: 60, hourlyRate: 175, dueInDays: 5, age: 40 },
56
+ { key: "messaging", client: "harbor", name: "Guest messaging pilot", status: "active", budgetHours: 90, hourlyRate: 160, dueInDays: 18, age: 28 },
57
+ { key: "maintenance", client: "kestrel", name: "Maintenance log digitisation", status: "paused", budgetHours: 240, hourlyRate: 165, dueInDays: 120, age: 20 },
58
+ { key: "parents", client: "orchard", name: "Parent communication hub", status: "paused", budgetHours: 80, hourlyRate: 140, dueInDays: 75, age: 33 },
59
+ { key: "parts", client: "kestrel", name: "Parts inventory system", status: "complete", budgetHours: 180, hourlyRate: 165, dueInDays: -35, age: 150 },
60
+ { key: "referrals", client: "larkspur", name: "Referral tracking", status: "complete", budgetHours: 100, hourlyRate: 165, dueInDays: -20, age: 110 },
59
61
  ];
60
62
 
61
63
  export const SEED_TASKS: SeedTask[] = [
62
64
  { key: "d-auth", project: "dispatch", title: "SSO with their identity provider", status: "done", estimateHours: 8, loggedHours: [3, 4.5] },
63
- { key: "d-model", project: "dispatch", title: "Route + stop data model", status: "done", estimateHours: 6, loggedHours: [5] },
64
- { key: "d-board", project: "dispatch", title: "Dispatcher board UI", status: "doing", estimateHours: 20, loggedHours: [6, 4] },
65
- { key: "d-mobile", project: "dispatch", title: "Driver mobile view", status: "review", estimateHours: 12, loggedHours: [9] },
65
+ { key: "d-model", project: "dispatch", title: "Route and stop data model", status: "done", estimateHours: 6, loggedHours: [5] },
66
+ { key: "d-map", project: "dispatch", title: "Map tiles and stop markers", status: "done", estimateHours: 10, loggedHours: [4, 6, 1.5] },
67
+ { key: "d-board", project: "dispatch", title: "Dispatcher board UI", status: "doing", estimateHours: 20, loggedHours: [6, 4, 5] },
68
+ { key: "d-assign", project: "dispatch", title: "Drag stops between drivers", status: "doing", estimateHours: 8, loggedHours: [3] },
69
+ { key: "d-mobile", project: "dispatch", title: "Driver mobile view", status: "review", estimateHours: 12, loggedHours: [9, 2] },
66
70
  { key: "d-report", project: "dispatch", title: "Weekly utilisation report", status: "todo", estimateHours: 6, loggedHours: [] },
67
71
  { key: "d-import", project: "dispatch", title: "Import legacy stop history", status: "todo", estimateHours: 10, loggedHours: [] },
72
+ { key: "d-alerts", project: "dispatch", title: "Late delivery alerts", status: "todo", estimateHours: 6, loggedHours: [] },
68
73
 
69
74
  { key: "i-forms", project: "intake", title: "Form builder", status: "done", estimateHours: 16, loggedHours: [8, 7, 4] },
70
75
  { key: "i-sig", project: "intake", title: "Consent signatures", status: "done", estimateHours: 8, loggedHours: [6, 5] },
76
+ { key: "i-insurance", project: "intake", title: "Insurance card capture", status: "done", estimateHours: 6, loggedHours: [4, 3.5] },
71
77
  { key: "i-sync", project: "intake", title: "Sync to practice software", status: "doing", estimateHours: 12, loggedHours: [9, 7] },
78
+ { key: "i-kiosk", project: "intake", title: "Waiting room kiosk mode", status: "review", estimateHours: 4, loggedHours: [5] },
72
79
  { key: "i-train", project: "intake", title: "Staff training session", status: "todo", estimateHours: 4, loggedHours: [] },
80
+
81
+ { key: "s-discovery", project: "scheduling", title: "Site visits and workflow mapping", status: "done", estimateHours: 18, loggedHours: [6, 6, 7] },
82
+ { key: "s-model", project: "scheduling", title: "Practitioner, room, and slot model", status: "done", estimateHours: 10, loggedHours: [5, 4] },
83
+ { key: "s-calendar", project: "scheduling", title: "Week calendar with room lanes", status: "doing", estimateHours: 30, loggedHours: [7, 6, 8] },
84
+ { key: "s-rules", project: "scheduling", title: "Booking rules per clinic", status: "doing", estimateHours: 16, loggedHours: [4] },
85
+ { key: "s-reminders", project: "scheduling", title: "SMS reminders", status: "todo", estimateHours: 8, loggedHours: [] },
86
+ { key: "s-waitlist", project: "scheduling", title: "Cancellation waitlist", status: "todo", estimateHours: 12, loggedHours: [] },
87
+ { key: "s-reports", project: "scheduling", title: "Utilisation by site", status: "todo", estimateHours: 8, loggedHours: [] },
88
+ { key: "s-migration", project: "scheduling", title: "Import the paper diaries", status: "todo", estimateHours: 20, loggedHours: [] },
89
+
90
+ { key: "o-checklist", project: "onboarding", title: "Digitise the 47-step checklist", status: "done", estimateHours: 12, loggedHours: [6, 6.5] },
91
+ { key: "o-docs", project: "onboarding", title: "Document upload and expiry", status: "done", estimateHours: 14, loggedHours: [5, 6, 4] },
92
+ { key: "o-approvals", project: "onboarding", title: "Approval chain", status: "doing", estimateHours: 16, loggedHours: [7, 3] },
93
+ { key: "o-portal", project: "onboarding", title: "Carrier self-service portal", status: "doing", estimateHours: 24, loggedHours: [8, 6] },
94
+ { key: "o-tms", project: "onboarding", title: "Push approved carriers to the TMS", status: "review", estimateHours: 10, loggedHours: [9] },
95
+ { key: "o-audit", project: "onboarding", title: "Audit trail export", status: "todo", estimateHours: 6, loggedHours: [] },
96
+ { key: "o-insurance", project: "onboarding", title: "Insurance certificate verification", status: "todo", estimateHours: 12, loggedHours: [] },
97
+
98
+ { key: "r-embed", project: "reporting", title: "Embed SDK", status: "done", estimateHours: 12, loggedHours: [7, 6] },
99
+ { key: "r-charts", project: "reporting", title: "Chart components", status: "done", estimateHours: 16, loggedHours: [8, 9, 3] },
100
+ { key: "r-filters", project: "reporting", title: "Cross-chart filters", status: "done", estimateHours: 10, loggedHours: [6, 7] },
101
+ { key: "r-perms", project: "reporting", title: "Row-level permissions", status: "doing", estimateHours: 14, loggedHours: [9, 8] },
102
+ { key: "r-export", project: "reporting", title: "PDF and CSV export", status: "review", estimateHours: 6, loggedHours: [5] },
103
+ { key: "r-docs", project: "reporting", title: "Integration guide", status: "todo", estimateHours: 4, loggedHours: [] },
104
+
105
+ { key: "m-sms", project: "messaging", title: "SMS channel", status: "done", estimateHours: 10, loggedHours: [6, 4.5] },
106
+ { key: "m-whatsapp", project: "messaging", title: "WhatsApp channel", status: "done", estimateHours: 12, loggedHours: [7, 6] },
107
+ { key: "m-inbox", project: "messaging", title: "Shared inbox for front desk", status: "doing", estimateHours: 20, loggedHours: [8, 7] },
108
+ { key: "m-templates", project: "messaging", title: "Message templates", status: "doing", estimateHours: 6, loggedHours: [2] },
109
+ { key: "m-pms", project: "messaging", title: "Link guests from the PMS", status: "review", estimateHours: 8, loggedHours: [7] },
110
+ { key: "m-rollout", project: "messaging", title: "Roll out to the two Boston properties", status: "todo", estimateHours: 8, loggedHours: [] },
111
+ { key: "m-metrics", project: "messaging", title: "Response time dashboard", status: "todo", estimateHours: 6, loggedHours: [] },
112
+
113
+ { key: "k-discovery", project: "maintenance", title: "Part 145 compliance review", status: "done", estimateHours: 16, loggedHours: [8, 8] },
114
+ { key: "k-model", project: "maintenance", title: "Work order and sign-off model", status: "doing", estimateHours: 12, loggedHours: [4] },
115
+ { key: "k-forms", project: "maintenance", title: "Inspection forms", status: "todo", estimateHours: 30, loggedHours: [] },
116
+ { key: "k-signatures", project: "maintenance", title: "Technician signatures", status: "todo", estimateHours: 10, loggedHours: [] },
117
+ { key: "k-archive", project: "maintenance", title: "Scan and index paper logs", status: "todo", estimateHours: 40, loggedHours: [] },
118
+
119
+ { key: "p-pilot", project: "parents", title: "Pilot scope with two schools", status: "done", estimateHours: 6, loggedHours: [3, 3] },
120
+ { key: "p-announce", project: "parents", title: "Announcements feed", status: "doing", estimateHours: 12, loggedHours: [5] },
121
+ { key: "p-consent", project: "parents", title: "Trip consent forms", status: "todo", estimateHours: 10, loggedHours: [] },
122
+ { key: "p-translate", project: "parents", title: "Translations for the top five languages", status: "todo", estimateHours: 8, loggedHours: [] },
123
+
124
+ { key: "pi-model", project: "parts", title: "Parts and location model", status: "done", estimateHours: 12, loggedHours: [6, 6] },
125
+ { key: "pi-barcode", project: "parts", title: "Barcode scanning", status: "done", estimateHours: 20, loggedHours: [8, 7, 6] },
126
+ { key: "pi-reorder", project: "parts", title: "Reorder points and alerts", status: "done", estimateHours: 10, loggedHours: [5, 5] },
127
+ { key: "pi-import", project: "parts", title: "Import the vendor catalogue", status: "done", estimateHours: 16, loggedHours: [8, 9] },
128
+ { key: "pi-reports", project: "parts", title: "Usage reports", status: "done", estimateHours: 8, loggedHours: [4, 4.5] },
129
+ { key: "pi-training", project: "parts", title: "Stores team training", status: "done", estimateHours: 4, loggedHours: [4] },
130
+
131
+ { key: "rf-model", project: "referrals", title: "Referral lifecycle model", status: "done", estimateHours: 8, loggedHours: [4, 4] },
132
+ { key: "rf-intake", project: "referrals", title: "Fax and email intake", status: "done", estimateHours: 14, loggedHours: [7, 8] },
133
+ { key: "rf-tracking", project: "referrals", title: "Status board per clinic", status: "done", estimateHours: 16, loggedHours: [8, 6, 3] },
134
+ { key: "rf-reports", project: "referrals", title: "Turnaround reports", status: "done", estimateHours: 8, loggedHours: [4, 5] },
73
135
  ];
74
136
 
75
137
  export interface ShapedSeed {
@@ -81,6 +143,7 @@ export interface ShapedSeed {
81
143
 
82
144
  export function shapeSeed(now: number = Date.now()): ShapedSeed {
83
145
  const at = (days: number) => new Date(now + days * 86_400_000).toISOString();
146
+ const projectAge = new Map(SEED_PROJECTS.map((p) => [p.key, p.age]));
84
147
 
85
148
  const tasks: ShapedSeed["tasks"] = [];
86
149
  const entries: ShapedSeed["entries"] = [];
@@ -91,6 +154,11 @@ export function shapeSeed(now: number = Date.now()): ShapedSeed {
91
154
  const position = positions.get(columnKey) ?? 0;
92
155
  positions.set(columnKey, position + 1);
93
156
 
157
+ const age = projectAge.get(task.project) ?? 30;
158
+ // Tasks were created over the first half of the project, in order.
159
+ const created = -age + (index % 6) * Math.max(1, Math.floor(age / 12));
160
+ const lastTouched = task.status === "todo" ? created : -Math.min(age, 1 + (index % 9));
161
+
94
162
  tasks.push({
95
163
  key: task.key,
96
164
  project: task.project,
@@ -99,11 +167,14 @@ export function shapeSeed(now: number = Date.now()): ShapedSeed {
99
167
  status: task.status,
100
168
  estimateMinutes: Math.round(task.estimateHours * 60),
101
169
  position,
102
- createdAt: at(-30 + index),
103
- updatedAt: at(-2),
170
+ createdAt: at(created),
171
+ updatedAt: at(lastTouched),
104
172
  },
105
173
  });
106
174
 
175
+ // Entries spread from creation towards today, one every few days.
176
+ const span = Math.max(1, -created - 1);
177
+ const step = task.loggedHours.length > 1 ? span / task.loggedHours.length : 0;
107
178
  task.loggedHours.forEach((hours, entryIndex) => {
108
179
  entries.push({
109
180
  task: task.key,
@@ -111,16 +182,16 @@ export function shapeSeed(now: number = Date.now()): ShapedSeed {
111
182
  row: {
112
183
  minutes: Math.round(hours * 60),
113
184
  note: null,
114
- spentOn: at(-14 + entryIndex * 2),
185
+ spentOn: at(Math.round(created + 1 + step * entryIndex)),
115
186
  },
116
187
  });
117
188
  });
118
189
  });
119
190
 
120
191
  return {
121
- clients: SEED_CLIENTS.map((c) => ({
192
+ clients: SEED_CLIENTS.map((c, index) => ({
122
193
  key: c.key,
123
- row: { name: c.name, email: c.email, createdAt: at(-90) },
194
+ row: { name: c.name, email: c.email, createdAt: at(-160 + index * 10) },
124
195
  })),
125
196
  projects: SEED_PROJECTS.map((p) => ({
126
197
  key: p.key,
@@ -131,7 +202,7 @@ export function shapeSeed(now: number = Date.now()): ShapedSeed {
131
202
  budgetMinutes: Math.round(p.budgetHours * 60),
132
203
  hourlyRateCents: Math.round(p.hourlyRate * 100),
133
204
  dueDate: at(p.dueInDays),
134
- createdAt: at(-45),
205
+ createdAt: at(-p.age),
135
206
  },
136
207
  })),
137
208
  tasks,
@@ -65,7 +65,7 @@ this pass before the user has to ask.
65
65
  - **`ctx.llm.stream(request, onEvent)` streams tokens as they generate** and still resolves with the full response, so `stop_reason === "tool_use"` drives an agent tool loop. Events are `text_delta` / `tool_use_start` / `tool_input_delta` / `done`. Same auth + model-allowlist gating as `ctx.llm.complete`. Streaming does NOT extend the call deadline (`PYLON_FN_CALL_TIMEOUT`, 30s default) — set `timeout: <secs>` on the def for a long run.
66
66
  - **`ctx.stream.write(text)` streams to the caller — and every fn stream is RESUMABLE**: the server buffers frames under a stream id (`streamFn`'s `onStreamId`), so a dropped connection or closed tab catches up via `resumeStream(id)`, including the final result after the handler finished. `ctx.rooms.broadcast(room, topic, data)` fans out to every CURRENTLY-CONNECTED subscriber (second device, second tab) but does not replay missed messages — use the stream id for anything that must survive a gap. Broadcast resolves `{ delivered: false }` when the room has no members — a no-op, not an error. Clients receive with `useRoomMessages(room, cb)` from `@pylonsync/react`; `useRoom` is the send side.
67
67
  - **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
68
- - **Use the supported file and scheduling APIs.** Files go through `<FileUpload>` and `/api/files/*`; there is no `ctx.files`. One-shot work uses `ctx.scheduler.runAfter`, `runAt`, or `cancel`; there is no `defineWorkflow` or `defineJob`. Recurring work uses `cron("0 * * * *", "fnName")` in `buildManifest({ crons: [...] })`, imported from `@pylonsync/sdk`. Make the target function `internal: true`. It runs with anonymous auth, but its own `ctx.db.*` calls are server-side and bypass policies. Use `ctx.auth.elevate({ admin: true, reason: "..." })`, with a mandatory reason, only when chaining another internal function through `ctx.scheduler`.
68
+ - **Use the supported file and scheduling APIs.** Files go through `<FileUpload>` and `/api/files/*`; an upload is readable by its uploader only, so pass `visibility: "public"` to `uploadFile` or `<FileUpload>` for files every visitor sees; there is no `ctx.files`. One-shot work uses `ctx.scheduler.runAfter`, `runAt`, or `cancel`; there is no `defineWorkflow` or `defineJob`. Recurring work uses `cron("0 * * * *", "fnName")` in `buildManifest({ crons: [...] })`, imported from `@pylonsync/sdk`. Make the target function `internal: true`. It runs with anonymous auth, but its own `ctx.db.*` calls are server-side and bypass policies. Use `ctx.auth.elevate({ admin: true, reason: "..." })`, with a mandatory reason, only when chaining another internal function through `ctx.scheduler`.
69
69
 
70
70
  ## Testing
71
71
 
@@ -23,42 +23,29 @@ export default function MarketingLayout({ children, auth }: LayoutProps) {
23
23
 
24
24
  return (
25
25
  <>
26
- <header className="sticky top-0 z-30 border-b border-zinc-200/70 bg-white/85 backdrop-blur">
27
- <div className="mx-auto flex h-14 max-w-5xl items-center justify-between px-6">
28
- <Link href="/" className="flex items-center gap-2">
29
- <span className="flex size-6 items-center justify-center rounded-[7px] bg-zinc-900 text-[13px] font-bold text-white">
30
- {brand.letter}
31
- </span>
32
- <span className="text-[15px] font-semibold tracking-tight text-zinc-900">
33
- {brand.name}
34
- </span>
26
+ {/* The nav sits over the hero photo: dark, translucent, cream type. */}
27
+ <header className="sticky top-0 z-30 border-b border-white/10 bg-[var(--ink)]/80 text-[var(--cream)] backdrop-blur">
28
+ <div className="mx-auto flex h-16 max-w-6xl items-center justify-between px-6">
29
+ <Link href="/" className="font-display text-[22px] tracking-tight">
30
+ {brand.name}
35
31
  </Link>
36
- <nav className="flex items-center gap-2">
37
- <a
38
- href="/#menu"
39
- className="hidden rounded-full px-3 py-1.5 text-[13px] font-medium text-zinc-600 transition-colors hover:text-zinc-900 sm:inline-flex"
40
- >
41
- Services
32
+ <nav className="flex items-center gap-1 sm:gap-2">
33
+ <a href="/#menu" className="hidden px-3 py-1.5 text-[13px] font-medium text-[var(--cream-2)] transition-colors hover:text-[var(--cream)] sm:inline-flex">
34
+ Menu
35
+ </a>
36
+ <a href="/#visit" className="hidden px-3 py-1.5 text-[13px] font-medium text-[var(--cream-2)] transition-colors hover:text-[var(--cream)] sm:inline-flex">
37
+ Visit
42
38
  </a>
43
39
  {signedIn ? (
44
- <Link
45
- href="/dashboard"
46
- className="inline-flex items-center rounded-full bg-zinc-900 px-3.5 py-1.5 text-[13px] font-medium text-white transition-colors hover:bg-zinc-700"
47
- >
40
+ <Link href="/dashboard" className="inline-flex items-center rounded-full border border-white/20 px-3.5 py-1.5 text-[13px] font-medium transition-colors hover:bg-white/10">
48
41
  Dashboard
49
42
  </Link>
50
43
  ) : (
51
44
  <>
52
- <Link
53
- href="/login"
54
- className="hidden rounded-full px-3 py-1.5 text-[13px] font-medium text-zinc-600 transition-colors hover:text-zinc-900 sm:inline-flex"
55
- >
45
+ <Link href="/login" className="hidden px-3 py-1.5 text-[13px] font-medium text-[var(--cream-2)] transition-colors hover:text-[var(--cream)] sm:inline-flex">
56
46
  Sign in
57
47
  </Link>
58
- <a
59
- href="/#reserve"
60
- className="inline-flex items-center rounded-full bg-brand px-3.5 py-1.5 text-[13px] font-medium text-white transition-colors hover:opacity-90"
61
- >
48
+ <a href="/#reserve" className="inline-flex items-center rounded-full bg-brand px-4 py-2 text-[13px] font-medium text-white transition-opacity hover:opacity-90">
62
49
  {siteConfig.hero.ctaLabel}
63
50
  </a>
64
51
  </>
@@ -77,38 +64,29 @@ export default function MarketingLayout({ children, auth }: LayoutProps) {
77
64
  function SiteFooter() {
78
65
  const { brand, location } = siteConfig;
79
66
  return (
80
- <footer className="border-t border-zinc-200/70 bg-white">
81
- <div className="mx-auto max-w-5xl px-6 py-12">
82
- <div className="flex flex-col items-start justify-between gap-6 sm:flex-row">
67
+ <footer className="border-t border-white/10 bg-[var(--ink)] text-[var(--cream-2)]">
68
+ <div className="mx-auto max-w-6xl px-6 py-14">
69
+ <div className="flex flex-col items-start justify-between gap-8 sm:flex-row">
83
70
  <div className="max-w-sm">
84
- <Link href="/" className="inline-flex items-center gap-2">
85
- <span className="flex size-6 items-center justify-center rounded-[7px] bg-zinc-900 text-[13px] font-bold text-white">
86
- {brand.letter}
87
- </span>
88
- <span className="text-[15px] font-semibold tracking-tight text-zinc-900">
89
- {brand.name}
90
- </span>
71
+ <Link href="/" className="font-display text-[26px] tracking-tight text-[var(--cream)]">
72
+ {brand.name}
91
73
  </Link>
92
- <p className="mt-3 text-[13px] leading-relaxed text-zinc-500">
93
- {brand.footerBlurb}
94
- </p>
74
+ <p className="mt-3 text-[13.5px] leading-relaxed">{brand.footerBlurb}</p>
95
75
  </div>
96
- <div className="text-[13px] leading-relaxed text-zinc-500">
97
- <div className="font-medium text-zinc-900">Visit</div>
76
+ <div className="text-[13.5px] leading-relaxed">
77
+ <div className="font-medium text-[var(--cream)]">Visit</div>
98
78
  <p className="mt-2 max-w-[14rem]">{location.address}</p>
99
79
  <p className="mt-2">{location.phone}</p>
100
- <a href={`mailto:${brand.email}`} className="mt-1 inline-block hover:text-zinc-900">
80
+ <a href={`mailto:${brand.email}`} className="mt-1 inline-block transition-colors hover:text-[var(--cream)]">
101
81
  {brand.email}
102
82
  </a>
103
83
  </div>
104
84
  </div>
105
- <div className="mt-10 flex flex-col items-start justify-between gap-3 border-t border-zinc-200/70 pt-6 text-[12px] text-zinc-400 sm:flex-row sm:items-center">
106
- <span>
107
- © {new Date().getFullYear()} {brand.copyrightName}
108
- </span>
85
+ <div className="mt-12 flex flex-col items-start justify-between gap-3 border-t border-white/10 pt-6 text-[12px] sm:flex-row sm:items-center">
86
+ <span>© {new Date().getFullYear()} {brand.copyrightName}</span>
109
87
  <span>
110
88
  Built with{" "}
111
- <a href="https://pylonsync.com" className="font-medium text-zinc-600 hover:text-zinc-900">
89
+ <a href="https://pylonsync.com" className="font-medium text-[var(--cream)]">
112
90
  Pylon
113
91
  </a>
114
92
  </span>