@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
@@ -18,28 +18,31 @@ export type BaseConfig = {
18
18
  copyrightName: string;
19
19
  socials: Social[];
20
20
  };
21
- colors: { brand: string; brandSoft: string; paper: string };
21
+ // brand: the one accent. paper: page background. ink: running text.
22
+ colors: { brand: string; brandSoft: string; paper: string; ink: string };
22
23
  seo: { title: string; description: string };
23
24
  };
24
25
 
25
26
  export type Offering = { title: string; body: string; price?: string };
26
27
  export type Testimonial = { quote: string; name: string; role: string };
27
28
  export type LinkItem = { label: string; href: string; note?: string };
29
+ export type Issue = { title: string; date: string; href: string; summary?: string };
28
30
 
29
31
  export type CreatorConfig = BaseConfig & {
30
32
  hero: {
31
- badge: string;
32
33
  name: string; // the person / personal brand
33
34
  tagline: string; // one-line "what you do"
34
- intro: string; // a short paragraph
35
+ intro: string; // two sentences of running text under the name
35
36
  };
36
- about: { eyebrow: string; headline: string; paragraphs: string[] };
37
- offerings: { eyebrow: string; headline: string; items: Offering[] };
38
- testimonials?: { eyebrow: string; headline: string; items: Testimonial[] };
39
- // The realtime feature: a live newsletter subscriber count.
37
+ about: { headline: string; paragraphs: string[] };
38
+ offerings: { headline: string; items: Offering[] };
39
+ // Recent newsletter issues or posts. Omit to hide the section.
40
+ writing?: { headline: string; items: Issue[] };
41
+ testimonials?: { headline: string; items: Testimonial[] };
42
+ // The realtime feature: a live newsletter subscriber count. The form sits in
43
+ // the hero, under the introduction.
40
44
  newsletter: {
41
- eyebrow: string;
42
- headline: string;
45
+ name: string; // the newsletter's name, e.g. "The Studio Notes"
43
46
  subcopy: string;
44
47
  emailPlaceholder: string;
45
48
  ctaLabel: string;
@@ -47,7 +50,7 @@ export type CreatorConfig = BaseConfig & {
47
50
  counterLabel: string; // e.g. "readers subscribed"
48
51
  seedCount?: number; // vanity baseline added to the real live count
49
52
  };
50
- links?: { eyebrow: string; headline: string; items: LinkItem[] };
53
+ links?: { headline: string; body: string; items: LinkItem[] };
51
54
  };
52
55
 
53
56
  /* ----------------------------- config ---------------------------- */
@@ -75,7 +78,7 @@ export const siteConfig: CreatorConfig = {
75
78
  ],
76
79
  },
77
80
 
78
- colors: { brand: "#0d9488", brandSoft: "#ccfbf1", paper: "#fafafa" },
81
+ colors: { brand: "#4a6b3f", brandSoft: "#e6ecdf", paper: "#f7f3ec", ink: "#2a2622" },
79
82
 
