Adds a planned-phase doc for the next ask on top of the closed MVP2 cycle:
weighted suggestion ordering + exclude items already on the active list.
- Extends item_frequency (migration 026) with `weight integer` written via
two admin-only SECURITY DEFINER RPCs (set_item_frequency_weight +
purge_item_frequency). The client never writes weight directly — the
existing all-deny policies on item_frequency stay in place.
- fetchSuggestions reorders by `weight desc, use_count desc, last_used_at
desc` and accepts an excludeNames arg to drop already-added items from
the dropdown.
- UI lives in /collective/manage as a 3-way segmented control per row
(Hide / Normal / Boost mapping to -50 / 0 / 50). Admin writes; member
read-only; guest hidden.
Rebrand: MVP2 reopens from 6/6 ✅ to in-progress 6/7 ✅, with Fase 15
pending. Headers in fases 9–14 bump from "·N/6" + "fases 9 → 14" to
"·N/7" + "fases 9 → 15". README status table + index updated, CLAUDE.md
project status appends the Fase 15 pending paragraph.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Stamps the six planned post-MVP fases as the MVP2 cycle (matching the
`mvp2` branch name). Each plan file gets an "MVP2 · N/6" header. README
status + index tables group them under an MVP2 bracket. CLAUDE.md project
status reframes the planned line as MVP2 and clarifies that Fase 7+8
shipped as MVP cleanup, not MVP2.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds four planned-phase docs covering the next post-MVP roadmap:
- Fase 11 — item tags (collective-scoped) + drag-reorder UI for the
existing `shopping_items.sort_order` column. Migration 021.
- Fase 12 — section visibility via JSONB `feature_flags` on `users` and
`collectives`, with a `section_enabled()` SQL helper that resolves
precedence (collective > user > default ON). Migration 022.
- Fase 13 — `/admin` area with a global `server_admins` role, append-only
`admin_actions` audit log, RPCs for collective CRUD + member removal,
and server-level section-visibility defaults that sit above Fase 12.
Migrations 023+024. Depends on Fase 12.
- Fase 14 — PWA hardening: `onNeedRefresh` update toast, explicit offline
indicator + sync queue badge + conflict surface, `__APP_VERSION__` and
`__APP_COMMIT__` baked at build with a chip in `/settings`. No migration.
Depends on Fase 9.3 (`sync_conflicts` wiring).
Also bumps CLAUDE.md project status with the planned-fases pointer, and
brings README.md status + index tables in line with Fase 7, 8, 9, 10
(previously missing) plus the four new ones.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes the deferrals from Fase 4 that gated a public deploy of the MVP.
PWA install
- Custom SW at apps/web/src/sw.ts (named sw.ts, not service-worker.ts, so
SvelteKit doesn't intercept it and block Workbox imports).
- vite.config.ts switched to injectManifest strategy with precache of the
built shell; no runtime caching of Supabase (offline writes stay on the
existing $lib/sync pending_ops queue).
- Root +layout.svelte onMount() calls registerSW({ immediate: true }) from
virtual:pwa-register — the plugin does not auto-register.
- Web manifest with 192/512/512-maskable icons + iOS meta tags
(apple-mobile-web-app-capable, apple-touch-icon, etc.) in app.html.
- Placeholder icons generated via ImageMagick; replace before public launch
(noted in apps/web/static/icons/README.md).
Kong rate-limit
- /rest/v1/collective_invitations POST: 20/hour per Authorization header.
Anti-spam on invitation creation is the only rate-limit that survived —
auth rate-limits were dropped during execution (see plan §6.3): Keycloak
already provides bruteForceProtected=true on the realm, and stacking a
Kong limit on /auth/v1/token throttled legitimate PKCE code exchanges
during E2E.
- Added 'rate-limiting' to KONG_PLUGINS in docker-compose.dev.yml.
- Discovered: strip_path=true on a full-table path sends "/" upstream and
PostgREST rejects POST to root with PGRST117. Route carries a
request-transformer plugin that rewrites the URI back.
JWT rotation + prod template
- infra/scripts/rotate-jwt.sh signs anon + service_role JWTs with a fresh
48-byte secret via openssl. Prints three values for the operator to
paste; does not touch files.
- Dev secret rotated as a dry-run. Updated both .env (local, gitignored)
and apps/web/.env.development. Existing sessions are invalidated — all
users must re-login once.
- .env.production.example committed with placeholders + rotation notes.
Lighthouse + RLS audit tooling
- `just lighthouse` boots the prod build + Supabase stack and runs
lighthouse CLI against PWA/A11y/Best-Practices.
- `just rls-audit` runs infra/scripts/rls-audit.sh which signs an Eva JWT
(non-member) and GETs 8 REST endpoints — all must return []. Complements
the Vitest rls-audit.test.ts suite.
Tests
- 236 green: 34 pgTAP + 140 Vitest integration + 15 unit + 46 Playwright.
- 1 rate-limit test gated behind RUN_RATE_LIMIT_TESTS=1 (Kong counters are
shared state); run via `just test-rate-limit` which force-recreates Kong
first to reset counters.
- 3 skipped in `just test-all` (2 Realtime presence upstream bug, 1 gated
rate-limit).
Deliberately out of scope: push notifications, CI ephemeral Docker, final
production icons, runtime caching of Supabase (SyncBanner + pending_ops
already covers offline).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New plan file closing out the Fase 4 deferrals with specifics confirmed
by user on 2026-04-14:
- 6.1 PWA injectManifest, MINIMAL scope (precache shell only; Supabase
GETs stay network-only since $lib/sync + SyncBanner already cover
offline writes + the offline indicator)
- 6.2 manifest.webmanifest + neutral placeholder icons (192/512/180 +
maskable 512) + iOS meta tags
- 6.3 Kong rate-limiting — 10/min + 60/hour on /auth/v1/token per IP,
30/min on /auth/v1/authorize per IP, 20/hour on invitations POST
per authenticated user
- 6.4 JWT rotation — dev rotate now + .env.production.example template
+ infra/scripts/rotate-jwt.sh
- 6.5 Lighthouse PWA ≥ 90 + scripted rls-audit.sh (curl edition of
Vitest's rls-audit.test.ts)
- 6.Z verification
Explicitly out of scope (recorded at bottom of the plan):
- Push notifications (no current use case)
- Ephemeral-Docker CI (deferred)
- Runtime caching of Supabase in the SW (deferred)
- Final production icon art (placeholders land now; real art before
public launch)
README phase table + index updated; plan total now 11–16 weeks.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Problem
The phone-preview path (http://192.168.1.167:5173) is a non-secure
origin. `window.crypto.randomUUID` is gated behind secure contexts
(HTTPS / localhost / 127.0.0.1 / file://) so it's undefined there,
and every optimistic-id call site crashed handleAdd with
`TypeError: crypto.randomUUID is not a function`.
Fix
apps/web/src/lib/utils/id.ts — generateId() uses crypto.randomUUID()
when available, otherwise falls back to crypto.getRandomValues()
+ RFC 4122 §4.4 byte assembly. getRandomValues IS exposed on
insecure contexts (unlike subtle), so the fallback is v4-compliant
and cryptographically acceptable for client-side optimistic ids.
Replaced crypto.randomUUID() at every call site:
stores/lists.ts, stores/tasks.ts, sync/queue.ts, sync/undoQueue.ts
routes/(app)/lists/[id]/+page.svelte
routes/(app)/tasks/[id]/+page.svelte
CLAUDE.md gotcha added: never call crypto.randomUUID() directly —
always import generateId from $lib/utils/id.
Tests (written first, confirmed failing on main)
src/lib/utils/id.test.ts (4 tests)
U-01 returns UUID v4 when randomUUID is available
U-02 falls back when randomUUID is undefined
U-03 50 fallback-generated ids are all unique
U-04 prefers randomUUID when present (doesn't drop into fallback)
tests/e2e/insecure-context.test.ts (1 test)
INS-01 stubs crypto.randomUUID = undefined via addInitScript,
drives the add-item flow, confirms the row renders (if the
old call site were still there, handleAdd would throw).
Verification
just test-all → exit 0, 233 green, 2 skipped:
34 pgTAP (unchanged)
140 Vitest integration + 2 skipped presence (unchanged)
15 Vitest unit (was 11, +4 generateId)
44 Playwright (was 43, +1 INS-01)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fase 5 is ✅ complete in automatable scope after the last five feature
commits (d6cea51, 4cc5f69, d14d6cd, 1cb408a, f3e8234). 228 tests green,
4 skipped. Deferred: WebKit-dependent swipe/long-press E2E, card
presence avatars, max-w-2xl reading width.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The duplicate design/DESIGN.md was removed; the authoritative "Monolith
Editorial" spec lives at design/slate_collective/DESIGN.md. Update
CLAUDE.md, README.md, and plan/fase-5-mobile-ux.md accordingly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add plan/fase-5-mobile-ux.md with 7-chunk breakdown:
5.0 mobile tests (Playwright mobile-shell/masthead/swipe-delete + Vitest helpers)
5.1 responsive shell (MobileTopBar + BottomTabBar + MobileDrawer)
5.2 ScreenMasthead + px-4/md:px-6 pass
5.3 /lists mobile cards
5.4 /lists/[id] rows + red-zone swipe-delete
5.5 undoQueue store + UndoToast
5.6 scrollable frequency chips
5.7 tasks/notes/search shell + masthead
5.Z verify 211 + new M-series green
- Explicit gap analysis vs. current app + deliberate exclusions
(no priority badges, no voice search, no fake meta like "Dairy · Weekly")
- Index design/ directory: Stitch mockups per screen (mobile + desktop variants),
plus the "Monolith Editorial" DESIGN.md directing tonal shifts, masthead
typography, glassmorphism, and the no-line rule
- Relocate DESIGN.md under design/slate_collective/DESIGN.md (old entry point) —
the current design direction lives at design/DESIGN.md
- Reference design/ from CLAUDE.md + add Fase 5 to README phase index
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fase 2b closes the critical-path product differentiator. Two sessions on the
same list see each other's changes in real time, mutations survive network
drops and sync when reconnected, and a dedicated full-screen shopping view
optimises the in-store experience.
2b.1 Realtime
- `$lib/stores/realtimeSync.ts` wraps `postgres_changes` with an `applyItemEvent`
reducer (INSERT/UPDATE/DELETE → next items[]) and a `subscribeToList` helper
that awaits the SUBSCRIBED handshake before returning
- `/lists/[id]` subscribes in onMount, unsubscribes in onDestroy. Uses a
`pendingTempIds` set to deduplicate own-mutation echoes against optimistic
rows (matches by name+created_by+sort_order)
- Client-generated UUIDs for new inserts make the path idempotent: if the
server response is lost and we retry, the second POST hits PK-conflict
which the queue treats as success
- CHECKED section div now carries role="listitem" so Playwright locators
follow the item across sections
2b.2 Offline queue
- `$lib/sync/queue.ts` — SyncQueue class backed by IndexedDB (idb), FIFO flush,
MAX_ATTEMPTS=5 retry budget, PK-conflict-as-success short-circuit
- `$lib/sync/index.ts` — app-wide singleton, window.online listener flushes
automatically, hydrateSyncState() at page load restores pendingOpsCount
- `$lib/stores/syncStatus.ts` — derived store (offline | syncing | synced)
tracking navigator.onLine + queue depth
- SyncBanner component renders the offline/syncing indicator in the detail
and session views
- handleAdd enqueues on failure instead of reverting, so an offline mutation
keeps its optimistic row and syncs on reconnect
2b.3 Modo Compra
- `/lists/[id]/session/+page.svelte` — full-screen overlay (fixed inset-0
z-50) that covers the sidebar while keeping the normal auth routing.
56×56 toggles, flip animation shuffling items between TO BUY / CHECKED,
Finish Shopping confirmation modal → completeList → goto('/lists')
- Link from `/lists/[id]` header (data-testid=start-session) with the
new `list_start_session` message
Testing infra
- apps/web gets its own Vitest config (jsdom + fake-indexeddb/auto) and a
new `pnpm test:unit` script. `just test-all` now chains pgTAP → integration
→ unit → e2e so a single command is the gate.
- packages/test-utils/tests/sync-queue.test.ts (placeholder scaffold) removed —
replaced by `apps/web/src/lib/sync/queue.test.ts` co-located with the module
Totals: 103 tests green
16 pgTAP
59 Vitest integration (in packages/test-utils)
6 Vitest unit (in apps/web — the SyncQueue)
22 Playwright E2E (5 auth + 4 lists + 6 items + 2 realtime + 2 offline + 3 session)
2 skipped (realtime-presence — upstream bug, unchanged)
Documented in plan/fase-2b and CLAUDE.md.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a 4-layer test stack covering RLS, triggers, and UI flows for Fases 0–2a,
then restructure every plan file so future fases start with tests and end with
a verification gate.
Test suite
- packages/test-utils: Vitest integration tests signing HS256 JWTs via jose so
each test acts as a specific seed user (createClientAs + createAdminClient)
- supabase/tests: pgTAP for accept_invitation(), item_frequency trigger, and
promote-on-admin-leave; each file self-installs pgtap extension
- apps/web/tests: Playwright E2E with live Keycloak login per test (storageState
caching doesn't rehydrate Supabase's session state reliably)
- just test-all chains the three suites; test-db forwards POSTGRES_PASSWORD
as PGPASSWORD with ON_ERROR_STOP=1 so failures abort the chain
Supabase auth gotcha
- PostgREST queries inside onAuthStateChange deadlock on GoTrue's navigator.locks
auth lock (getAccessToken → getSession → initializePromise waits on the same
lock that's held during event dispatch). Fix is two defenses: a pass-through
lock in $lib/supabase, and a setTimeout(0) defer in root +layout.svelte to
push loadUserCollectives out of the callback's microtask chain. Either alone
is insufficient; both together unblock the Playwright suite.
Env key rotation
- apps/web/.env.development had a stale demo anon key signed with a different
secret than root .env; Vite inlined that into the browser bundle so Kong (which
uses the root .env value) rejected every request with 401. Aligned the two
files and added a memory entry to flag this for the next rotation.
Plan restructure (TDD)
- Every fase now opens with X.0 Tests primero and closes with X.Z Verificación
final. Completed fases (0, 1, 2a) show the pattern retroactively with the
tests that currently cover them; pending fases (2b, 3, 4) list the tests to
write before implementation.
Docs
- CLAUDE.md status line reports 54 + 12 + 15 = 81 green tests, adds gotchas
#11 (auth-lock deadlock) and #12 (no storageState caching)
- README.md adds a TDD methodology section and the test-all command
- .gitignore excludes Playwright's generated reports and auth state
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove all 1px border separators (sidebar border-r, page header border-b,
section border-t, member list divide-y, invite card border). Separation is
now achieved via background color shifts and whitespace per the "No-Line" rule.
- Fix sidebar: bg-surface-raised vs bg-background creates tonal separation.
- Correct dark background to #020617 (slate-950) per spec.
- Add surface-container-low, text-primary/secondary/muted, destructive tokens.
- Fix CSS variable color space: all tokens are RGB triplets; switch Tailwind
config and app.css from hsl() to rgb() to prevent yellow/warm color bleed.
- Replace emoji nav icons with Lucide icons at 16px / 1.5 stroke-width.
- Apply Masthead typography (uppercase Label-MD + Title-LG) to all page headers
and section labels across lists, tasks, notes, search, settings, manage.
- Destructive action (sign out) uses bg-destructive per spec.
- Add `just serve` command: builds and runs the prod Node server at :3000
against the dev Docker stack for production build testing.
- Add nav_collective i18n key (en + es).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Auth: `getSession()` in a SvelteKit load function races with Supabase's async
localStorage init and can return null for a valid session, triggering a spurious
login() redirect. Moved auth redirect to (app)/+layout.svelte via $effect, which
correctly waits for onAuthStateChange to fire. Also fixed collective data never
loading on page refresh (INITIAL_SESSION event, not SIGNED_IN).
PWA: SvelteKit blocks Workbox imports in src/service-worker.ts, preventing
@vite-pwa/sveltekit from finding the compiled SW output. Switched to generateSW
strategy (plugin auto-generates the SW). Custom SW deferred to Fase 2b using
src/sw.ts (a filename SvelteKit does not intercept).
Docs: added gotchas 6–8 to CLAUDE.md covering both root causes so they are not
reintroduced. Updated plan/fase-2b with the sw.ts workaround note.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>