@vibes.diy/prompts 14.1.37 → 14.1.39
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/llms/access.md +26 -18
- package/llms/backend.md +124 -20
- package/llms/bluesky.md +2 -2
- package/llms/calendar.md +2 -2
- package/llms/callai.md +1 -1
- package/llms/connections.md +3 -3
- package/llms/create-vibe.md +2 -2
- package/llms/d3.md +1 -1
- package/llms/fireproof.md +4 -4
- package/llms/image-gen.md +1 -1
- package/llms/p5.md +1 -1
- package/llms/spotify.md +2 -2
- package/llms/three-js.md +1 -1
- package/llms/use-vibe.md +8 -3
- package/llms/use-viewer.md +1 -1
- package/llms/webxr.md +1 -1
- package/llms/youtube.md +2 -2
- package/package.json +4 -4
package/llms/access.md
CHANGED
|
@@ -66,7 +66,9 @@ Keep the two visually distinct (a checkbox with its own explanatory line; a sepa
|
|
|
66
66
|
|
|
67
67
|
App.jsx — the checkbox is the consent call; the picker is a doc write:
|
|
68
68
|
|
|
69
|
-
```jsx
|
|
69
|
+
```jsx file=App.jsx
|
|
70
|
+
const { can } = useVibe("logs"); // the write gate is always the server's own verdict
|
|
71
|
+
|
|
70
72
|
// checking is the consent preflight; unchecking is the off switch. Never one verb for both edges.
|
|
71
73
|
<label>
|
|
72
74
|
<input
|
|
@@ -82,12 +84,16 @@ App.jsx — the checkbox is the consent call; the picker is a doc write:
|
|
|
82
84
|
{showFollowers && <ul>{followers.map((f) => <li key={f.handle}>{f.handle}</li>)}</ul>}
|
|
83
85
|
|
|
84
86
|
{/* the app's own list — one share doc per person picked */}
|
|
85
|
-
{can.create({ type: "share", authorHandle: me?.userHandle, buddyHandle: "x" }).ok &&
|
|
87
|
+
{can.create({ type: "share", authorHandle: me?.userHandle, buddyHandle: "x" }).ok && (
|
|
88
|
+
<AddBuddyForm
|
|
89
|
+
onAdd={(buddyHandle) => database.put({ type: "share", authorHandle: me?.userHandle, buddyHandle })}
|
|
90
|
+
/>
|
|
91
|
+
)}
|
|
86
92
|
```
|
|
87
93
|
|
|
88
94
|
access.js — the buddy grant and the followers audience arm are independent:
|
|
89
95
|
|
|
90
|
-
```js
|
|
96
|
+
```js file=access.js
|
|
91
97
|
const ch = `log:${user.userHandle}`;
|
|
92
98
|
|
|
93
99
|
if (doc.type === "share") {
|
|
@@ -105,6 +111,8 @@ if (doc.type === "entry") {
|
|
|
105
111
|
audience: { followersOf: user.userHandle },
|
|
106
112
|
};
|
|
107
113
|
}
|
|
114
|
+
|
|
115
|
+
throw { forbidden: "unknown document type" }; // every access.js ends by denying what it does not know
|
|
108
116
|
```
|
|
109
117
|
|
|
110
118
|
### The ImgGen version-append nuance
|
|
@@ -195,7 +203,7 @@ Use `oldDoc` (the previous version of the document) to enforce invariants across
|
|
|
195
203
|
|
|
196
204
|
Some documents belong to the machinery rather than to a person: a scheduled run's state doc, a counter a `scheduled` sweep advances, a notification the backend generates. Gate those on the **origin** with `ctx.requireBackend(...)` — one line, no conditional:
|
|
197
205
|
|
|
198
|
-
```js
|
|
206
|
+
```js file=access.js partial
|
|
199
207
|
if (doc.type === "runState") {
|
|
200
208
|
ctx.requireBackend("scheduled");
|
|
201
209
|
return { channels: ["show"] };
|
|
@@ -224,7 +232,7 @@ Worked example — members-only chat writes
|
|
|
224
232
|
|
|
225
233
|
access.js
|
|
226
234
|
|
|
227
|
-
```js
|
|
235
|
+
```js file=access.js partial
|
|
228
236
|
export function chat(doc, oldDoc, user, ctx) {
|
|
229
237
|
if (!user) throw { forbidden: "authentication required" };
|
|
230
238
|
|
|
@@ -279,7 +287,7 @@ _Each example below closes its type dispatch with the default terminal `throw {
|
|
|
279
287
|
|
|
280
288
|
access.js
|
|
281
289
|
|
|
282
|
-
```js
|
|
290
|
+
```js file=access.js partial
|
|
283
291
|
export function wall(doc, oldDoc, user, ctx) {
|
|
284
292
|
if (!user) throw { forbidden: "sign in" };
|
|
285
293
|
|
|
@@ -310,7 +318,7 @@ export function wall(doc, oldDoc, user, ctx) {
|
|
|
310
318
|
|
|
311
319
|
access.js
|
|
312
320
|
|
|
313
|
-
```js
|
|
321
|
+
```js file=access.js partial
|
|
314
322
|
export function board(doc, oldDoc, user, ctx) {
|
|
315
323
|
if (!user) throw { forbidden: "sign in" };
|
|
316
324
|
const channel = `board:${doc.boardId}`;
|
|
@@ -343,7 +351,7 @@ export function board(doc, oldDoc, user, ctx) {
|
|
|
343
351
|
|
|
344
352
|
access.js
|
|
345
353
|
|
|
346
|
-
```js
|
|
354
|
+
```js file=access.js partial
|
|
347
355
|
export function notes(doc, oldDoc, user, ctx) {
|
|
348
356
|
if (!user) throw { forbidden: "sign in" };
|
|
349
357
|
const ch = `user:${user.userHandle}`; // this visitor's own private space
|
|
@@ -372,7 +380,7 @@ Every item a visitor creates lands on their own `user:<handle>` channel, private
|
|
|
372
380
|
|
|
373
381
|
access.js
|
|
374
382
|
|
|
375
|
-
```js
|
|
383
|
+
```js file=access.js partial
|
|
376
384
|
export function habits(doc, oldDoc, user, ctx) {
|
|
377
385
|
if (!user) throw { forbidden: "sign in" };
|
|
378
386
|
|
|
@@ -414,7 +422,7 @@ The leaderboard is just the access model: read the public `track:` channels and
|
|
|
414
422
|
|
|
415
423
|
The personal blog's post branch is small — public read is the blog's resting state, declared inline on every post result (readers are the point of publishing):
|
|
416
424
|
|
|
417
|
-
```js
|
|
425
|
+
```js file=access.js partial
|
|
418
426
|
if (doc.type === "post") {
|
|
419
427
|
if (!oldDoc) {
|
|
420
428
|
if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
|
|
@@ -496,7 +504,7 @@ This example shows the full round-trip — access.js declares channels and grant
|
|
|
496
504
|
|
|
497
505
|
access.js
|
|
498
506
|
|
|
499
|
-
```js
|
|
507
|
+
```js file=access.js
|
|
500
508
|
export function announcements(doc, oldDoc, user, ctx) {
|
|
501
509
|
if (!user) throw { forbidden: "sign in" };
|
|
502
510
|
|
|
@@ -546,7 +554,7 @@ export function announcements(doc, oldDoc, user, ctx) {
|
|
|
546
554
|
|
|
547
555
|
App.jsx — `useVibe().can` gates every write surface (posts AND owner-only management); `access.hasChannel()` reflects display-only membership:
|
|
548
556
|
|
|
549
|
-
```jsx
|
|
557
|
+
```jsx file=App.jsx
|
|
550
558
|
import React from "react";
|
|
551
559
|
import { useFireproof } from "use-fireproof";
|
|
552
560
|
import { useViewer, useVibe } from "use-vibes";
|
|
@@ -648,7 +656,7 @@ Channels everyone can read, and any signed-in user can post to. The channel doc
|
|
|
648
656
|
|
|
649
657
|
access.js
|
|
650
658
|
|
|
651
|
-
```js
|
|
659
|
+
```js file=access.js partial
|
|
652
660
|
export function chat(doc, oldDoc, user, ctx) {
|
|
653
661
|
if (!user) throw { forbidden: "sign in" };
|
|
654
662
|
|
|
@@ -718,7 +726,7 @@ Reach for this by default, and certainly whenever the prompt says **invite, join
|
|
|
718
726
|
|
|
719
727
|
access.js
|
|
720
728
|
|
|
721
|
-
```js
|
|
729
|
+
```js file=access.js partial
|
|
722
730
|
export default function (doc, oldDoc, user, ctx) {
|
|
723
731
|
if (!user) throw { forbidden: "sign in" };
|
|
724
732
|
const ch = (id) => `list:${id}`;
|
|
@@ -805,7 +813,7 @@ Reach for this variant whenever the app is built around a *thing* people come ba
|
|
|
805
813
|
|
|
806
814
|
access.js
|
|
807
815
|
|
|
808
|
-
```js
|
|
816
|
+
```js file=access.js partial
|
|
809
817
|
export function boards(doc, oldDoc, user, ctx) {
|
|
810
818
|
if (!user?.userHandle) throw { forbidden: "sign in" };
|
|
811
819
|
const safeId = (id) => {
|
|
@@ -911,7 +919,7 @@ That bounds what the build's own closing line says: the rules and the screen it
|
|
|
911
919
|
|
|
912
920
|
access.js
|
|
913
921
|
|
|
914
|
-
```js
|
|
922
|
+
```js file=access.js partial
|
|
915
923
|
export function chat(doc, oldDoc, user, ctx) {
|
|
916
924
|
if (!user) throw { forbidden: "authentication required" };
|
|
917
925
|
|
|
@@ -997,7 +1005,7 @@ App.jsx — show only channels the viewer is in (`access.hasChannel`), gate the
|
|
|
997
1005
|
|
|
998
1006
|
access.js
|
|
999
1007
|
|
|
1000
|
-
```js
|
|
1008
|
+
```js file=access.js partial
|
|
1001
1009
|
export function survey(doc, oldDoc, user, ctx) {
|
|
1002
1010
|
if (doc.type === "survey-response") {
|
|
1003
1011
|
if (oldDoc) throw { forbidden: "responses are write-once" };
|
|
@@ -1073,7 +1081,7 @@ When the prompt says **anyone can sign / submit without logging in** (a guestboo
|
|
|
1073
1081
|
|
|
1074
1082
|
access.js
|
|
1075
1083
|
|
|
1076
|
-
```js
|
|
1084
|
+
```js file=access.js partial
|
|
1077
1085
|
export function guestbook(doc, oldDoc, user, ctx) {
|
|
1078
1086
|
if (doc.type === "entry") {
|
|
1079
1087
|
if (oldDoc) throw { forbidden: "entries are write-once" };
|
package/llms/backend.md
CHANGED
|
@@ -444,7 +444,7 @@ in `nodeReleases`, and records refresh state in that same database.
|
|
|
444
444
|
|
|
445
445
|
backend.js
|
|
446
446
|
|
|
447
|
-
```js
|
|
447
|
+
```js file=backend.js
|
|
448
448
|
export const config = { scheduled: { interval: "15m" } };
|
|
449
449
|
|
|
450
450
|
const SNAPSHOT = [
|
|
@@ -486,6 +486,7 @@ async function putIfChanged(ctx, next, db) {
|
|
|
486
486
|
}
|
|
487
487
|
|
|
488
488
|
export async function scheduled(event, ctx) {
|
|
489
|
+
ctx.log("debug", "refresh tick", { at: event.scheduledTime }); // proof of life, before any read
|
|
489
490
|
await ensureSnapshot(ctx);
|
|
490
491
|
|
|
491
492
|
// One request, no retry storm; ctx.fetch enforces 15s/10MB and egress caps.
|
|
@@ -540,7 +541,7 @@ export async function scheduled(event, ctx) {
|
|
|
540
541
|
|
|
541
542
|
App.jsx
|
|
542
543
|
|
|
543
|
-
```jsx
|
|
544
|
+
```jsx file=App.jsx
|
|
544
545
|
import React from "react";
|
|
545
546
|
import { useFireproof } from "use-fireproof";
|
|
546
547
|
|
|
@@ -612,7 +613,7 @@ Runs for requests to the app's `/_api` route. The request path is rooted after
|
|
|
612
613
|
`/_api`: a call to `https://slug--owner.host/_api/webhooks/pay` arrives with
|
|
613
614
|
pathname `/webhooks/pay`. Return a standard `Response`.
|
|
614
615
|
|
|
615
|
-
```js
|
|
616
|
+
```js file=backend.js
|
|
616
617
|
export async function fetch(request, ctx) {
|
|
617
618
|
const url = new URL(request.url);
|
|
618
619
|
if (url.pathname === "/rsvp" && request.method === "POST") {
|
|
@@ -628,7 +629,7 @@ export async function fetch(request, ctx) {
|
|
|
628
629
|
|
|
629
630
|
From `App.jsx`, call it with a relative fetch — no host needed:
|
|
630
631
|
|
|
631
|
-
```js
|
|
632
|
+
```js file=App.jsx partial
|
|
632
633
|
const res = await fetch("/_api/rsvp", { method: "POST", body: JSON.stringify({ name }) });
|
|
633
634
|
```
|
|
634
635
|
|
|
@@ -640,7 +641,7 @@ owner, declare `config.fetch.unfilteredReads` for the collections it reads, page
|
|
|
640
641
|
`ctx.db.query` returns, and keep progress in a state document read by id so a second run continues
|
|
641
642
|
where the first stopped.
|
|
642
643
|
|
|
643
|
-
```js
|
|
644
|
+
```js file=backend.js
|
|
644
645
|
export const config = { fetch: { unfilteredReads: ["rsvps"] } };
|
|
645
646
|
|
|
646
647
|
export async function fetch(request, ctx) {
|
|
@@ -681,7 +682,7 @@ branch on: a score or a summary is only ever computed from text the handler actu
|
|
|
681
682
|
|
|
682
683
|
backend.js
|
|
683
684
|
|
|
684
|
-
```js
|
|
685
|
+
```js file=backend.js
|
|
685
686
|
export async function fetch(request, ctx) {
|
|
686
687
|
const url = new URL(request.url);
|
|
687
688
|
if (url.pathname === "/page") {
|
|
@@ -720,7 +721,7 @@ export async function fetch(request, ctx) {
|
|
|
720
721
|
|
|
721
722
|
From `App.jsx`, the page asks the handler for the address it was given:
|
|
722
723
|
|
|
723
|
-
```js
|
|
724
|
+
```js file=App.jsx partial
|
|
724
725
|
const res = await fetch(`/_api/page?url=${encodeURIComponent(url)}`);
|
|
725
726
|
const page = await res.json();
|
|
726
727
|
if (page.ok === false) {
|
|
@@ -747,7 +748,7 @@ server owes in response.
|
|
|
747
748
|
Runs after any document write to the app's databases commits (user writes and
|
|
748
749
|
backend writes alike). The event:
|
|
749
750
|
|
|
750
|
-
```js
|
|
751
|
+
```js file=backend.js
|
|
751
752
|
export async function onChange(event, ctx) {
|
|
752
753
|
// event: { dbName, docId, doc, oldDoc, seq, deleted }
|
|
753
754
|
if (event.dbName !== "votes" || event.deleted) return;
|
|
@@ -771,11 +772,12 @@ Two rules keep change-reactions sane:
|
|
|
771
772
|
Requires a `config` export with a **static string-literal** interval between
|
|
772
773
|
`"5s"` and `"1h"` (e.g. `"30s"`, `"5m"`, `"1h"` — computed values are rejected):
|
|
773
774
|
|
|
774
|
-
```js
|
|
775
|
+
```js file=backend.js
|
|
775
776
|
export const config = { scheduled: { interval: "15m" } };
|
|
776
777
|
|
|
777
778
|
export async function scheduled(event, ctx) {
|
|
778
779
|
// event: { scheduledTime } — ISO timestamp of this tick
|
|
780
|
+
ctx.log("debug", "heartbeat tick", { at: event.scheduledTime }); // proof of life, every tick
|
|
779
781
|
await ctx.db.put({ kind: "heartbeat", at: event.scheduledTime }, { db: "status" });
|
|
780
782
|
}
|
|
781
783
|
```
|
|
@@ -798,7 +800,7 @@ saving on its own, which a stored hour-offset does not.
|
|
|
798
800
|
|
|
799
801
|
App.jsx — when the owner saves the schedule:
|
|
800
802
|
|
|
801
|
-
```jsx
|
|
803
|
+
```jsx file=App.jsx partial
|
|
802
804
|
await merge({
|
|
803
805
|
hour: 12, // the hour they picked, in their own day
|
|
804
806
|
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, // e.g. "America/Detroit"
|
|
@@ -807,7 +809,7 @@ await merge({
|
|
|
807
809
|
|
|
808
810
|
backend.js — the tick compares in that zone:
|
|
809
811
|
|
|
810
|
-
```js
|
|
812
|
+
```js file=backend.js partial
|
|
811
813
|
const hourLocal = Number(
|
|
812
814
|
new Intl.DateTimeFormat("en-US", {
|
|
813
815
|
timeZone: settings.timeZone ?? "UTC",
|
|
@@ -842,6 +844,87 @@ And **only say a thing you computed.** "Tonight", "this morning", "in an hour"
|
|
|
842
844
|
are claims about the reader's clock; if the code did not derive them from that
|
|
843
845
|
reader's zone, name the moment instead and let them place it.
|
|
844
846
|
|
|
847
|
+
### Recurring reminders on somebody's clock
|
|
848
|
+
|
|
849
|
+
**A recurring reminder fires on each person's own clock, never the server's.**
|
|
850
|
+
Once a reminder repeats — "every weekday at 7" — the zone stops being one
|
|
851
|
+
setting on one schedule document and becomes a property of each person the
|
|
852
|
+
reminder is for. Everything below is per person, and none of it is the
|
|
853
|
+
server's day or the server's hour.
|
|
854
|
+
|
|
855
|
+
**Capture the zone as an effect, not as a question.** Do not ask them for a
|
|
856
|
+
timezone: the browser already knows it, so write it once when they first sign
|
|
857
|
+
in and leave it alone. Store it where the reminder is addressed — on the
|
|
858
|
+
person's own profile document when the list is shared — and guard the write so
|
|
859
|
+
it only ever happens on a real change, or the effect re-fires forever.
|
|
860
|
+
|
|
861
|
+
**Derive the weekday and the hour together, in that zone, from one
|
|
862
|
+
`formatToParts` call.** `getUTCDay()`/`getUTCHours()` answer a question about
|
|
863
|
+
the machine the handler happens to be running on; at 07:00 in Los Angeles it is
|
|
864
|
+
already a different day in UTC, so a weekday gate on the server's clock silently
|
|
865
|
+
drops Monday mornings and fires on Saturday ones.
|
|
866
|
+
|
|
867
|
+
**An unrecognised zone string throws.** `Intl.DateTimeFormat` raises a
|
|
868
|
+
`RangeError` on a zone it does not know, so in a loop over people the formatting
|
|
869
|
+
goes in a `try` and a bad profile is skipped — one stale record must not stop
|
|
870
|
+
everybody else's reminder.
|
|
871
|
+
|
|
872
|
+
The unit of work is that person's local date (`en-CA` formats it as
|
|
873
|
+
`YYYY-MM-DD`), keyed per person, per "A tick that repeats is not a run that
|
|
874
|
+
repeats" above: the interval is only how often the handler wakes up.
|
|
875
|
+
|
|
876
|
+
App.jsx — write the zone once, when they arrive:
|
|
877
|
+
|
|
878
|
+
```jsx file=App.jsx
|
|
879
|
+
useEffect(() => {
|
|
880
|
+
if (!me?.userHandle) return;
|
|
881
|
+
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
882
|
+
if (!timeZone || myProfile?.timeZone === timeZone) return; // already stored — don't churn
|
|
883
|
+
database.put({ _id: `profile:${me.userHandle}`, type: "profile", userHandle: me.userHandle, timeZone });
|
|
884
|
+
}, [me?.userHandle, myProfile?.timeZone]);
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
backend.js — evaluate each person in their own zone:
|
|
888
|
+
|
|
889
|
+
```js file=backend.js
|
|
890
|
+
export const config = { scheduled: { interval: "15m" } };
|
|
891
|
+
const WEEKDAYS = new Set(["Mon", "Tue", "Wed", "Thu", "Fri"]);
|
|
892
|
+
|
|
893
|
+
export async function scheduled(event, ctx) {
|
|
894
|
+
const at = new Date(event.scheduledTime);
|
|
895
|
+
const profiles = await ctx.db.query({ db: DB, field: "type", key: "profile", limit: 500 });
|
|
896
|
+
ctx.log("debug", "reminder tick", { profiles: profiles.length });
|
|
897
|
+
|
|
898
|
+
for (const profile of profiles) {
|
|
899
|
+
const { timeZone, userHandle } = profile;
|
|
900
|
+
if (!timeZone || !userHandle) continue;
|
|
901
|
+
let weekday, hour, dateKey;
|
|
902
|
+
try {
|
|
903
|
+
// One formatter, one zone: the weekday and the hour must agree about the day.
|
|
904
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
905
|
+
timeZone,
|
|
906
|
+
weekday: "short",
|
|
907
|
+
hour: "2-digit",
|
|
908
|
+
hourCycle: "h23",
|
|
909
|
+
}).formatToParts(at);
|
|
910
|
+
weekday = parts.find((p) => p.type === "weekday")?.value;
|
|
911
|
+
hour = Number(parts.find((p) => p.type === "hour")?.value);
|
|
912
|
+
dateKey = new Intl.DateTimeFormat("en-CA", { timeZone }).format(at); // "2026-09-18", their day
|
|
913
|
+
} catch {
|
|
914
|
+
continue; // unrecognised zone on one profile — skip the person, not the tick
|
|
915
|
+
}
|
|
916
|
+
if (!WEEKDAYS.has(weekday) || hour !== 7) continue;
|
|
917
|
+
|
|
918
|
+
const id = `morningReminder:${userHandle}:${dateKey}`; // the unit of work, per person
|
|
919
|
+
if (await ctx.db.get(id, { db: DB })) continue; // this morning is already done
|
|
920
|
+
await ctx.db.put(
|
|
921
|
+
{ _id: id, type: "morningReminder", ownerHandle: userHandle, date: dateKey, checkedAt: at.toISOString() },
|
|
922
|
+
{ db: DB }
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
```
|
|
927
|
+
|
|
845
928
|
### An unattended tick reports what it did
|
|
846
929
|
|
|
847
930
|
**An unattended tick earns its spend.** Nobody is watching a scheduled run, so
|
|
@@ -859,7 +942,7 @@ is the only place an owner can tell. For a job that acts twice a month that mean
|
|
|
859
942
|
weeks of silence that reads exactly like a dead alarm. So log once per tick,
|
|
860
943
|
before any early return:
|
|
861
944
|
|
|
862
|
-
```js
|
|
945
|
+
```js file=backend.js partial
|
|
863
946
|
export async function scheduled(event, ctx) {
|
|
864
947
|
const due = eventsDueNow(event.scheduledTime);
|
|
865
948
|
ctx.log("debug", "tick", { due: due.length }); // proof of life; no reads, no spend
|
|
@@ -941,7 +1024,7 @@ genuinely produced something is a real state change and belongs in that doc —
|
|
|
941
1024
|
unlike a per-tick `checkedAt`, which is the churn the zero-churn rule above is
|
|
942
1025
|
about.
|
|
943
1026
|
|
|
944
|
-
```js
|
|
1027
|
+
```js file=backend.js
|
|
945
1028
|
export const config = { scheduled: { interval: "15m" } };
|
|
946
1029
|
|
|
947
1030
|
const SHOW_DB = "show"; // the one database App.jsx live-queries
|
|
@@ -951,6 +1034,7 @@ const SHOW_DB = "show"; // the one database App.jsx live-queries
|
|
|
951
1034
|
let lastFinishedDay;
|
|
952
1035
|
|
|
953
1036
|
export async function scheduled(event, ctx) {
|
|
1037
|
+
ctx.log("debug", "digest tick", { at: event.scheduledTime }); // proof of life, before any return
|
|
954
1038
|
const settings = await ctx.db.get("schedule", { db: SHOW_DB });
|
|
955
1039
|
const zone = settings?.timeZone ?? "UTC"; // captured in the owner's browser
|
|
956
1040
|
const at = new Date(event.scheduledTime);
|
|
@@ -1016,7 +1100,13 @@ export async function scheduled(event, ctx) {
|
|
|
1016
1100
|
// The day stays claimed: the owner sees what went wrong and is not billed twice for it.
|
|
1017
1101
|
ctx.log("error", "[digest] draft failed after the day was claimed", { message: err.message });
|
|
1018
1102
|
await ctx.db.put(
|
|
1019
|
-
{
|
|
1103
|
+
{
|
|
1104
|
+
_id: "run:status",
|
|
1105
|
+
type: "runStatus",
|
|
1106
|
+
state: "failed",
|
|
1107
|
+
checkedAt: event.scheduledTime, // a second failure must still read as a new one
|
|
1108
|
+
message: "today's draft didn't finish",
|
|
1109
|
+
},
|
|
1020
1110
|
{ db: SHOW_DB }
|
|
1021
1111
|
);
|
|
1022
1112
|
}
|
|
@@ -1030,7 +1120,7 @@ including the owner's own — while still no-opping under admin mode, so a hand
|
|
|
1030
1120
|
repair runs the function and keeps the routing. `access.js` is where that is
|
|
1031
1121
|
said:
|
|
1032
1122
|
|
|
1033
|
-
```js
|
|
1123
|
+
```js file=access.js
|
|
1034
1124
|
// access.js
|
|
1035
1125
|
export default function (doc, oldDoc, user, ctx) {
|
|
1036
1126
|
if (doc.type === "runState" || doc.type === "runStatus") {
|
|
@@ -1042,11 +1132,24 @@ export default function (doc, oldDoc, user, ctx) {
|
|
|
1042
1132
|
}
|
|
1043
1133
|
```
|
|
1044
1134
|
|
|
1045
|
-
App.jsx
|
|
1046
|
-
|
|
1135
|
+
App.jsx writes the `schedule` document that tick reads — the hour the owner
|
|
1136
|
+
picked and, beside it, the zone their own browser reported — and reads the same
|
|
1137
|
+
database back, rendering the status beside the drafts so a failed run and a good
|
|
1138
|
+
one never look alike:
|
|
1139
|
+
|
|
1140
|
+
```jsx file=App.jsx
|
|
1141
|
+
const { useLiveQuery, database } = useFireproof("show");
|
|
1142
|
+
|
|
1143
|
+
async function saveSchedule(hour) {
|
|
1144
|
+
// The hour is theirs and so is the zone: capture it here, store it beside the hour.
|
|
1145
|
+
await database.put({
|
|
1146
|
+
_id: "schedule",
|
|
1147
|
+
type: "schedule",
|
|
1148
|
+
hour,
|
|
1149
|
+
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1047
1152
|
|
|
1048
|
-
```jsx
|
|
1049
|
-
const { useLiveQuery } = useFireproof("show");
|
|
1050
1153
|
const { docs: statusDocs } = useLiveQuery("type", { key: "runStatus" });
|
|
1051
1154
|
const run = statusDocs.find((doc) => doc._id === "run:status");
|
|
1052
1155
|
// …
|
|
@@ -1066,7 +1169,7 @@ mirrors each note into a per-author `activity` entry (acting as that author —
|
|
|
1066
1169
|
same privilege, just automated), and a `scheduled` sweep maintains one `digest`
|
|
1067
1170
|
doc that `access.js` restricts to the **owner**, so no user can forge it.
|
|
1068
1171
|
|
|
1069
|
-
```js
|
|
1172
|
+
```js file=backend.js
|
|
1070
1173
|
export const config = { scheduled: { interval: "15m" } };
|
|
1071
1174
|
|
|
1072
1175
|
export async function onChange(event, ctx) {
|
|
@@ -1078,6 +1181,7 @@ export async function onChange(event, ctx) {
|
|
|
1078
1181
|
}
|
|
1079
1182
|
|
|
1080
1183
|
export async function scheduled(event, ctx) {
|
|
1184
|
+
ctx.log("debug", "digest sweep", { at: event.scheduledTime }); // proof of life, before any return
|
|
1081
1185
|
// Acts as the app owner — access.js allows `digest` docs for the owner only,
|
|
1082
1186
|
// making this document genuinely server-controlled.
|
|
1083
1187
|
//
|
package/llms/bluesky.md
CHANGED
|
@@ -135,7 +135,7 @@ person can act on — "no data" is not.
|
|
|
135
135
|
|
|
136
136
|
## Complete backend.js
|
|
137
137
|
|
|
138
|
-
```js
|
|
138
|
+
```js file=backend.js app=bluesky-browser
|
|
139
139
|
const XRPC = "https://public.api.bsky.app/xrpc";
|
|
140
140
|
const DB = "bluesky";
|
|
141
141
|
|
|
@@ -253,7 +253,7 @@ export async function fetch(request, ctx) {
|
|
|
253
253
|
A profile card, its follow graph, and the account's posts — one handle in, no
|
|
254
254
|
account of the visitor's involved.
|
|
255
255
|
|
|
256
|
-
```jsx
|
|
256
|
+
```jsx file=App.jsx app=bluesky-browser
|
|
257
257
|
import React, { useState } from "react";
|
|
258
258
|
|
|
259
259
|
export default function App() {
|
package/llms/calendar.md
CHANGED
|
@@ -110,7 +110,7 @@ button is simply there once the user has subscribable content.
|
|
|
110
110
|
there means the Subscribe link never renders, even though the mint effect and
|
|
111
111
|
the feed itself work.
|
|
112
112
|
|
|
113
|
-
```jsx
|
|
113
|
+
```jsx file=App.jsx app=calendar-feed
|
|
114
114
|
// access.js needs a `caltoken` branch routing the doc to the owner's PRIVATE
|
|
115
115
|
// channel (like notes) — the token is the secret.
|
|
116
116
|
const { docs: calTokens } = useLiveQuery(byTypeUser, { key: ["caltoken", me.userHandle] });
|
|
@@ -153,7 +153,7 @@ the button tooltip.
|
|
|
153
153
|
"2026-07-31", time: "18:00:00", endtime: "", title, venue, url } }` — adapt
|
|
154
154
|
the doc shape, db name, and timezone to the app.
|
|
155
155
|
|
|
156
|
-
```js
|
|
156
|
+
```js file=backend.js app=calendar-feed
|
|
157
157
|
export const config = { scheduled: { interval: "1m" } };
|
|
158
158
|
|
|
159
159
|
const TZ = "America/Los_Angeles"; // the app's local timezone
|
package/llms/callai.md
CHANGED
|
@@ -62,7 +62,7 @@ Send an image by passing the prompt as an array of content parts: an `image_url`
|
|
|
62
62
|
|
|
63
63
|
The prompt is the content of one user message, so a plain string means exactly one text part and the array is that same message's parts. Read the picture the user picked with a `FileReader` and hand over the data URL it produces:
|
|
64
64
|
|
|
65
|
-
```javascript
|
|
65
|
+
```javascript file=App.jsx
|
|
66
66
|
import { callAI } from "call-ai";
|
|
67
67
|
|
|
68
68
|
function readAsDataUrl(file) {
|
package/llms/connections.md
CHANGED
|
@@ -31,7 +31,7 @@ a public profile) is an ordinary `ctx.fetch` and needs no connection.
|
|
|
31
31
|
|
|
32
32
|
### 1. Declare it — `backend.js`
|
|
33
33
|
|
|
34
|
-
```js
|
|
34
|
+
```js file=backend.js app=instagram-connection
|
|
35
35
|
export const config = {
|
|
36
36
|
connections: [
|
|
37
37
|
{
|
|
@@ -70,7 +70,7 @@ for the rest.
|
|
|
70
70
|
|
|
71
71
|
### 2. Ask for it — `App.jsx`
|
|
72
72
|
|
|
73
|
-
```jsx
|
|
73
|
+
```jsx file=App.jsx app=instagram-connection
|
|
74
74
|
import { useConnection } from "use-vibes";
|
|
75
75
|
|
|
76
76
|
function ConnectInstagram() {
|
|
@@ -112,7 +112,7 @@ the platform's Settings page, not in your app.
|
|
|
112
112
|
|
|
113
113
|
### 3. Read it — `backend.js`
|
|
114
114
|
|
|
115
|
-
```js
|
|
115
|
+
```js file=backend.js app=instagram-connection
|
|
116
116
|
import { instagram, connectionState } from "@vibes.diy/social";
|
|
117
117
|
|
|
118
118
|
export async function fetch(request, ctx) {
|
package/llms/create-vibe.md
CHANGED
|
@@ -24,7 +24,7 @@ never automatically from an effect, a timer, or straight out of an async `callAI
|
|
|
24
24
|
resolution. Gate it behind an explicit "Create my ___" button the user presses
|
|
25
25
|
when they're ready.
|
|
26
26
|
|
|
27
|
-
```jsx
|
|
27
|
+
```jsx file=App.jsx
|
|
28
28
|
import React from "react";
|
|
29
29
|
import { createVibe } from "use-vibes";
|
|
30
30
|
|
|
@@ -63,7 +63,7 @@ Let the user edit each slide inline and present full-screen.`;
|
|
|
63
63
|
|
|
64
64
|
## Full pattern — interview, then hand off
|
|
65
65
|
|
|
66
|
-
```jsx
|
|
66
|
+
```jsx file=App.jsx
|
|
67
67
|
import React from "react";
|
|
68
68
|
import { callAI } from "call-ai";
|
|
69
69
|
import { createVibe } from "use-vibes";
|
package/llms/d3.md
CHANGED
package/llms/fireproof.md
CHANGED
|
@@ -29,7 +29,7 @@ This complete app shows Fireproof's core: `useFireproof` gives you hooks, `useDo
|
|
|
29
29
|
|
|
30
30
|
App.jsx
|
|
31
31
|
|
|
32
|
-
```jsx
|
|
32
|
+
```jsx file=App.jsx
|
|
33
33
|
import React from "react";
|
|
34
34
|
import { useFireproof } from "use-fireproof";
|
|
35
35
|
|
|
@@ -112,7 +112,7 @@ When an app ships with default content — starter slides, template rows, exampl
|
|
|
112
112
|
|
|
113
113
|
The trap is the live-query empty state. `useLiveQuery` renders empty before data arrives, so `docs.length === 0` is briefly true on **every** fresh load — and a `useRef`/`useState` "already seeded" flag only guards the current session, never the next reload, a second device, or another collaborator. With **auto-generated** `_id`s, each of those re-seeds the full set and duplicate copies pile up without bound. Deterministic `_id`s collapse every re-seed back onto the same N documents.
|
|
114
114
|
|
|
115
|
-
```jsx
|
|
115
|
+
```jsx file=App.jsx
|
|
116
116
|
import React from "react";
|
|
117
117
|
import { useFireproof } from "use-fireproof";
|
|
118
118
|
import { useVibe } from "use-vibes";
|
|
@@ -442,7 +442,7 @@ Building an image uploader with `_files`:
|
|
|
442
442
|
|
|
443
443
|
App.jsx
|
|
444
444
|
|
|
445
|
-
```jsx
|
|
445
|
+
```jsx file=App.jsx
|
|
446
446
|
import React from "react";
|
|
447
447
|
import { useFireproof } from "use-fireproof";
|
|
448
448
|
|
|
@@ -576,7 +576,7 @@ App.jsx
|
|
|
576
576
|
|
|
577
577
|
Code listing for todo tracker App.jsx. Note the code ordering: hooks, then handlers, then classNames right before JSX.
|
|
578
578
|
|
|
579
|
-
```js
|
|
579
|
+
```js file=App.jsx
|
|
580
580
|
import React from "react";
|
|
581
581
|
import { useFireproof } from "use-fireproof";
|
|
582
582
|
|
package/llms/image-gen.md
CHANGED
package/llms/p5.md
CHANGED
package/llms/spotify.md
CHANGED
|
@@ -158,7 +158,7 @@ the app looks broken rather than misconfigured.
|
|
|
158
158
|
|
|
159
159
|
## Complete backend.js
|
|
160
160
|
|
|
161
|
-
```js
|
|
161
|
+
```js file=backend.js app=spotify-browser
|
|
162
162
|
const TOKEN_URL = "https://accounts.spotify.com/api/token";
|
|
163
163
|
const API = "https://api.spotify.com/v1";
|
|
164
164
|
|
|
@@ -301,7 +301,7 @@ function artistRow(a) {
|
|
|
301
301
|
|
|
302
302
|
## App.jsx sketch
|
|
303
303
|
|
|
304
|
-
```jsx
|
|
304
|
+
```jsx file=App.jsx app=spotify-browser
|
|
305
305
|
import React, { useState } from "react";
|
|
306
306
|
|
|
307
307
|
const SETUP_STEPS = [
|
package/llms/three-js.md
CHANGED
package/llms/use-vibe.md
CHANGED
|
@@ -26,7 +26,7 @@ Never stamp author fields from `me` while the verdict is pending — `me` is nul
|
|
|
26
26
|
|
|
27
27
|
Gate every write affordance on `can.*`. Hold it while `pending`, and write your own copy when denied. Never branch write permission on `viewer`, `access.hasRole()`/`access.hasChannel()`, or document fields — those drift from what `access.js` actually does. Rendering **other** users (authors, rosters) is `useViewer()`'s `<ViewerTag userHandle={...} />`, not `useVibe`. The current viewer's own pill and the "signed in as" label are system chrome in the Vibes Switch (the logo) — don't build them into the app. Asking an anonymous visitor to sign in is a different thing and it IS yours: `useViewer().requestLogin()`, at the moment their work becomes worth keeping (see use-viewer docs). A resolved denial is one such moment — write the sign-in copy in your app's own words and wire it to that callable, never to a sentence about the logo. (The one exception is inline avatar self-edit: a guarded no-prop `{viewer && <ViewerTag />}` lets any signed-in member change their own photo in place — see use-viewer docs.)
|
|
28
28
|
|
|
29
|
-
```jsx
|
|
29
|
+
```jsx file=App.jsx
|
|
30
30
|
import { useVibe } from "use-vibes";
|
|
31
31
|
|
|
32
32
|
function PromptBar({ database }) {
|
|
@@ -38,9 +38,14 @@ function PromptBar({ database }) {
|
|
|
38
38
|
if (!v.ok) return <p className="muted">Join the board to add a tile.</p>;
|
|
39
39
|
// 3. allowed
|
|
40
40
|
return (
|
|
41
|
-
<form
|
|
41
|
+
<form
|
|
42
|
+
onSubmit={(e) => {
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
database.put({ type: "tile", authorHandle: me?.userHandle, text: e.target.tile.value });
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
42
47
|
{/* no current-user pill — identity + sign-in live in the Vibes Switch (the logo) */}
|
|
43
|
-
<input placeholder="Add a tile…" />
|
|
48
|
+
<input name="tile" placeholder="Add a tile…" />
|
|
44
49
|
<button type="submit">Post</button>
|
|
45
50
|
</form>
|
|
46
51
|
);
|
package/llms/use-viewer.md
CHANGED
package/llms/webxr.md
CHANGED
|
@@ -417,7 +417,7 @@ for (let i = 0; i < 200; i++) {
|
|
|
417
417
|
|
|
418
418
|
A floating galaxy of 2000 particles with a custom-shader core sphere, navigable in VR. Session data stored in Fireproof.
|
|
419
419
|
|
|
420
|
-
```javascript
|
|
420
|
+
```javascript file=App.jsx
|
|
421
421
|
import * as BABYLON from "@babylonjs/core";
|
|
422
422
|
import React, { useEffect, useRef } from "react";
|
|
423
423
|
import { useFireproof } from "use-fireproof";
|
package/llms/youtube.md
CHANGED
|
@@ -250,7 +250,7 @@ nothing they can act on, and the four cases above each have a different fix.
|
|
|
250
250
|
|
|
251
251
|
## Complete backend.js
|
|
252
252
|
|
|
253
|
-
```js
|
|
253
|
+
```js file=backend.js app=youtube-dashboard
|
|
254
254
|
export const config = {
|
|
255
255
|
scheduled: { interval: "1h" },
|
|
256
256
|
// The visitor path below reads the snapshot docs on the fetch lane, which
|
|
@@ -507,7 +507,7 @@ export async function scheduled(event, ctx) {
|
|
|
507
507
|
|
|
508
508
|
## App.jsx sketch
|
|
509
509
|
|
|
510
|
-
```jsx
|
|
510
|
+
```jsx file=App.jsx app=youtube-dashboard
|
|
511
511
|
import React, { useState } from "react";
|
|
512
512
|
import { useFireproof } from "use-fireproof";
|
|
513
513
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibes.diy/prompts",
|
|
3
|
-
"version": "14.1.
|
|
3
|
+
"version": "14.1.39",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"exports": {
|
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
"license": "Apache-2.0",
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@adviser/cement": "~0.5.34",
|
|
37
|
-
"@vibes.diy/call-ai-v2": "14.1.
|
|
38
|
-
"@vibes.diy/identity": "14.1.
|
|
39
|
-
"@vibes.diy/use-vibes-types": "14.1.
|
|
37
|
+
"@vibes.diy/call-ai-v2": "14.1.39",
|
|
38
|
+
"@vibes.diy/identity": "14.1.39",
|
|
39
|
+
"@vibes.diy/use-vibes-types": "14.1.39",
|
|
40
40
|
"arktype": "~2.2.3",
|
|
41
41
|
"json-schema-faker": "~0.6.3"
|
|
42
42
|
},
|