@pylonsync/create-pylon 0.11.1 → 0.11.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-pylon.js +1 -1
- package/package.json +1 -1
- package/templates/_root/AGENTS.md +1 -1
- package/templates/agency/AGENTS.md +1 -1
- package/templates/agency/app/(marketing)/contact-form.tsx +22 -32
- package/templates/agency/app/(marketing)/layout.tsx +36 -64
- package/templates/agency/app/(marketing)/not-found.tsx +6 -8
- package/templates/agency/app/(marketing)/page.tsx +93 -144
- package/templates/agency/app/(marketing)/work/[slug]/page.tsx +69 -71
- package/templates/agency/app/(marketing)/work/page.tsx +20 -17
- package/templates/agency/app/globals.css +15 -6
- package/templates/agency/app.ts +10 -0
- package/templates/agency/components/marketing.tsx +40 -104
- package/templates/agency/lib/site.config.ts +34 -52
- package/templates/agency/public/images/hero.jpg +0 -0
- package/templates/agency/public/images/team/claire-donovan.jpg +0 -0
- package/templates/agency/public/images/team/dana-okafor.jpg +0 -0
- package/templates/agency/public/images/team/marcus-lee.jpg +0 -0
- package/templates/agency/public/images/work/atlas-health.jpg +0 -0
- package/templates/agency/public/images/work/cohort.jpg +0 -0
- package/templates/agency/public/images/work/ledger.jpg +0 -0
- package/templates/agency/public/images/work/vela.jpg +0 -0
- package/templates/ai-chat/AGENTS.md +1 -1
- package/templates/ai-studio/AGENTS.md +1 -1
- package/templates/barebones/AGENTS.md +1 -1
- package/templates/chat/AGENTS.md +1 -1
- package/templates/consumer/AGENTS.md +1 -1
- package/templates/consumer/README.md +39 -23
- package/templates/consumer/app/(app)/explore/page.tsx +13 -0
- package/templates/consumer/app/(app)/layout.tsx +17 -0
- package/templates/consumer/app/(app)/p/[id]/page.tsx +13 -0
- package/templates/consumer/app/(app)/page.tsx +15 -0
- package/templates/consumer/app/(app)/u/[username]/page.tsx +14 -0
- package/templates/consumer/app/error.tsx +1 -1
- package/templates/consumer/app/globals.css +25 -0
- package/templates/consumer/app/layout.tsx +3 -10
- package/templates/consumer/app/llms.ts +10 -10
- package/templates/consumer/app/not-found.tsx +9 -22
- package/templates/consumer/app/robots.ts +3 -4
- package/templates/consumer/app/sitemap.ts +7 -21
- package/templates/consumer/app.ts +152 -33
- package/templates/consumer/components/social/app-chrome.tsx +109 -0
- package/templates/consumer/components/social/avatar.tsx +50 -0
- package/templates/consumer/components/social/comment-form.tsx +51 -0
- package/templates/consumer/components/social/create-post.tsx +155 -0
- package/templates/consumer/components/social/edit-profile.tsx +103 -0
- package/templates/consumer/components/social/explore-screen.tsx +23 -0
- package/templates/consumer/components/social/home-screen.tsx +161 -0
- package/templates/consumer/components/social/post-card.tsx +77 -0
- package/templates/consumer/components/social/post-grid.tsx +46 -0
- package/templates/consumer/components/social/post-parts.tsx +146 -0
- package/templates/consumer/components/social/post-screen.tsx +173 -0
- package/templates/consumer/components/social/profile-screen.tsx +146 -0
- package/templates/consumer/components/social/rich-text.tsx +22 -0
- package/templates/consumer/components/social/session.tsx +43 -0
- package/templates/consumer/components/social/use-social.ts +86 -0
- package/templates/consumer/functions/addComment.ts +27 -0
- package/templates/consumer/functions/createPost.ts +39 -0
- package/templates/consumer/functions/ensureProfile.ts +39 -0
- package/templates/consumer/functions/seedFeed.ts +38 -0
- package/templates/consumer/functions/updateProfile.ts +61 -0
- package/templates/consumer/lib/seed.ts +192 -0
- package/templates/consumer/lib/site.ts +6 -0
- package/templates/consumer/lib/social.ts +245 -0
- package/templates/consumer/tests/social.test.ts +157 -0
- package/templates/creator/AGENTS.md +1 -1
- package/templates/creator/app/(marketing)/layout.tsx +35 -69
- package/templates/creator/app/(marketing)/newsletter-signup.tsx +15 -26
- package/templates/creator/app/(marketing)/not-found.tsx +8 -6
- package/templates/creator/app/(marketing)/page.tsx +90 -106
- package/templates/creator/app/globals.css +14 -0
- package/templates/creator/app/layout.tsx +1 -0
- package/templates/creator/app.ts +10 -0
- package/templates/creator/components/marketing.tsx +28 -89
- package/templates/creator/lib/site.config.ts +51 -29
- package/templates/creator/public/images/headshot.jpg +0 -0
- package/templates/crm/AGENTS.md +1 -1
- package/templates/crm/lib/seed.ts +153 -21
- package/templates/directory/AGENTS.md +1 -1
- package/templates/directory/app/(marketing)/directory-browse.tsx +99 -137
- package/templates/directory/app/(marketing)/layout.tsx +37 -88
- package/templates/directory/app/(marketing)/not-found.tsx +11 -10
- package/templates/directory/app/(marketing)/page.tsx +12 -33
- package/templates/directory/app/(marketing)/submit/page.tsx +9 -10
- package/templates/directory/app/(marketing)/submit-form.tsx +26 -26
- package/templates/directory/app/globals.css +15 -4
- package/templates/directory/app.ts +10 -0
- package/templates/directory/components/marketing.tsx +27 -91
- package/templates/directory/lib/site.config.ts +13 -24
- package/templates/helpdesk/AGENTS.md +1 -1
- package/templates/helpdesk/lib/seed.ts +188 -104
- package/templates/inventory/AGENTS.md +1 -1
- package/templates/inventory/lib/seed.ts +175 -44
- package/templates/invoices/AGENTS.md +1 -1
- package/templates/invoices/lib/seed.ts +74 -89
- package/templates/local-service/AGENTS.md +1 -1
- package/templates/local-service/app/(marketing)/booking-widget.tsx +31 -31
- package/templates/local-service/app/(marketing)/layout.tsx +25 -37
- package/templates/local-service/app/(marketing)/not-found.tsx +4 -4
- package/templates/local-service/app/(marketing)/page.tsx +145 -139
- package/templates/local-service/app/globals.css +11 -0
- package/templates/local-service/app/layout.tsx +2 -0
- package/templates/local-service/app.ts +9 -0
- package/templates/local-service/components/marketing.tsx +14 -39
- package/templates/local-service/lib/site.config.ts +16 -23
- package/templates/local-service/public/images/hero.jpg +0 -0
- package/templates/marketplace/AGENTS.md +1 -1
- package/templates/marketplace/client/SellForm.tsx +2 -0
- package/templates/projects/AGENTS.md +1 -1
- package/templates/projects/lib/seed.ts +103 -32
- package/templates/restaurant/AGENTS.md +1 -1
- package/templates/restaurant/app/(marketing)/layout.tsx +26 -48
- package/templates/restaurant/app/(marketing)/page.tsx +146 -123
- package/templates/restaurant/app/globals.css +11 -0
- package/templates/restaurant/app/layout.tsx +1 -0
- package/templates/restaurant/app.ts +10 -0
- package/templates/restaurant/components/marketing.tsx +10 -0
- package/templates/restaurant/lib/site.config.ts +16 -14
- package/templates/restaurant/public/images/bar.jpg +0 -0
- package/templates/restaurant/public/images/branzino.jpg +0 -0
- package/templates/restaurant/public/images/hearth.jpg +0 -0
- package/templates/restaurant/public/images/hero.jpg +0 -0
- package/templates/saas/AGENTS.md +1 -1
- package/templates/shop/AGENTS.md +1 -1
- package/templates/shop/public/images/products/cedar-smoke.jpg +0 -0
- package/templates/shop/public/images/products/linen.jpg +0 -0
- package/templates/todo/AGENTS.md +1 -1
- package/templates/waitlist/AGENTS.md +1 -1
- package/templates/waitlist/app/(marketing)/layout.tsx +39 -85
- package/templates/waitlist/app/(marketing)/not-found.tsx +8 -9
- package/templates/waitlist/app/(marketing)/page.tsx +51 -86
- package/templates/waitlist/app/(marketing)/waitlist-hero.tsx +43 -83
- package/templates/waitlist/app/globals.css +23 -0
- package/templates/waitlist/app/layout.tsx +1 -0
- package/templates/waitlist/app.ts +18 -0
- package/templates/waitlist/components/marketing.tsx +67 -81
- package/templates/waitlist/lib/site.config.ts +52 -59
- package/templates/consumer/app/feed-client.tsx +0 -159
- package/templates/consumer/app/page.tsx +0 -27
- package/templates/consumer/functions/_keep.ts +0 -13
- package/templates/consumer/functions/seedPosts.ts +0 -56
- 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
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
},
|
|
49
|
-
|
|
50
|
-
{
|
|
51
|
-
|
|
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
|
|
64
|
-
{ key: "d-
|
|
65
|
-
{ key: "d-
|
|
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(
|
|
103
|
-
updatedAt: at(
|
|
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(
|
|
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(-
|
|
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(-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
>
|
|
41
|
-
|
|
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-
|
|
81
|
-
<div className="mx-auto max-w-
|
|
82
|
-
<div className="flex flex-col items-start justify-between gap-
|
|
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="
|
|
85
|
-
|
|
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-[
|
|
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-[
|
|
97
|
-
<div className="font-medium text-
|
|
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-
|
|
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-
|
|
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-
|
|
89
|
+
<a href="https://pylonsync.com" className="font-medium text-[var(--cream)]">
|
|
112
90
|
Pylon
|
|
113
91
|
</a>
|
|
114
92
|
</span>
|