Commit Graph

10 Commits

Author SHA1 Message Date
286f59225f feat(fase-12): nav gating + redirect guard + realtime publication
UI side:
  * DesktopSidebar / BottomTabBar filter their entries by $enabledSections,
    tag each entry with data-section, and BottomTabBar exposes a
    data-section-count attribute so the grid is observable from tests.
  * "lists" is force-shown unconditionally — the §12.3.4 always-one-
    landing rule covers the edge case where every layer says OFF.
  * (app)/+layout.svelte adds a $effect that, when the URL matches a
    disabled section (/tasks, /notes, /search), goto's /lists and shows
    a transient `section_disabled_for_collective` toast.
  * Root +layout.svelte exposes the supabase singleton on window.__sb
    in dev so the new Playwright spec can patch rows without a parallel
    client; dead-code-eliminated in production builds.

DB side:
  * Migration 023 grows by ALTER PUBLICATION supabase_realtime ADD TABLE
    public.users + public.collectives + REPLICA IDENTITY FULL on both.
    Without this membership the features.ts subscriptions get zero
    events and SV-02 (collective toggle → realtime → member's nav
    updates) silently fails. Caught while running the spec.
  * pgTAP 015 grows from 16 to 20 assertions to cover publication
    membership + REPLICA IDENTITY for both new realtime tables.

Paraglide messages: section_disabled_for_collective, the visibility
section titles + blurbs, section_label_* per SectionKey. Both en + es.

Playwright tests/e2e/section-visibility.test.ts (SV-01..SV-03):
  SV-01 user toggle → nav reflects → /tasks redirects to /lists
  SV-02 admin collective toggle → member sees it disappear in realtime
  SV-03 collective ON beats user OFF — per-collective resolution

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 04:10:19 +02:00
59c425f6f6 feat(fase-12): features store + integration coverage
Adds the client mirror of section_enabled():

  * SectionKey / FeatureFlags types + SECTION_KEYS constant in
    @colectivo/types so the SQL and TS sides share the same vocabulary.
  * apps/web/src/lib/stores/features.ts:
      - currentUserFeatures      writable, loaded by root layout
      - enabledSections          derived map { section → boolean }
      - enabledSectionList       ordered SectionKey[] visible to UI;
                                 forces 'lists' if every layer says OFF
      - setUserFeature           per-user toggle (optimistic + rollback)
      - setCollectiveFeature     admin toggle (rolls back on RLS deny)
      - realtime subscriptions   one channel for the user's row, one
                                 for the active collective; rebuilt
                                 whenever those targets change

Wired into the root layout: loadCurrentUserFeatures runs from inside
the existing setTimeout-deferred onAuthStateChange callback (the
documented gotcha), and currentCollective.subscribe drives
subscribeCollectiveFeatures so collective switches re-target the
realtime filter without an extra auth event.

loadUserCollectives + every other place that fabricates a Collective
object now carries feature_flags (defaulting to {} for freshly created
collectives) so the Collective interface stays sound.

Integration spec (6 tests, SV-01..SV-06) covers default-ON, user-only,
collective-beats-user, RLS writes (user_update_own + collectives_update),
and per-collective isolation for a user in two collectives.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 03:53:54 +02:00
378fcbe5f4 feat(fase-10): auto language detection on first login (10.8)
New $lib/utils/accept-language.ts parses Accept-Language strings
(server-style "es;q=0.9,en;q=0.8") and navigator.languages arrays,
returns 'en' or 'es'. Rule (plan §10.8.1): the highest-q-scored 'es'
entry wins if q >= 0.5; otherwise fall back to 'en'. Unknown
languages (e.g. fr) → 'en'. Pure function, 12 unit tests covering
bare tags, regional variants, q-score ordering, malformed input,
navigator.languages arrays, and the borderline q=0.5 case.

The root +layout.svelte calls maybeBootstrapLanguage() after each
SIGNED_IN: it only fires for genuinely new users (public.users row
created within the last 60s) whose language is still the default
('en'); it reads navigator.languages, runs the parser, and
UPDATEs public.users.language if the result is 'es'. setLanguageTag()
flips the live Paraglide runtime so the user lands on the next
page already in Spanish — no reload needed.

