make major bonus updates
This commit is contained in:
@@ -18,10 +18,10 @@
|
||||
|
||||
## Auth
|
||||
|
||||
| Role | Auth | Session | Record in |
|
||||
| ---------------- | -------------------------- | --------------------------- | -------------------- |
|
||||
| Admin (parent) | PB email+pass | 24hr JWT | `fam_admins` |
|
||||
| Member (child) | Invite code + device token | `device_token` cookie only | `members` |
|
||||
| Role | Auth | Session | Record in |
|
||||
| -------------- | -------------------------- | -------------------------- | ------------ |
|
||||
| Admin (parent) | PB email+pass | 24hr JWT | `fam_admins` |
|
||||
| Member (child) | Invite code + device token | `device_token` cookie only | `members` |
|
||||
|
||||
- **Admins** (parents) have a PB auth record + `fam_admins` record. They authenticate via email/password login, get a session cookie (`session`) with `{ famId, userId, famSlug, memberName, role: "parent" }`.
|
||||
- **Members** (children) exist only in the `members` collection. They authenticate via invite code + device token (SHA-256 hashed). The `device_token` cookie is set on join; no session cookie.
|
||||
@@ -61,11 +61,13 @@
|
||||
## Data Flow
|
||||
|
||||
### Reads (both roles)
|
||||
|
||||
- **Parent (admin):** `famStore.init()` fetches all collections via PB SDK (authenticated via `pb_token` cookie).
|
||||
- **Child (member):** `famStore.init()` fetches all collections via PB SDK — **unauthenticated/anonymous**. All family-scoped collections have public `listRule` / `viewRule` (empty string = allow all), so reads work without any auth. PB SDK `.subscribe()` also works anonymously for public collections.
|
||||
- **TopNav season pills:** Read from `famStore.seasons` — reactive, no extra fetches needed.
|
||||
|
||||
### Writes (both roles go through Hono proxy)
|
||||
|
||||
- **Chore toggle:** Browser → Hono proxy → PB (auth via device token or admin JWT)
|
||||
- **Admin CRUD:** Form actions → Hono proxy → PB (admin JWT via `sessionHeaders`)
|
||||
- **Member updates:** Browser → Hono proxy → PB (auth via `x-device-token` + `x-device-famid`)
|
||||
@@ -75,6 +77,7 @@
|
||||
- **WhatsApp:** Deferred — Hono CRON handler has pluggable notification interface
|
||||
|
||||
### UI reactivity
|
||||
|
||||
- Svelte `$state` / `$derived` / `$effect`
|
||||
- PB SDK `.subscribe()` for realtime multi-user sync (public reads → works for both roles)
|
||||
- `famStore.applyRecord()` for instant optimistic UI feedback from form actions
|
||||
@@ -84,15 +87,16 @@
|
||||
Env vars must be declared in `frontend/src/env.ts` using `defineEnvVars` from `@sveltejs/kit/hooks`:
|
||||
|
||||
```ts
|
||||
import { defineEnvVars } from '@sveltejs/kit/hooks';
|
||||
import { defineEnvVars } from "@sveltejs/kit/hooks";
|
||||
export const variables = defineEnvVars({
|
||||
DEBUG_RECORD_ID: {},
|
||||
SERVER_IP: {public: true},
|
||||
PB_PORT: {public: true}
|
||||
SERVER_IP: { public: true },
|
||||
PB_PORT: { public: true },
|
||||
});
|
||||
```
|
||||
|
||||
Only vars with `{public: true}` are exposed to client-side code via `$app/env/public`. If you need a new client-side env var (e.g., `PUBLIC_PB_URL`), you must:
|
||||
|
||||
1. Add it to `.env` with `PUBLIC_` prefix
|
||||
2. Add it to `frontend/src/env.ts` with `{public: true}`
|
||||
|
||||
@@ -101,6 +105,7 @@ The actual values come from `.env` (symlinked from project root at `frontend/.en
|
||||
## ⚠️ CRITICAL: Reactivity Pattern (NEVER use `$effect` to sync from famStore)
|
||||
|
||||
**Do NOT do this:**
|
||||
|
||||
```svelte
|
||||
<!-- ❌ WRONG: $effect syncing local state from famStore fights Svelte reactivity -->
|
||||
let items = $state(data.items);
|
||||
@@ -111,21 +116,45 @@ $effect(() => {
|
||||
```
|
||||
|
||||
**Do this instead:**
|
||||
|
||||
```svelte
|
||||
<!-- ✅ RIGHT: Initialize from famStore, mutate via applyRecord in callbacks -->
|
||||
let items = $state(famStore.initialized ? famStore.items : (data.items || []));
|
||||
```
|
||||
|
||||
### ⚠️ CRITICAL: `$state` vs `$derived` for famStore collections
|
||||
|
||||
**When a page reads data that should update via SSE (realtime from other users/actions), you MUST use `$derived` — NOT `$state`.**
|
||||
|
||||
This is the most commonly missed rule. Agents repeatedly initialize collections from `famStore` with `$state`, which captures a **frozen snapshot** at init time. When PocketBase pushes a record via SSE → `famStore.applyRecord()`, the `$state` variable never reacts — the UI stays stale until a page refresh.
|
||||
|
||||
**Rule: if the data should update without a refresh, use `$derived`.**
|
||||
|
||||
```svelte
|
||||
// ❌ WRONG — frozen snapshot, won't react to SSE updates
|
||||
let configs = $state(famStore.initialized ? famStore.bonusConfigs : data.configs);
|
||||
|
||||
// ✅ RIGHT — reactive, updates when famStore changes via SSE
|
||||
let configs = $derived(
|
||||
famStore.initialized ? famStore.bonusConfigs : (data.configs || [])
|
||||
);
|
||||
```
|
||||
|
||||
**The only exception:** high-frequency member actions (like chore toggles) that need **optimistic UI** before the server responds. Those use `$state` + `famStore.applyRecord()` for instant feedback, then reconcile on the server response. See `applyRecord` pattern below.
|
||||
|
||||
**All other pages (admin CRUD, bonuses, rewards, settings, etc.) must use `$derived`** so that SSE updates flow through `famStore` → `$derived` → UI automatically.
|
||||
|
||||
## Update Patterns
|
||||
|
||||
Two patterns based on who's acting:
|
||||
|
||||
| Pattern | Who | Frequency | Sensitivity | Optimistic? | Auth |
|
||||
|---------|-----|-----------|-------------|-------------|------|
|
||||
| Direct `fetch` + `memberApi` | Member | High (chore toggles) | None | Yes (instant UI, reconcile on response) | `x-device-token` header |
|
||||
| Form action | Admin | Low (CRUD) | High (settings, members) | No — form is server-side, wait for round trip | httpOnly `session` cookie |
|
||||
| Pattern | Who | Frequency | Sensitivity | Optimistic? | Auth |
|
||||
| ---------------------------- | ------ | -------------------- | ------------------------ | --------------------------------------------- | ------------------------- |
|
||||
| Direct `fetch` + `memberApi` | Member | High (chore toggles) | None | Yes (instant UI, reconcile on response) | `x-device-token` header |
|
||||
| Form action | Admin | Low (CRUD) | High (settings, members) | No — form is server-side, wait for round trip | httpOnly `session` cookie |
|
||||
|
||||
**Member direct fetch** — optimistic UI via local state mutation, reconciled on response:
|
||||
|
||||
```svelte
|
||||
let completions = $state(data.completions)
|
||||
async function toggle(chore) {
|
||||
|
||||
Reference in New Issue
Block a user