16 Commits

Author SHA1 Message Date
489fc3ec87 feat(fase-17): deploy script HEAD-tag check + tag column in deploy log
deploy-erosi.sh now runs `git describe --exact-match --tags HEAD` up
front. When HEAD is not at a tag, prints a yellow warning and sleeps
5s so the operator can Ctrl-C and tag before the rsync starts (no
abort — keeps untagged hotfix deploys possible). When a tag is
present, surfaces it in the banner next to the SHA.

`.deploys.log` gains a 4th tab-separated column: the tag (empty when
absent). The 3-column compat for pre-Fase-17 rows is preserved — the
rollback script's existing `cut -f1/-f2/-f3` keeps working, and `cut
-f4` returns the empty string for the older rows.

rollback-erosi.sh:
- `--list` reformats each row through awk so the tag is visible (with
  "(no tag)" placeholder for older rows), falling back to the raw log
  when awk isn't on PATH.
- The target banner shows `git tag: <tag>` when present.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 01:21:23 +02:00
1638d2f6e8 deploy(prod): NOTIFY pgrst reload schema after migrations + after rollback
Post-deploy, PostgREST keeps its pre-migration pg_proc snapshot and
404s any RPC added in the just-applied migrations until the rest
container restarts. Confirmed on 2026-05-18 after migration 026
(set_item_frequency_weight + purge_item_frequency) — the "Purgar"
button in /collective/manage returned "Could not find the function
public.purge_item_frequency in the schema cache".

`NOTIFY pgrst, 'reload schema'` triggers the same reload path the
container would do on restart, but without a 2s blip in flight requests.
Added at the end of the migration loop in deploy-erosi.sh, and at the
end of the rebuild step in rollback-erosi.sh (rollback can change RPC
shapes too).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 00:15:06 +02:00
ae4fd45b99 deploy(prod): pre-deploy DB backup + paired code/DB rollback skill
Every `just deploy` now takes a pg_dumpall to
/opt/colectivo/backups/predeploy-<ts>-<sha>.sql.gz BEFORE rebuilding the
app or applying migrations, aborts the deploy if the dump is < 1 KiB,
and (on success) appends `<iso-ts> \t <sha> \t <backup-file>` to
/opt/colectivo/.deploys.log on ambrosio. Keeps the newest 10 backups
(prunes older predeploy-* files).

New `infra/scripts/rollback-erosi.sh` reads .deploys.log and pairs a
code rollback with a DB restore atomically:

  just rollback-list          # show recent deploys
  just rollback               # roll back to N-1
  just rollback-to <sha>      # roll back to a specific deploy
  just rollback-code          # roll back code only, keep current DB

Rollback safety:
- 5-second abort window.
- Verifies the target SHA exists locally + the backup is still on prod
  (warns if it's been pruned past the 10-deploy window).
- Uses a temporary git worktree so the user's working tree isn't
  disturbed.
- Stops app/auth/rest/realtime/storage before the gunzip|psql restore.
- Rebuilds the app image at the rolled-back SHA with the same GIT_SHA
  build-arg path the deploy uses (so __APP_COMMIT__ in the bundle
  matches the running code).
- Does NOT write a new .deploys.log entry — rollback is intentionally
  not a deploy event; the next `just deploy` is from current HEAD.
- Storage volume (/var/lib/storage user uploads) is NOT rolled back.

Justfile `deploy` recipe repointed from the stale `deploy.sh` (which
referenced GHCR pull) to `deploy-erosi.sh` (the active prod path).

New project-scoped skill: `.claude/skills/deploy/SKILL.md` documents the
flow + safety boundaries for Claude-assisted invocations. `.gitignore`
keeps `.claude/settings.local.json` ignored but tracks `.claude/skills/`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 14:57:25 +02:00
38b2be61ef feat(fase-14): version chip in /settings + GIT_SHA bake-through (14.3)
`apps/web/vite.config.ts` now injects three build-time constants via
`define`: `__APP_VERSION__` (root package.json), `__APP_COMMIT__`
(GIT_SHA env > local `git rev-parse` > 'dev'), `__BUILD_TIME__` (ISO
timestamp at build). They're declared in `apps/web/src/global.d.ts`
and re-exported through `$lib/version` — components consume the named
exports rather than the magic globals so an accidental tree-shake
would break the test, not the page silently.

`/settings` gains an "About" section at the bottom: a small chip
`v{version} · {commit} · {date}` in muted text. Not interactive,
purely diagnostic — useful when a user reports a bug from a stale
PWA install.

Deploy pipeline:
  - `apps/web/Dockerfile` accepts a `GIT_SHA` build arg and exports
    it as an env var for the SvelteKit build step.
  - `infra/docker-compose.erosi.yml` forwards `${GIT_SHA:-dev}` into
    the build args.
  - `infra/scripts/deploy-erosi.sh` captures `git rev-parse --short
    HEAD` locally (the deploy host has .git; the remote doesn't —
    rsync excludes it) and forwards it via `ssh ... env GIT_SHA=…`
    so the remote `docker compose build` sees it.

V-01/V-02 vitest: stub the three globals via `vi.stubGlobal` (vitest
doesn't run through the main vite.config so `define` isn't applied),
assert the named exports thread through and never collapse to
undefined or the 'dev' sentinel under a real build.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 07:09:15 +02:00
b1858542d0 feat(fase-13): server_admins + admin_actions audit log model
Migration 024 introduces the orthogonal global role and the append-only
audit log. server_admins is a separate table (not a column on users) so
that promote/revoke don't require a JWT re-issue; is_server_admin() is a
STABLE SECURITY DEFINER helper matching the shape of is_admin/is_member.

admin_actions has only a SELECT policy (admin-only) — no INSERT/UPDATE/
DELETE policies, so only the SECURITY DEFINER RPCs in migration 025 can
write to it. actor_id FK uses RESTRICT so the audit trail outlives
admins.

Bootstrap via infra/db-init/10-server-admin-seed.sh on first volume
init (reads SERVER_ADMIN_EMAIL); supabase/seed.sql also pre-seeds Ana
for dev/test because docker-entrypoint-initdb.d does not re-fire on
long-lived dev volumes. Documented in .env.erosi.example.

20 new pgTAP assertions cover schema shape, RLS posture, FK behaviour,
and the SECURITY DEFINER + STABLE attributes on is_server_admin().

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 04:52:18 +02:00
d4a781ddcf deploy(prod): swap NetBird-Traefik labels for off-stack proxy + internal Caddy
The stack no longer talks to any specific external proxy. Removed traefik.*
labels and the shared `traefik` external network from kong + app, and dropped
TRAEFIK_NETWORK / TRAEFIK_ENTRYPOINT / TRAEFIK_CERTRESOLVER from the .env
template and the deploy-script bootstrap.

Internal edge is a `caddy:2-alpine` container on the `colectivo` network,
publishing host port 3000. `infra/Caddyfile.erosi` does the path split:
/rest|/auth|/realtime|/storage|/graphql|/pg -> kong:8000, everything else
-> app:3000. Global block sets `auto_https off` + `admin off` so :3000 is
the only listener.

Whatever proxy fronts the host (NetBird's Traefik, nginx, anything) just
terminates TLS for erosi.limonia.net and forwards plain HTTP to
ambrosio:3000. Off-stack and out of this repo.

Docs (CLAUDE.md + docs/deployment.md) updated to describe the two-layer
edge. Includes the gotcha that bind-mounted file edits (Caddyfile) need
`docker compose restart caddy` since `up -d` won't recreate the container
on file-only changes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 22:26:06 +02:00
33b32cae4a deploy(prod): swap host Caddy for NetBird-Traefik labels + external Keycloak
Ambrosio's TLS is now handled by the Traefik that NetBird's self-hosted
installer deploys. kong + app attach to that Traefik network and expose
themselves via Docker-provider labels (erosi-kong priority 100 for
Supabase API paths, erosi-app priority 1 for everything else on
erosi.limonia.net). No container publishes host ports; host Caddy is
out of the deploy path permanently.

Keycloak is no longer bundled — PUBLIC_KEYCLOAK_URL points at an
external IdP. The deploy script writes .env with FILL_IN_* placeholders
for PUBLIC_KEYCLOAK_URL, KEYCLOAK_CLIENT_SECRET, and TRAEFIK_NETWORK,
and fails fast until they are filled.

New domain: erosi.limonia.net. realm-export.erosi.json is retained
as reference for the external Keycloak operator; it is no longer
imported by this stack.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 23:49:10 +02:00
3d85d0a50e fix(deploy): stop docker compose exec -T from draining the heredoc
Symptom observed twice (migrations 012 and 013): deploy-erosi.sh ran the
migration loop's first iteration (the `CREATE TABLE IF NOT EXISTS
_applied_migrations` step) then silently skipped every file in
supabase/migrations/*.sql without printing a single skip/apply line. Had
to `psql -f <new-migration>` by hand on ambrosio after every deploy.

Root cause: the deploy uses `ssh host bash -s << 'REMOTE' ... REMOTE` so
the outer bash on the remote reads its own script from stdin. Any
`docker compose exec -T db psql ...` inside that script — particularly
inside a command substitution like `already=$(docker compose exec -T ...)`
— inherits that stdin and consumes it, eating the rest of the heredoc.

Fix: pass `</dev/null` (or the migration file for the actual `apply`
step) to every `docker compose exec -T` call so docker gets an empty /
file-scoped stdin instead of the parent's heredoc.

Verified on ambrosio: deploy now prints all 13 `skip ... (applied)` lines
as expected, and will apply new migrations going forward without manual
intervention.

- CLAUDE.md: gotcha #20 documenting the pattern; applies to any
  heredoc-delivered remote script using `docker compose exec -T` or
  `kubectl exec`.
2026-04-15 00:31:00 +02:00
74e919aa71 feat(deploy): ambrosio production stack (erosi.oier.ovh)
Self-contained compose for a single-VPS deploy behind the host's Caddy.
App + Supabase API share erosi.oier.ovh; Keycloak on auth.oier.ovh.

- infra/docker-compose.erosi.yml — full stack bound to 127.0.0.1 only
  (db, auth, rest, realtime, storage, imgproxy, kong, meta, keycloak, app)
- infra/caddy/erosi.Caddyfile — host-Caddy snippet with TLS + path routing
  (/auth /rest /storage /realtime /graphql → kong, rest → app)
- infra/scripts/deploy-erosi.sh — rsync + first-run secret generation +
  build + migrations + Caddy snippet install
- keycloak/realm-export.erosi.json — prod redirect URIs, registration on,
  client secret as __KEYCLOAK_CLIENT_SECRET__ placeholder (deploy sub'd)
- .env.erosi.example — env template with placeholders

Supporting fixes to make the deploy work on a clean Supabase-postgres volume:

- infra/db-init/00-role-passwords.sh — rewrote as idempotent bootstrap:
  CREATE the Supabase service roles if missing, set passwords on pre-existing
  ones, enable pg_cron/pgcrypto/uuid-ossp, create auth/storage/graphql_public/
  _realtime/realtime schemas, create empty supabase_realtime publication,
  set per-role search_path (auth→auth, storage→storage). Old version only
  ALTERed passwords and relied on the roles already existing — worked for a
  grandfathered dev volume, failed on a fresh prod init.

- infra/db-init/00-role-passwords.sql — removed (folded into .sh).

- apps/web/vite.config.ts — PWA strategy generateSW (was injectManifest).
  The plugin's injectManifest hardcodes the SW source filename to
  service-worker.js and ignores the filename: 'sw.ts' override, making the
  production build fail. generateSW's auto-generated precache-only SW is
  functionally equivalent to our 5-line src/sw.ts.

Tested end-to-end on ambrosio: all 9 containers up, 11 migrations applied,
https://erosi.oier.ovh returns 200, Keycloak OIDC discovery serves the
correct issuer, /auth/v1/settings lists Keycloak as the sole external
provider.
2026-04-14 19:41: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
5a63e5fc4a feat(dev): expose full auth stack on LAN IP for on-device preview
The dev stack previously mixed two hostnames (`localhost` in some places,
`keycloak` in others). That only worked because the laptop's /etc/hosts
maps `keycloak` to 127.0.0.1. Phones, tablets, or a second laptop had no
such mapping, so the OAuth flow broke the moment you tried to log in from
any non-laptop device.

Switched the SPA, Kong, GoTrue, and Keycloak client to all agree on a
single external host — the laptop's LAN IP (192.168.1.167) — so the full
Keycloak → GoTrue → Supabase → SvelteKit round-trip works from any device
on the same Wi-Fi.

Changes
  .env (root, ungitignored): PUBLIC_SUPABASE_URL / PUBLIC_KEYCLOAK_URL /
    PUBLIC_APP_URL now use the LAN IP (updated locally; tracked via
    apps/web/.env.development below).
  apps/web/.env.development: mirror the LAN IP so `$env/static/public`
    resolves the same on laptop and phone.
  apps/web/vite.config.ts: `server.host: true` — bind Vite to 0.0.0.0.
  apps/web/playwright.config.ts: `baseURL: PUBLIC_APP_URL ?? localhost`
    so E2E uses the same origin the stack is configured with.
  apps/web/tests/fixtures/login.ts:
    - wait for the collective button in the sidebar to render before
      returning (LAN-IP latency surfaced a pre-existing race in
      `handleCreate` where Enter fired before `$currentCollective`
      hydrated, silently no-op'ing).
    - derive expected app origin from PUBLIC_APP_URL.
  apps/web/tests/e2e/items.test.ts: scope D-04 delete assertion to the
    `[role="listitem"]` locator — with undoQueue enabled the toast text
    "Deleted <name>" also matches `getByText(itemName)` and shadowed the
    original check.
  infra/docker-compose.dev.yml:
    - GOTRUE_EXTERNAL_KEYCLOAK_URL and REDIRECT_URI now read from
      PUBLIC_KEYCLOAK_URL / PUBLIC_SUPABASE_URL so they follow the same
      host as the rest of the stack.
    - NEW: GOTRUE_URI_ALLOW_LIST with `/auth/callback` on both the LAN
      IP and localhost. Without an explicit allow-list GoTrue rejected
      `redirect_to=.../auth/callback` and fell back to SITE_URL root,
      stranding `?code=` at `/` and re-triggering signIn → infinite loop.
  keycloak/realm-export.json: `colectivo-web` client gains LAN-IP
    redirectUris and webOrigins (alongside the existing localhost
    entries). Persisted + live-applied via admin API.
  .gitignore: add .claude/ (per-project scheduler runtime state).

Verification
  just test-all → 224 passed, 4 skipped:
    34 pgTAP
    140 Vitest integration + 2 skipped (Realtime presence, upstream bug)
    11 Vitest unit
    39 Playwright + 2 skipped (mobile-swipe touch, WebKit-only)
  Phone sanity check: open http://192.168.1.167:5173 on a LAN device,
  log in as a seed user, full RLS-respecting session.

Caveats
  LAN IP (192.168.1.167) is DHCP-assigned — if it rotates, rerun the
  Keycloak admin API update (realm-export.json needs a new entry) and
  update the three PUBLIC_* URLs. Consider a Tailscale magic-DNS name
  as a stable replacement for the prod-deploy sprint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 16:09:32 +02:00
cb07f67a69 feat(realtime): full Fase 2b.0 test suite + infra hardening
Extend the 2b.0 red-phase coverage: presence, RLS isolation, offline-queue
contract, and Playwright scaffolds for the shopping-session UX. Only the
presence test is blocked on an upstream Realtime bug (documented below); all
other Realtime paths are proven end-to-end.

Tests
- realtime-postgres-changes.test.ts: now uses afterEach socket disconnect
  (Vitest singleFork kept Phoenix sockets alive between files, leaking state)
- realtime-isolation.test.ts (new): R-I-01 David (guest, same collective)
  receives events; R-I-02 Eva (non-member) receives NONE — RLS is enforced
  server-side per subscribed JWT
- realtime-presence.test.ts (new, describe.skip): two-client roster + leave
  assertions ready, gated on upstream bug
- sync-queue.test.ts (new, describe.skip): 7 placeholders pinning the
  apps/web/src/lib/sync/queue.ts contract (Q-01..Q-04 enqueue / in-order
  flush / retry; F-01..F-03 online-event flush + last-write-wins)
- apps/web/tests/e2e/{session,realtime,offline}.test.ts (new, describe.skip):
  S-01..S-03, R-E-01, R-E-02, O-01, O-02 pinning the Modo Compra UI contract
- vitest.config.ts: fileParallelism=false, drop singleFork so each file gets
  a fresh worker fork — prevents RealtimeClient singleton leakage

Infra
- db-init/00-role-passwords.sh: pre-create `realtime` schema with
  AUTHORIZATION supabase_admin. Realtime's per-tenant migrator creates tables
  INSIDE the schema but does not create the schema itself; without this the
  first tenant connect fails with "schema realtime does not exist"
- docker-compose.dev.yml: adopt the upstream supabase/supabase Realtime env
  shape — SEED_SELF_HOST=true + RUN_JANITOR=true + RLIMIT_NOFILE=10000 +
  drop the custom `command: eval seeds` (that bypasses the normal supervisor
  startup and was observed to cause GenServer crashes under load).
  Version pinned to v2.76.5 to match the official docker-compose.

Known upstream bug
- Realtime v2.76.5 and v2.83.0 both crash on presence_diff:
  `(UndefinedFunctionError) RealtimeChannel.handle_out/3 is undefined`.
  postgres_changes + isolation unaffected (they don't traverse handle_out).
  Presence tests stay `describe.skip` until a fixed upstream is released.

Totals: 59 Vitest passed (+ 9 skipped awaiting implementation/upstream),
16 pgTAP, 15 Playwright (+ 7 E2E scaffolded for 2b UI) = 90 green.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 02:55:55 +02:00
4c913cf495 feat(realtime): Fase 2b.0 infra + first R-series tests green
Enable postgres_changes broadcasts on shopping_items / shopping_lists and prove
the path end-to-end with Vitest Realtime tests and a pgTAP configuration check.

Infra changes that were silently blocking events
- Migration 007: shopping_items + shopping_lists added to supabase_realtime
  publication with REPLICA IDENTITY FULL (UPDATE/DELETE payloads need the full
  row so list_id-based filters work and RLS can evaluate DELETE against OLD)
- Realtime service: DB_USER=supabase_admin (superuser; supabase_replication_admin
  lacks CREATE on the `realtime` schema that the service auto-creates per tenant)
- Realtime service: SELF_HOST_TENANT_NAME=realtime so the seed tenant name
  matches the default supabase-js resolves for localhost URLs (was realtime-dev
  → TenantNotFound)

Tests
- pgTAP 004_realtime_publication.sql — P-01..P-04: publication membership and
  REPLICA IDENTITY FULL for both shopping tables
- Vitest realtime-postgres-changes.test.ts — R-01 INSERT broadcast,
  R-02 UPDATE carries full row, R-03 list_id filter isolates events
- test-utils realtime-helpers.ts — subscribePostgresChanges() returns a
  waitFor(predicate, timeout) helper that captures the SUBSCRIBED handshake
  before returning so mutations issued right after are not lost
- createClientAs now calls realtime.setAuth(token) so the server evaluates
  RLS against the test user's JWT
- Justfile test-db now globs supabase/tests/*.sql so new pgTAP files are picked
  up automatically

Totals: 54→57 Vitest, 12→16 pgTAP, 15 Playwright = 88 tests green.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 02:33:40 +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