Established users keep their explicit choice (plan §10.8.5 / Riesgo
5). Server-side Accept-Language sniffing was rejected: the OIDC
dance lives outside SvelteKit (Keycloak ↔ GoTrue), so a hooks.server.ts
would never see those requests. navigator.languages is the
client-side equivalent and survives Paraglide's compile-time
tree-shaking.

LANG-01/02 (vitest integration): UPDATE public.users RLS contract
the bootstrap relies on — own row OK, other user's row rejected.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 02:30:09 +02:00
9592879ad7 feat(fase-9): wire ThemeToggle into /settings + cross-device theme sync
Closes 9.1.6 (settings UI), 9.1.7 (reactivity), 9.1.8 (Playwright E2E).

- /settings now has an "Appearance" section housing ThemeToggle. The
  onChange callback fires an UPDATE on public.users.theme so the choice
  follows the user across devices. The DB write is best-effort: a failed
  UPDATE does not roll back the immediate localStorage + <html data-theme>
  flip the toggle already performed.
- Root +layout.svelte calls initTheme() once on mount so the JS-side
  stores hydrate from localStorage (the inline anti-FOUC script in
  app.html has already painted the correct theme by this point — this
  just rebuilds the JS state on top so reactive consumers see it).
- (app)/+layout.svelte gains a one-shot effect that reads
  public.users.theme on first hydration and adopts the remote value when
  it differs from localStorage. Defers the PostgREST query through
  setTimeout(..., 0) per the Supabase auth-lock gotcha.
- tests/e2e/theme.test.ts adds T-01 (toggle persists across reload),
  T-02 (system mode follows emulated prefers-color-scheme), and T-03
  (refresh in dark mode never paints a light first frame, including on
  /logged-out which lives outside the (app) group and relies entirely
  on the inline app.html script).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 01:15:47 +02:00
f5c8cb68be feat(fase-7): collective-flow E2E coverage + fix 3 latent bugs
12 new Playwright tests closing the UI coverage gap in the collective
lifecycle (onboarding → invitation → admin manage). Writing them surfaced
three product-code bugs that have silently been present since Fase 1;
fixed as part of this phase.

Tests:
- tests/e2e/onboarding.test.ts       O-01..O-03 (Eva auto-redirect, happy
                                     path create, empty-name submit-disabled)
- tests/e2e/invitation.test.ts       I-01..I-05 (token generation, accept
                                     logged-out + logged-in, expired, used)
- tests/e2e/manage-collective.test.ts MC-02..MC-05 (promote, demote, remove
                                     member, generate pending invite)
- tests/fixtures/db.ts               resetEva, seedExpiredInvitation,
                                     seedUsedInvitation, restoreSeedMembership,
                                     countMembership — all via raw SQL so they
                                     don't depend on SUPABASE_SERVICE_ROLE_KEY

Bugs found & fixed:
- supabase/migrations/012_create_collective_rpc.sql: atomic
  `create_collective(name, emoji)` SECURITY DEFINER RPC. Fase 1 onboarding
  used `.insert(...).select().single()` which triggers the SELECT policy on
  the fresh row — creator isn't a member yet, so the INSERT was rejected with
  "row-level security" even though the INSERT policy passed. onboarding now
  calls the RPC which inserts both the collective AND the creator-as-admin
  membership in one transaction, bypassing RLS. F-08 in rls-isolation.test.ts
  has been silently masking this bug (only the spoof branch was asserted).

- routes/+layout.svelte: post-login redirect now reads sessionStorage
  `pendingInvitationToken` and routes back to /invitation/<token> instead of
  defaulting to /onboarding or /lists. The invitation page already stashed
  the token before kicking off Keycloak, but nothing read it back.

- routes/(app)/collective/manage/+page.svelte: loadMembers now subscribes
  to currentCollective so it re-runs when the store resolves after cold
  navigation. onMount alone races with the auth listener's first emit and
  the member list stayed empty on direct URL hits.

- routes/invitation/[token]/+page.svelte: awaits authLoading before deciding
  whether to redirect to login. Previously raced with the auth listener and
  triggered a redundant Keycloak round-trip for freshly-authenticated users.