80
83
  seo: {
81
84
  title: "Maya Rivera — product design coach & writer",
@@ -84,16 +87,14 @@ export const siteConfig: CreatorConfig = {
84
87
  },
85
88
 
86
89
  hero: {
87
- badge: "Now coaching · 2 spots open",
88
90
  name: "Maya Rivera",
89
91
  tagline: "Product design coach & writer.",
90
92
  intro:
91
- "I help product designers get unstuck, sharpen their portfolios, and do the bravest work of their careers. Fifteen years in the room; now I spend it in yours.",
93
+ "I help product designers get unstuck, sharpen their portfolios, and do the bravest work of their careers. I spent fifteen years leading design teams, and now I spend that time with you.",
92
94
  },
93
95
 
94
96
  about: {
95
- eyebrow: "About",
96
- headline: "Hi, I'm Maya.",
97
+ headline: "About",
97
98
  paragraphs: [
98
99
  "I've led design at two startups and a public company, shipped products to millions, and mentored designers who now lead teams of their own.",
99
100
  "These days I coach one-on-one, review portfolios, and write a weekly newsletter about the craft and the career. Every issue focuses on practical ways to move the work forward.",
@@ -101,8 +102,7 @@ export const siteConfig: CreatorConfig = {
101
102
  },
102
103
 
103
104
  offerings: {
104
- eyebrow: "Work with me",
105
- headline: "Three ways I can help.",
105
+ headline: "What I do",
106
106
  items: [
107
107
  {
108
108
  title: "1:1 coaching",
@@ -117,14 +117,37 @@ export const siteConfig: CreatorConfig = {
117
117
  {
118
118
  title: "Team workshops",
119
119
  body: "Half-day workshops on critique, design systems, and shipping faster without lowering the bar.",
120
- price: "let's talk",
120
+ price: "priced per team",
121
+ },
122
+ ],
123
+ },
124
+
125
+ writing: {
126
+ headline: "Recent issues",
127
+ items: [
128
+ {
129
+ title: "Critique is a skill, not a meeting",
130
+ date: "Sep 7, 2026",
131
+ href: "#",
132
+ summary: "How to run a critique that ends with decisions instead of notes.",
133
+ },
134
+ {
135
+ title: "Your portfolio has too many projects",
136
+ date: "Aug 31, 2026",
137
+ href: "#",
138
+ summary: "Three case studies, told well, beat twelve told badly.",
139
+ },
140
+ {
141
+ title: "What senior designers stop asking permission for",
142
+ date: "Aug 24, 2026",
143
+ href: "#",
144
+ summary: "The small decisions that mark the change from mid-level to senior.",
121
145
  },
122
146
  ],
123
147
  },
124
148
 
125
149
  testimonials: {
126
- eyebrow: "Kind words",
127
- headline: "From people I've worked with.",
150
+ headline: "From people I have worked with",
128
151
  items: [
129
152
  {
130
153
  quote:
@@ -148,24 +171,23 @@ export const siteConfig: CreatorConfig = {
148
171
  },
149
172
 
150
173
  newsletter: {
151
- eyebrow: "The Studio Notes",
152
- headline: "A weekly letter on the craft and the career.",
153
- subcopy:
154
- "One short, useful email every Sunday — on design, taste, and doing brave work. No spam, unsubscribe anytime.",
174
+ name: "The Studio Notes",
175
+ subcopy: "One short email every Sunday on design, taste, and doing brave work.",
155
176
  emailPlaceholder: "you@email.com",
156
177
  ctaLabel: "Subscribe",
157
- successMessage: "You're in — watch your inbox this Sunday.",
178
+ successMessage: "You are in. The next issue arrives on Sunday.",
158
179
  counterLabel: "designers reading",
159
180
  seedCount: 2400,
160
181
  },
161
182
 
162
183
  links: {
163
- eyebrow: "Elsewhere",
164
- headline: "Find me around the web.",
184
+ headline: "Work with me",
185
+ body:
186
+ "If you want coaching or a portfolio review, book a short intro call first. We talk through what you need, and I tell you whether I can help.",
165
187
  items: [
166
- { label: "Portfolio", href: "#", note: "Selected work, 2010–today" },
167
- { label: "Read the archive", href: "#", note: "Every past issue of The Studio Notes" },
168
188
  { label: "Book a free intro call", href: "#", note: "15 minutes, no pitch" },
189
+ { label: "Read the archive", href: "#", note: "Every past issue of The Studio Notes" },
190
+ { label: "Portfolio", href: "#", note: "Selected work, 2010 to today" },
169
191
  ],
170
192
  },
171
193
  };
@@ -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,8 @@
1
1
  // Demo data for a brand-new workspace.
2
2
  //
3
- // An empty CRM teaches you nothing: no board, no forecast, no sense of what the
4
- // thing is for. The first sign-in seeds a realistic pipeline so the app opens
5
- // with something to look at, then never touches the data again.
3
+ // The first sign-in seeds a working pipeline: two dozen companies, a contact or
4
+ // two at each, deals in every stage, and a few weeks of activity. The app
5
+ // opens with something to look at, then never touches the data again.
6
6
  //
7
7
  // Pure data + pure shaping, so the seeding function stays a thin wrapper and
8
8
  // this file can be edited (or emptied) without reading any server code.
@@ -30,6 +30,8 @@ export interface SeedDeal {
30
30
  stage: string;
31
31
  /** Days from today; negative is in the past. */
32
32
  closeInDays: number;
33
+ /** Days since the deal was created. */
34
+ age: number;
33
35
  }
34
36
 
35
37
  export interface SeedActivity {
@@ -47,6 +49,24 @@ export const SEED_COMPANIES: SeedCompany[] = [
47
49
  { key: "atlas", name: "Atlas Fitness", domain: "atlasfitness.io", industry: "Fitness", size: "10-50" },
48
50
  { key: "quarry", name: "Quarry Design Co", domain: "quarry.design", industry: "Agency", size: "1-10" },
49
51
  { key: "beacon", name: "Beacon Property", domain: "beaconproperty.com", industry: "Real Estate", size: "50-200" },
52
+ { key: "larkspur", name: "Larkspur Clinics", domain: "larkspurclinics.com", industry: "Healthcare", size: "200-500" },
53
+ { key: "meridian", name: "Meridian Freight", domain: "meridianfreight.com", industry: "Logistics", size: "500+" },
54
+ { key: "copperline", name: "Copperline Brewing", domain: "copperline.beer", industry: "Food & Beverage", size: "10-50" },
55
+ { key: "tidewater", name: "Tidewater Marine", domain: "tidewatermarine.com", industry: "Manufacturing", size: "50-200" },
56
+ { key: "sable", name: "Sable & Finch", domain: "sableandfinch.com", industry: "Retail", size: "10-50" },
57
+ { key: "orchard", name: "Orchard Learning", domain: "orchardlearning.org", industry: "Education", size: "50-200" },
58
+ { key: "granite", name: "Granite Peak Outfitters", domain: "granitepeak.com", industry: "Retail", size: "10-50" },
59
+ { key: "fernhill", name: "Fernhill Veterinary", domain: "fernhillvet.com", industry: "Healthcare", size: "10-50" },
60
+ { key: "kestrel", name: "Kestrel Aviation Services", domain: "kestrelaviation.com", industry: "Aviation", size: "200-500" },
61
+ { key: "bluefin", name: "Bluefin Analytics", domain: "bluefin.io", industry: "Software", size: "50-200" },
62
+ { key: "harbor", name: "Harbor Point Hotels", domain: "harborpointhotels.com", industry: "Hospitality", size: "500+" },
63
+ { key: "wren", name: "Wren Architecture", domain: "wren.studio", industry: "Agency", size: "10-50" },
64
+ { key: "summit", name: "Summit Credit Union", domain: "summitcu.org", industry: "Finance", size: "200-500" },
65
+ { key: "pinecrest", name: "Pinecrest Farms", domain: "pinecrestfarms.com", industry: "Agriculture", size: "50-200" },
66
+ { key: "vantage", name: "Vantage Solar", domain: "vantagesolar.com", industry: "Energy", size: "50-200" },
67
+ { key: "ironwood", name: "Ironwood Furniture", domain: "ironwoodfurniture.com", industry: "Manufacturing", size: "10-50" },
68
+ { key: "cobalt", name: "Cobalt Staffing", domain: "cobaltstaffing.com", industry: "Recruiting", size: "50-200" },
69
+ { key: "halcyon", name: "Halcyon Senior Living", domain: "halcyonliving.com", industry: "Healthcare", size: "200-500" },
50
70
  ];
51
71
 
52
72
  export const SEED_CONTACTS: SeedContact[] = [
@@ -54,29 +74,135 @@ export const SEED_CONTACTS: SeedContact[] = [
54
74
  { company: "northwind", name: "Marcus Iyer", email: "marcus@northwind.co", title: "Head of IT" },
55
75
  { company: "hallmark", name: "Priya Raman", email: "priya@hallmarkdental.com", title: "Practice Manager" },
56
76
  { company: "riverbed", name: "Tom Alvarez", email: "tom@riverbed.coffee", title: "Founder" },
77
+ { company: "riverbed", name: "Jess Nakamura", email: "jess@riverbed.coffee", title: "Operations Lead" },
57
78
  { company: "atlas", name: "Simone Clarke", email: "simone@atlasfitness.io", title: "Owner" },
58
79
  { company: "quarry", name: "Ben Osei", email: "ben@quarry.design", title: "Principal" },
59
80
  { company: "beacon", name: "Lena Fischer", email: "lena@beaconproperty.com", title: "Director of Sales" },
81
+ { company: "beacon", name: "Owen Hart", email: "owen@beaconproperty.com", title: "IT Manager" },
82
+ { company: "larkspur", name: "Dr. Amara Bello", email: "abello@larkspurclinics.com", title: "Medical Director" },
83
+ { company: "larkspur", name: "Kevin Doyle", email: "kdoyle@larkspurclinics.com", title: "COO" },
84
+ { company: "meridian", name: "Rachel Stone", email: "rstone@meridianfreight.com", title: "VP Technology" },
85
+ { company: "meridian", name: "Luis Herrera", email: "lherrera@meridianfreight.com", title: "Procurement Manager" },
86
+ { company: "copperline", name: "Mike Brennan", email: "mike@copperline.beer", title: "Co-founder" },
87
+ { company: "tidewater", name: "Sarah Lindqvist", email: "sarah@tidewatermarine.com", title: "Plant Manager" },
88
+ { company: "sable", name: "Isabel Moreau", email: "isabel@sableandfinch.com", title: "Owner" },
89
+ { company: "orchard", name: "David Park", email: "dpark@orchardlearning.org", title: "Director of Technology" },
90
+ { company: "granite", name: "Hannah Weiss", email: "hannah@granitepeak.com", title: "General Manager" },
91
+ { company: "fernhill", name: "Dr. Chris Okafor", email: "chris@fernhillvet.com", title: "Owner" },
92
+ { company: "kestrel", name: "James Whitaker", email: "jwhitaker@kestrelaviation.com", title: "Director of Maintenance" },
93
+ { company: "kestrel", name: "Nadia Petrov", email: "npetrov@kestrelaviation.com", title: "Systems Analyst" },
94
+ { company: "bluefin", name: "Elena Vasquez", email: "elena@bluefin.io", title: "Head of Product" },
95
+ { company: "harbor", name: "Robert Chen", email: "rchen@harborpointhotels.com", title: "VP Guest Experience" },
96
+ { company: "harbor", name: "Monica Reyes", email: "mreyes@harborpointhotels.com", title: "Regional Manager" },
97
+ { company: "wren", name: "Alex Duffy", email: "alex@wren.studio", title: "Partner" },
98
+ { company: "summit", name: "Patricia Nguyen", email: "pnguyen@summitcu.org", title: "SVP Member Services" },
99
+ { company: "summit", name: "Greg Holloway", email: "gholloway@summitcu.org", title: "Information Security" },
100
+ { company: "pinecrest", name: "Walt Jensen", email: "walt@pinecrestfarms.com", title: "Owner" },
101
+ { company: "vantage", name: "Anita Rao", email: "anita@vantagesolar.com", title: "Head of Installations" },
102
+ { company: "ironwood", name: "Paul Marchetti", email: "paul@ironwoodfurniture.com", title: "Owner" },
103
+ { company: "cobalt", name: "Tara Singh", email: "tara@cobaltstaffing.com", title: "Managing Director" },
104
+ { company: "cobalt", name: "Derek Lowe", email: "derek@cobaltstaffing.com", title: "Operations Manager" },
105
+ { company: "halcyon", name: "Margaret O'Neill", email: "moneill@halcyonliving.com", title: "Executive Director" },
106
+ { company: "halcyon", name: "Sam Kowalski", email: "skowalski@halcyonliving.com", title: "IT Lead" },
60
107
  ];
61
108
 
62
109
  export const SEED_DEALS: SeedDeal[] = [
63
- { company: "northwind", contact: "Dana Whitfield", title: "Fleet dispatch rollout", value: 48000, stage: "proposal", closeInDays: 12 },
64
- { company: "northwind", contact: "Marcus Iyer", title: "Warehouse scanner pilot", value: 9500, stage: "qualified", closeInDays: 26 },
65
- { company: "hallmark", contact: "Priya Raman", title: "Patient intake portal", value: 15200, stage: "won", closeInDays: -8 },
66
- { company: "riverbed", contact: "Tom Alvarez", title: "Multi-store inventory", value: 22000, stage: "proposal", closeInDays: 5 },
67
- { company: "atlas", contact: "Simone Clarke", title: "Class booking system", value: 7400, stage: "lead", closeInDays: 40 },
68
- { company: "quarry", contact: "Ben Osei", title: "Client portal build", value: 31000, stage: "qualified", closeInDays: 18 },
69
- { company: "beacon", contact: "Lena Fischer", title: "Listing sync integration", value: 64000, stage: "lead", closeInDays: 55 },
70
- { company: "beacon", contact: "Lena Fischer", title: "Agent CRM migration", value: 18500, stage: "lost", closeInDays: -21 },
110
+ // Lead
111
+ { company: "atlas", contact: "Simone Clarke", title: "Class booking system", value: 7400, stage: "lead", closeInDays: 40, age: 6 },
112
+ { company: "beacon", contact: "Lena Fischer", title: "Listing sync integration", value: 64000, stage: "lead", closeInDays: 55, age: 4 },
113
+ { company: "copperline", contact: "Mike Brennan", title: "Taproom POS integration", value: 12500, stage: "lead", closeInDays: 45, age: 3 },
114
+ { company: "granite", contact: "Hannah Weiss", title: "Rental fleet tracking", value: 9800, stage: "lead", closeInDays: 60, age: 2 },
115
+ { company: "pinecrest", contact: "Walt Jensen", title: "CSA subscription portal", value: 16000, stage: "lead", closeInDays: 50, age: 8 },
116
+ { company: "ironwood", contact: "Paul Marchetti", title: "Custom order configurator", value: 21000, stage: "lead", closeInDays: 70, age: 1 },
117
+ { company: "wren", contact: "Alex Duffy", title: "Project archive search", value: 8200, stage: "lead", closeInDays: 35, age: 5 },
118
+ { company: "fernhill", contact: "Dr. Chris Okafor", title: "Appointment reminders", value: 4600, stage: "lead", closeInDays: 30, age: 9 },
119
+ { company: "harbor", contact: "Monica Reyes", title: "Housekeeping dispatch app", value: 38000, stage: "lead", closeInDays: 75, age: 2 },
120
+ { company: "halcyon", contact: "Sam Kowalski", title: "Family portal", value: 27000, stage: "lead", closeInDays: 65, age: 7 },
121
+
122
+ // Qualified
123
+ { company: "northwind", contact: "Marcus Iyer", title: "Warehouse scanner pilot", value: 9500, stage: "qualified", closeInDays: 26, age: 18 },
124
+ { company: "quarry", contact: "Ben Osei", title: "Client portal build", value: 31000, stage: "qualified", closeInDays: 18, age: 22 },
125
+ { company: "larkspur", contact: "Kevin Doyle", title: "Multi-site scheduling", value: 72000, stage: "qualified", closeInDays: 34, age: 15 },
126
+ { company: "meridian", contact: "Luis Herrera", title: "Carrier onboarding workflow", value: 44000, stage: "qualified", closeInDays: 28, age: 20 },
127
+ { company: "orchard", contact: "David Park", title: "Parent communication hub", value: 19500, stage: "qualified", closeInDays: 21, age: 12 },
128
+ { company: "kestrel", contact: "Nadia Petrov", title: "Maintenance log digitisation", value: 56000, stage: "qualified", closeInDays: 42, age: 25 },
129
+ { company: "summit", contact: "Patricia Nguyen", title: "Member onboarding forms", value: 33500, stage: "qualified", closeInDays: 30, age: 16 },
130
+ { company: "cobalt", contact: "Tara Singh", title: "Candidate pipeline dashboard", value: 14800, stage: "qualified", closeInDays: 24, age: 11 },
131
+ { company: "vantage", contact: "Anita Rao", title: "Installer scheduling", value: 26500, stage: "qualified", closeInDays: 38, age: 14 },
132
+ { company: "tidewater", contact: "Sarah Lindqvist", title: "Shop floor work orders", value: 41000, stage: "qualified", closeInDays: 33, age: 19 },
133
+
134
+ // Proposal
135
+ { company: "northwind", contact: "Dana Whitfield", title: "Fleet dispatch rollout", value: 48000, stage: "proposal", closeInDays: 12, age: 34 },
136
+ { company: "riverbed", contact: "Tom Alvarez", title: "Multi-store inventory", value: 22000, stage: "proposal", closeInDays: 5, age: 29 },
137
+ { company: "meridian", contact: "Rachel Stone", title: "Customer tracking portal", value: 88000, stage: "proposal", closeInDays: 9, age: 41 },
138
+ { company: "bluefin", contact: "Elena Vasquez", title: "Embedded reporting module", value: 36000, stage: "proposal", closeInDays: 14, age: 27 },
139
+ { company: "harbor", contact: "Robert Chen", title: "Guest messaging platform", value: 67000, stage: "proposal", closeInDays: 7, age: 38 },
140
+ { company: "halcyon", contact: "Margaret O'Neill", title: "Care plan tracking", value: 52000, stage: "proposal", closeInDays: 16, age: 31 },
141
+ { company: "summit", contact: "Greg Holloway", title: "Security review tooling", value: 18000, stage: "proposal", closeInDays: 3, age: 24 },
142
+ { company: "sable", contact: "Isabel Moreau", title: "Loyalty programme", value: 11200, stage: "proposal", closeInDays: 10, age: 26 },
143
+
144
+ // Won
145
+ { company: "hallmark", contact: "Priya Raman", title: "Patient intake portal", value: 15200, stage: "won", closeInDays: -8, age: 52 },
146
+ { company: "riverbed", contact: "Jess Nakamura", title: "Roastery ordering app", value: 13400, stage: "won", closeInDays: -30, age: 74 },
147
+ { company: "larkspur", contact: "Dr. Amara Bello", title: "Referral tracking", value: 29000, stage: "won", closeInDays: -19, age: 63 },
148
+ { company: "kestrel", contact: "James Whitaker", title: "Parts inventory system", value: 47500, stage: "won", closeInDays: -44, age: 90 },
149
+ { company: "orchard", contact: "David Park", title: "Enrolment forms", value: 8900, stage: "won", closeInDays: -12, age: 48 },
150
+ { company: "cobalt", contact: "Derek Lowe", title: "Timesheet approvals", value: 12600, stage: "won", closeInDays: -61, age: 102 },
151
+ { company: "vantage", contact: "Anita Rao", title: "Site survey app", value: 19800, stage: "won", closeInDays: -25, age: 70 },
152
+ { company: "beacon", contact: "Owen Hart", title: "Tenant maintenance requests", value: 23000, stage: "won", closeInDays: -3, age: 45 },
153
+ { company: "wren", contact: "Alex Duffy", title: "Proposal templates", value: 6400, stage: "won", closeInDays: -37, age: 66 },
154
+
155
+ // Lost
156
+ { company: "beacon", contact: "Lena Fischer", title: "Agent CRM migration", value: 18500, stage: "lost", closeInDays: -21, age: 58 },
157
+ { company: "tidewater", contact: "Sarah Lindqvist", title: "Quality inspection app", value: 28000, stage: "lost", closeInDays: -40, age: 81 },
158
+ { company: "granite", contact: "Hannah Weiss", title: "Online store rebuild", value: 15500, stage: "lost", closeInDays: -14, age: 49 },
159
+ { company: "bluefin", contact: "Elena Vasquez", title: "Data warehouse sync", value: 42000, stage: "lost", closeInDays: -55, age: 95 },
160
+ { company: "ironwood", contact: "Paul Marchetti", title: "Showroom kiosk", value: 9200, stage: "lost", closeInDays: -9, age: 40 },
71
161
  ];
72
162
 
73
163
  export const SEED_ACTIVITIES: SeedActivity[] = [
74
- { deal: "Fleet dispatch rollout", kind: "call", body: "Walked Dana through the dispatch flow. Wants a security review before signing — sending the SOC 2 summary.", age: 5 },
164
+ { deal: "Fleet dispatch rollout", kind: "call", body: "Walked Dana through the dispatch flow. Wants a security review before signing. Sending the SOC 2 summary.", age: 5 },
75
165
  { deal: "Fleet dispatch rollout", kind: "email", body: "Sent revised proposal with the 3-year term and onboarding included.", age: 30 },
76
- { deal: "Patient intake portal", kind: "note", body: "Signed. Kickoff scheduled for the 14th — Priya is the day-to-day contact.", age: 190 },
166
+ { deal: "Fleet dispatch rollout", kind: "meeting", body: "Pricing workshop with Dana and Marcus. They asked for a per-vehicle price instead of per-seat.", age: 120 },
167
+ { deal: "Warehouse scanner pilot", kind: "note", body: "Marcus has 12 Zebra scanners on hand. Pilot will run in the Rotterdam warehouse only.", age: 54 },
168
+ { deal: "Patient intake portal", kind: "note", body: "Signed. Kickoff scheduled for the 14th. Priya is the day-to-day contact.", age: 190 },
77
169
  { deal: "Multi-store inventory", kind: "meeting", body: "Demo with Tom and the two store leads. Main question is offline mode at the market stall.", age: 48 },
78
- { deal: "Client portal build", kind: "note", body: "Ben wants SSO with their Google Workspace. Confirmed that's in scope.", age: 72 },
79
- { deal: "Listing sync integration", kind: "call", body: "Intro call. They're evaluating two other vendors; decision in Q3.", age: 96 },
170
+ { deal: "Multi-store inventory", kind: "email", body: "Sent the offline sync write-up and the pricing for a third store.", age: 20 },
171
+ { deal: "Client portal build", kind: "note", body: "Ben wants SSO with their Google Workspace. Confirmed that is in scope.", age: 72 },
172
+ { deal: "Client portal build", kind: "call", body: "Ben is comparing us to a Webflow build. Sent three portal references.", age: 8 },
173
+ { deal: "Listing sync integration", kind: "call", body: "Intro call. They are evaluating two other vendors. Decision in Q3.", age: 96 },
174
+ { deal: "Multi-site scheduling", kind: "meeting", body: "Site visit at the Westgate clinic. 14 practitioners, 3 rooms, paper diary today.", age: 140 },
175
+ { deal: "Multi-site scheduling", kind: "email", body: "Kevin asked for HIPAA documentation before the next meeting. Sent the BAA template.", age: 36 },
176
+ { deal: "Customer tracking portal", kind: "meeting", body: "Executive review with Rachel and the CFO. Budget approved pending legal.", age: 26 },
177
+ { deal: "Customer tracking portal", kind: "note", body: "Legal wants a data residency clause. Ours already covers EU, so this should be quick.", age: 12 },
178
+ { deal: "Customer tracking portal", kind: "call", body: "Rachel confirmed the board wants this live before peak season.", age: 200 },
179
+ { deal: "Carrier onboarding workflow", kind: "email", body: "Luis sent the current onboarding checklist. 47 steps, 6 systems.", age: 90 },
180
+ { deal: "Embedded reporting module", kind: "meeting", body: "Technical review with Elena's engineers. They want the embed to support row-level permissions.", age: 44 },
181
+ { deal: "Embedded reporting module", kind: "email", body: "Sent proposal v2 with the permissions work broken out as a phase 2 option.", age: 18 },
182
+ { deal: "Guest messaging platform", kind: "call", body: "Robert wants SMS and WhatsApp on day one. Email can wait.", age: 60 },
183
+ { deal: "Guest messaging platform", kind: "meeting", body: "Pilot proposal for the two Boston properties. Monica will be the operational sponsor.", age: 15 },
184
+ { deal: "Care plan tracking", kind: "note", body: "Margaret needs sign-off from the clinical board. They meet on the first Tuesday.", age: 70 },
185
+ { deal: "Care plan tracking", kind: "email", body: "Sent the audit log and access control write-up Sam asked for.", age: 33 },
186
+ { deal: "Security review tooling", kind: "call", body: "Greg walked through their current spreadsheet process. Wants the pilot scoped to vendor reviews only.", age: 9 },
187
+ { deal: "Loyalty programme", kind: "meeting", body: "Isabel liked the points model. Asked whether it can work with their Square terminals.", age: 52 },
188
+ { deal: "Member onboarding forms", kind: "call", body: "Patricia confirmed they process about 900 applications a month. Paper today.", age: 80 },
189
+ { deal: "Candidate pipeline dashboard", kind: "email", body: "Tara sent sample data. Their ATS export is a CSV with 23 columns.", age: 100 },
190
+ { deal: "Installer scheduling", kind: "meeting", body: "Rode along with an install crew. Real pain is the morning dispatch call, not the calendar.", age: 150 },
191
+ { deal: "Shop floor work orders", kind: "note", body: "Sarah wants tablets on the floor. Rugged cases are their cost, not ours.", age: 64 },
192
+ { deal: "Maintenance log digitisation", kind: "meeting", body: "FAA compliance is the whole conversation. Nadia is pulling the relevant Part 145 sections.", age: 112 },
193
+ { deal: "Parent communication hub", kind: "call", body: "David wants a pilot with two schools this term.", age: 30 },
194
+ { deal: "Roastery ordering app", kind: "note", body: "Live since March. Jess mentioned a wholesale tier as a possible expansion.", age: 400 },
195
+ { deal: "Referral tracking", kind: "note", body: "Went live across all four clinics. Amara is the reference for Halcyon.", age: 320 },
196
+ { deal: "Parts inventory system", kind: "note", body: "Renewal due in eleven months. James is happy with the barcode workflow.", age: 700 },
197
+ { deal: "Tenant maintenance requests", kind: "email", body: "Contract countersigned. Onboarding starts Monday with Owen's team.", age: 50 },
198
+ { deal: "Agent CRM migration", kind: "note", body: "Lost to the incumbent. Lena said the switching cost for 80 agents was the blocker.", age: 500 },
199
+ { deal: "Quality inspection app", kind: "note", body: "Lost. They built it in-house with their ERP vendor.", age: 960 },
200
+ { deal: "Online store rebuild", kind: "note", body: "Lost to a Shopify agency. Budget was smaller than we scoped for.", age: 330 },
201
+ { deal: "Data warehouse sync", kind: "note", body: "Lost. Elena's team chose Fivetran for the sync and kept us for reporting, which is the open proposal.", age: 1300 },
202
+ { deal: "Taproom POS integration", kind: "email", body: "Mike asked which POS systems we already integrate with. Sent the list.", age: 40 },
203
+ { deal: "CSA subscription portal", kind: "call", body: "Walt runs 340 CSA boxes a week on a spreadsheet. Wants pickup site management too.", age: 180 },
204
+ { deal: "Housekeeping dispatch app", kind: "note", body: "Inbound from the Guest messaging conversation. Monica introduced us to her ops lead.", age: 44 },
205
+ { deal: "Family portal", kind: "email", body: "Sam sent the current family newsletter process. Wants photo sharing with consent controls.", age: 160 },
80
206
  ];
81
207
 
82
208
  export interface ShapedSeed {
@@ -94,20 +220,26 @@ export function shapeSeed(now: number = Date.now()): ShapedSeed {
94
220
  const at = (days: number) => new Date(now + days * 86_400_000).toISOString();
95
221
  const hoursAgo = (hours: number) => new Date(now - hours * 3_600_000).toISOString();
96
222
 
223
+ const latestActivity = new Map<string, number>();
224
+ for (const a of SEED_ACTIVITIES) {
225
+ const current = latestActivity.get(a.deal);
226
+ if (current === undefined || a.age < current) latestActivity.set(a.deal, a.age);
227
+ }
228
+
97
229
  return {
98
- companies: SEED_COMPANIES.map((c) => ({
230
+ companies: SEED_COMPANIES.map((c, index) => ({
99
231
  key: c.key,
100
232
  row: {
101
233
  name: c.name,
102
234
  domain: c.domain,
103
235
  industry: c.industry,
104
236
  size: c.size,
105
- createdAt: hoursAgo(240),
237
+ createdAt: at(-120 + index * 3),
106
238
  },
107
239
  })),
108
- contacts: SEED_CONTACTS.map((c) => ({
240
+ contacts: SEED_CONTACTS.map((c, index) => ({
109
241
  company: c.company,
110
- row: { name: c.name, email: c.email, title: c.title, createdAt: hoursAgo(200) },
242
+ row: { name: c.name, email: c.email, title: c.title, createdAt: at(-110 + index * 2) },
111
243
  })),
112
244
  deals: SEED_DEALS.map((d) => ({
113
245
  company: d.company,
@@ -118,8 +250,8 @@ export function shapeSeed(now: number = Date.now()): ShapedSeed {
118
250
  value: d.value,
119
251
  stage: d.stage,
120
252
  closeDate: at(d.closeInDays),
121
- createdAt: hoursAgo(160),
122
- updatedAt: hoursAgo(4),
253
+ createdAt: at(-d.age),
254
+ updatedAt: hoursAgo(Math.min(latestActivity.get(d.title) ?? d.age * 24, d.age * 24)),
123
255
  },
124
256
  })),
125
257
  activities: SEED_ACTIVITIES.map((a) => ({
@@ -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