Stable selectors added (Playwright):
- Onboarding: onboarding-create-tab, onboarding-join-tab,
  collective-name-input, collective-submit
- Manage: member-row-<uid>, role-select-<uid>, remove-member-<uid>,
  generate-invite, invite-link
- Invitation: invitation-accept, invitation-error (+ data-error-key attr)

Scope dropped from the plan — UI does not exist, would be feature work:
- O-04: "+ new collective" affordance in the sidebar.
- MC-01: rename-collective input in /collective/manage.
Both flagged as follow-ups in plan/fase-7-collective-flow-tests.md.

Test totals: 34 pgTAP + 140 Vitest integration + 15 Vitest unit + 58
Playwright + 1 gated rate-limit. 3 skipped (2 Realtime presence, 1 gated).
2026-04-14 22:33:33 +02:00
2db5aaac14 Fase 6: deploy readiness (PWA + invitation rate-limit + JWT rotation)
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>
2026-04-14 04:48:04 +02:00
f396897cb5 test: full test suite (Vitest + pgTAP + Playwright) + TDD plan restructure
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>
2026-04-13 01:40:16 +02:00
b297d253d9 fix(auth+pwa): resolve stuck-loading race condition and PWA build error
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>
2026-04-12 21:44:14 +02:00
7d91705fc2 feat(fase-1): auth, collective management, and DB migrations
- SQL migrations: users, collectives, collective_members, collective_invitations,
  full RLS policies, is_active_member/is_member/is_admin helpers,
  accept_invitation SECURITY DEFINER function, admin auto-promote trigger,
  avatars storage bucket
- Auth refactor: replace keycloak-js with Supabase OAuth (GoTrue OIDC proxy,
  Option B). signInWithOAuth({ provider: 'keycloak' }) + PKCE flow
- GoTrue config: GOTRUE_EXTERNAL_KEYCLOAK_URL + GOTRUE_EXTERNAL_KEYCLOAK_REDIRECT_URI
  added to docker-compose.dev.yml; Keycloak realm updated with GoTrue callback URI
- New routes: /auth/callback, /invitation/[token], /(app)/collective/manage
- Functional onboarding: create collective or join via invite link
- Full settings page: display name (debounced), avatar (initials/emoji/upload),
  language switcher, Keycloak account console link
- Components: Avatar.svelte (initials/emoji/upload with fallback),
  ImageCropper.svelte (cropperjs, 1:1 crop, lazy-loaded)
- i18n: added all Fase 1 message keys (en + es); renamed 'delete' → 'action_delete'
  (JS reserved word); fixed plugin-m-function-matcher major-version URL format
- Database types: manually seeded packages/types/src/database.ts from migrations
- .env.development: committed public dev defaults for SvelteKit type checking
- CLAUDE.md: documented /etc/hosts requirement for keycloak hostname

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 15:29:29 +02:00
973f9e413c Fase 0: scaffold monorepo, SvelteKit skeleton, and dev infrastructure
- Turborepo + pnpm workspaces with apps/web and packages/types
- SvelteKit app: Tailwind, Paraglide i18n (en/es), keycloak-js, Supabase client,
  auth/collective stores, PWA service worker skeleton, all route placeholders
- packages/types: domain types (User, Collective, ShoppingList, etc.) and
  database.ts placeholder for generated types
- Justfile with dev, db-*, kc-*, build, backup, restore, deploy recipes
- infra/docker-compose.dev.yml: Postgres 15, GoTrue, PostgREST, Realtime v2.83,
  Storage, Kong (port 8001), Studio, Keycloak 24
- infra/docker-compose.prod.yml, kong.yml, db-init scripts, backup/deploy scripts
- keycloak/realm-export.json with colectivo realm and 5 dev test users
- supabase/config.toml and seed.sql with sample collective and items
- GitHub Actions: ci.yml (lint+typecheck+build) and deploy.yml (GHCR + SSH)
- .env.example documenting all required variables
- Fixed docker-compose issues: Studio image tag, Kong port conflict (8001),
  internal role passwords init script, Realtime METRICS_JWT_SECRET/APP_NAME

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 14:37:51 +02:00