commit f5903ef442dbbe8c64c5abc5ec2ccfa9efa749b5 Author: Oier Bravo Urtasun Date: Sun Apr 12 06:35:07 2026 +0200 Add planning docs, design system, and Stitch mockups - CLAUDE.md: project guidance (stack, domain model, auth, i18n, gotchas) - DESIGN.md: full design system spec derived from Stitch mockups (slate monochrome, Inter, shadcn-svelte, screen patterns for all 5 views) - README.md: development plan with Justfile commands - analysis/: functional spec (domain model, use cases, business rules) - plan/: phase-by-phase implementation plans (Fase 0–4) - .stitch/designs/: generated mockups (lists, shopping session, notes, tasks, list detail with frequency chips) Co-Authored-By: Claude Sonnet 4.6 diff --git a/.stitch/designs/01-lists-view.png b/.stitch/designs/01-lists-view.png new file mode 100644 index 0000000..bd798ca Binary files /dev/null and b/.stitch/designs/01-lists-view.png differ diff --git a/.stitch/designs/02-shopping-session.png b/.stitch/designs/02-shopping-session.png new file mode 100644 index 0000000..1697017 Binary files /dev/null and b/.stitch/designs/02-shopping-session.png differ diff --git a/.stitch/designs/03-notes-view.png b/.stitch/designs/03-notes-view.png new file mode 100644 index 0000000..35ada67 Binary files /dev/null and b/.stitch/designs/03-notes-view.png differ diff --git a/.stitch/designs/04-tasks-view.png b/.stitch/designs/04-tasks-view.png new file mode 100644 index 0000000..59220c4 Binary files /dev/null and b/.stitch/designs/04-tasks-view.png differ diff --git a/.stitch/designs/05-list-detail.png b/.stitch/designs/05-list-detail.png new file mode 100644 index 0000000..9fdaaae Binary files /dev/null and b/.stitch/designs/05-list-detail.png differ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..622371f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,152 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Code Language + +**All code must be written in English.** This applies to variable names, function names, type names, database column names, SQL identifiers, comments, and any other code artifact. The UI may display text in any language, but the code layer is always English. + +## Project Status + +This repository is in the **planning phase**. No application code exists yet. The repo contains: +- `README.md` — full development plan, confirmed tech stack, Justfile reference, and technical warnings +- `analysis/analisis-funcional.md` — complete functional specification (domain model, use cases, data models, business rules) +- `plan/fase-*.md` — phase-by-phase implementation plans (Fase 0 through Fase 4) + +Start by reading these documents before implementing anything. + +## Planned Stack + +| Layer | Technology | +|---|---| +| Frontend / PWA | SvelteKit 2 + `@vite-pwa/sveltekit` + Workbox | +| Auth / IdP | Keycloak self-hosted (OIDC/OAuth2) | +| Backend / BaaS | Supabase self-hosted (GoTrue + Realtime + Storage + Kong) | +| Database | PostgreSQL 15 (via Supabase) | +| Offline storage | IndexedDB via `idb` | +| Monorepo | Turborepo + pnpm workspaces | +| Infra | Docker Compose (dev + prod) + nginx (prod, managed externally) | +| CI/CD | GitHub Actions | + +## Planned Repository Layout + +``` +apps/web/src/lib/ + supabase.ts # Supabase singleton client + auth.ts # keycloak-js OIDC client + session helpers + stores/ # Svelte global stores + sync/ # offline queue + conflict resolution + components/ # reusable UI components +apps/web/src/routes/ # SvelteKit file-based routes +packages/types/src/ + database.ts # types generated from Supabase schema (never edit manually) + domain.ts # domain types (Collective, ShoppingList, Item, etc.) +supabase/ + migrations/ # versioned SQL migrations + seed.sql # dev test data (5 Keycloak users + sample collective) + functions/ # Edge Functions (invitations, etc.) + config.toml # Supabase CLI config + Keycloak OIDC +keycloak/ + realm-export.json # "colectivo" realm — imported on dev startup +infra/ + docker-compose.dev.yml + docker-compose.prod.yml + scripts/backup.sh / backup-cron.sh / restore.sh / deploy.sh +``` + +## Development Commands (Justfile) + +```bash +just dev # start docker-compose.dev.yml + SvelteKit dev server +just dev-stop # stop local environment +just db-reset # drop + migrate + seed (dev) +just db-migrate # apply pending migrations (dev) +just db-types # regenerate TS types from Supabase schema → packages/types +just kc-export # export Keycloak realm → keycloak/realm-export.json +just kc-open # open Keycloak Admin Console (localhost:8080) +just deploy # run infra/scripts/deploy.sh (prod) +just backup supabase # immediate manual backup +just restore supabase # restore a specific dump +just logs # docker compose logs -f (prod) +``` + +## Domain Model + +The central organizing unit is the **Collective** (household group), not the individual user. All content belongs to the Collective. + +**Roles:** `admin` (full CRUD + member management) | `member` (full CRUD on content) | `guest` (read-only) + +**Core entities:** `Collective` → `CollectiveMember`, `ShoppingList` → `ShoppingItem`, `TaskList` → `Task`, `Note`, `CollectiveInvitation` + +**Key field names (English):** `collective_id`, `list_id`, `is_checked`, `checked_by`, `checked_at`, `created_by`, `created_at`, `completed_at`, `sort_order`, `display_name`, `avatar_type`, `avatar_emoji`, `avatar_url`, `language` + +**Key business rules:** +- Only one active shopping session per list at a time. +- Completing a list keeps items as history; it can be "reset" (uncheck all → back to active). +- Trash retains deleted items/notes for 7 days before permanent deletion. +- If the sole admin deletes their account, the system auto-promotes the oldest member before allowing deletion. + +## Auth Architecture (Keycloak → Supabase) + +Auth flows through Keycloak as the OIDC IdP. Supabase validates Keycloak-issued JWTs. Supabase RLS uses `auth.uid()` which reads the `sub` claim from the JWT (must be a UUID v4 — Keycloak default). + +The Keycloak RS256 public key must be set as `JWT_SECRET` in Supabase. **Rotating Keycloak signing keys requires updating Supabase `JWT_SECRET` simultaneously.** + +Logout must invalidate both sides: call `keycloak.logout()` (invalidates Keycloak session) AND clear the Supabase client locally. + +## Sync Strategy + +| Content | Mechanism | Target latency | +|---|---|---| +| Items in active shopping session | WebSocket (Supabase Realtime) | < 1 second | +| Lists, tasks (outside session) | SSE / polling | < 5 seconds | +| Notes | Polling | < 30 seconds | + +**Offline-first:** all operations execute locally first (IndexedDB), then sync in background. Conflict resolution is last-write-wins. Conflicts are logged to a `sync_conflicts` table for debugging — no CRDT in MVP. + +## Critical Platform Gotchas + +**iOS Safari / PWA:** +- `keycloak-js` silent token refresh uses iframes (`checkLoginIframe`) — Safari blocks third-party cookies in iframes. **Set `checkLoginIframe: false`** and use `updateToken()` manually instead. +- Background Sync API is not supported in Safari. Implement a manual fallback using the `online` event. +- Push notifications require iOS 16.4+ and the PWA must be installed to the home screen. +- Test the shopping session mode on a real iPhone during development, not only in DevTools. + +**nginx (prod) — WebSocket for Realtime:** +- The `/realtime/` location block **must come before** the `/` block. +- `proxy_read_timeout 3600s` is required on the Realtime block — without it, nginx closes WebSocket connections after 60 seconds, forcing continuous reconnects during active shopping sessions. +- CSP `connect-src` must include both `https://` and `wss://` for the API domain. + +**Keycloak behind nginx:** +- `X-Forwarded-Host` and `X-Forwarded-Port` headers are mandatory. Without them, Keycloak builds redirect URIs with the internal port (8080) instead of 443, breaking the OIDC flow. + +**Supabase Realtime self-hosted:** +- Check `MAX_REPLICATION_SLOTS` in PostgreSQL and `REALTIME_MAX_CONNECTIONS` before going to production. +- Presence subscriptions are more resource-intensive than Postgres Changes — limit them to lists with an active session. + +## Dev Test Users + +Defined in `keycloak/realm-export.json` and `supabase/seed.sql`. All use password `test1234`. + +| User | Email | Role in test collective | +|---|---|---| +| `ana` | ana@dev.local | Admin | +| `borja` | borja@dev.local | Member | +| `carmen` | carmen@dev.local | Member | +| `david` | david@dev.local | Guest | +| `eva` | eva@dev.local | (no collective — for onboarding testing) | + +## Internationalisation + +Library: **Paraglide JS** (`@inlang/paraglide-sveltekit`). Compile-time, zero runtime overhead, tree-shaken per language. + +- Message files: `messages/en.json` (default) and `messages/es.json` +- Language detection order: `users.language` (if logged in) → `Accept-Language` header → `en` +- Language switching: update `users.language` in Supabase + call `setLanguageTag()` — no page reload +- **Never hardcode visible user-facing strings in components.** All UI text goes through Paraglide messages. + +Supported languages: `en`, `es`. + +## TypeScript Types + +`packages/types/src/database.ts` is **generated** by `just db-types` (`supabase gen types typescript`). Never edit it manually. Domain-level types go in `packages/types/src/domain.ts`. diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..4283b55 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,442 @@ +# DESIGN.md + +Design reference for the Colectivo app. Stack: SvelteKit + shadcn-svelte + Tailwind CSS. + +Generated mockups are in `.stitch/designs/`. Stitch project ID: `10316144241804324155`. + +--- + +## Visual Style + +### Direction + +Clean and minimal. Content is the UI — no decorative elements, no gradients, no shadows unless they carry meaning. Generous whitespace. Every element earns its place. + +### Color + +Monochromatic slate palette. No accent color — contrast and weight carry hierarchy. + +``` +--background: slate-50 / slate-950 +--surface: white / slate-900 +--surface-raised: slate-100 / slate-800 +--border: slate-200 / slate-700 +--text-primary: slate-900 / slate-50 +--text-secondary: slate-500 / slate-400 +--text-disabled: slate-300 / slate-600 +--destructive: red-500 / red-400 ← only color in the UI +``` + +Interactive elements (buttons, checkboxes, active states) use `slate-900` on light and `slate-50` on dark — inverting the surface. The only true color is destructive red. + +### Typography + +Font: **Inter** (or `system-ui` as fallback — acceptable on iOS/macOS). + +| Role | Size | Weight | Notes | +|---|---|---|---| +| Screen title | 20px | 600 | | +| Section label | 13px | 600 | Uppercase, `letter-spacing: 0.05em` | +| Body / list item | 16px | 400 | | +| Meta / secondary | 13px | 400 | `text-slate-500` | +| Badge / tag | 11px | 500 | Uppercase, `rounded-full` | +| Input | 16px | 400 | Min 16px prevents iOS zoom on focus | + +Line height: 1.5 for body, 1.2 for headings. No text smaller than 11px. + +### Spacing + +Base unit: 4px (Tailwind default). Prefer multiples of 4. Common values: + +- List item padding: `px-4 py-3` (min 48px touch target) +- Screen horizontal margin: `px-4` (mobile), `px-6` (desktop) +- Section gap: `gap-6` +- Between section label and content: `mt-2` +- Bottom nav / sticky input safe area: always account for `env(safe-area-inset-bottom)` + +### Iconography + +**Lucide** (ships with shadcn-svelte). Size `20px` inline, `24px` for nav and primary actions. Stroke width `1.5`. Never fill icons. + +### Dark Mode + +Class-based (`dark` on ``). Respect `prefers-color-scheme` on first load, then persist user preference in `localStorage`. shadcn-svelte's CSS variable system handles theming — no additional setup needed per component. + +--- + +## Component Library + +**shadcn-svelte** — copy-paste components built on bits-ui + Tailwind. No runtime dependency, full ownership of component code. + +### Conventions + +- Copy components into `apps/web/src/lib/components/ui/` (shadcn-svelte default) +- App-specific compositions go in `apps/web/src/lib/components/` (e.g. `ItemRow.svelte`, `ListCard.svelte`) +- Never modify shadcn-svelte primitives directly — compose or wrap them +- Use shadcn-svelte's `cn()` utility for conditional classes + +### Key components to install + +| Component | Used for | +|---|---| +| `Button` | All actions | +| `Input` | Add item, search, forms | +| `Checkbox` | Shopping items, tasks | +| `Sheet` | Mobile bottom drawers (item edit, member invite, avatar picker) | +| `Dialog` | Confirmations (delete list, dissolve collective) | +| `Popover` | Presence avatars tooltip | +| `Badge` | Priority tags (TODAY, URGENT), status labels (DRAFT, HIGH PRIORITY) | +| `Skeleton` | Loading states | +| `Sonner` | Toast notifications (sync errors, member joined, undo delete) | + +--- + +## Layout & Navigation + +### App Shell + +Two-column on desktop, single-column on mobile. + +``` +Mobile Desktop (lg+) +┌─────────────────────┐ ┌──────────┬──────────────────────┐ +│ Header │ │ │ Breadcrumb / Title │ +├─────────────────────┤ │ Sidebar │──────────────────────┤ +│ │ │ │ │ +│ Content │ │ │ Content │ +│ │ │ │ │ +├─────────────────────┤ │ User │ │ +│ Bottom Nav │ │ Info │ │ +└─────────────────────┘ └──────────┴──────────────────────┘ +``` + +### Sidebar (desktop) + +Fixed left sidebar, `surface-raised` background (vs `surface` main area — tonal shift, no border line). + +``` +┌──────────────┐ +│ Colectivo │ ← collective name, 14px/600 +├──────────────┤ +│ 🛒 Lists │ ← active item: slate-900 bg, white text +│ ✓ Tasks │ ← inactive: slate-500 text +│ 📄 Notes │ +│ 🔍 Search │ +│ │ +│ (spacer) │ +│ │ +│ ┌──┐ Name │ ← user avatar + display name +│ │AG│ Role │ ⚙ ← settings gear icon +└──────────────┘ +``` + +- Collective name at top, 14px/600 +- 4 nav items: Lists, Tasks, Notes, Search +- User avatar + display name + settings gear at bottom +- Width: `w-56` (`224px`) + +### Navigation (mobile) + +Fixed bottom tab bar, 4 tabs. Icons only — no text labels. Active icon: `slate-900`. Inactive: `slate-400`. Glassmorphism background: `backdrop-blur-md bg-white/80`. Respects `safe-area-inset-bottom`. + +| Tab | Icon | Route | +|---|---|---| +| Lists | `shopping-cart` | `/lists` | +| Tasks | `check-square` | `/tasks` | +| Notes | `file-text` | `/notes` | +| Search | `search` | `/search` | + +### Page Headers + +Desktop: breadcrumb-style context path above the title, in uppercase 11px/500 `text-slate-400`: + +``` +WORKSPACE OVERVIEW +Shared Lists +``` + +``` +PROJECT ATLAS / Tasks +``` + +Mobile: plain title with optional back arrow and overflow `⋯` menu on the right. + +### Responsive Breakpoints + +Tailwind defaults: `sm` (640), `md` (768), `lg` (1024). Sidebar appears at `lg`. Content max-width: `max-w-2xl` centered on large screens. + +--- + +## Screen Patterns + +### Lists View (`/lists`) + +Featured card for the primary/active list (full width, larger), secondary lists in a 2-column grid below. + +``` +WORKSPACE OVERVIEW +Shared Lists +┌─────────────────────────────────────┐ +│ Home 👤👤 │ ← featured card +│ 12 items · Updated 2h ago │ +│ [GROCERIES] [HOUSEHOLD] │ ← category tags, rounded-full +└─────────────────────────────────────┘ +┌──────────────┐ ┌──────────────┐ +│ Office │ │ Party │ +│ 5 items │ │ 26 items │ +│ 🗂 │ │ HIGH PRTY │ ← badge +└──────────────┘ └──────────────┘ +┌─────────────────────────────────────┐ +│ + Create a new shared list... [Create] │ ← sticky bottom input +└─────────────────────────────────────┘ +``` + +- Category tags on list cards: `rounded-full text-xs font-medium bg-slate-100 px-2 py-0.5` +- Presence avatars overlap on cards with active members + +### List Detail (`/lists/[id]`) + +Normal (non-session) view. Items with quantity controls. + +``` +ACTIVE LIST +Grocery List ⋯ +┌─────────────────────────────────────┐ +│ ☐ Honeycrisp Apples 6 + │ +│ ☐ Organic Baby Spinach 2 + │ +│ ☐ Whole milk 1L 2 + │ +│ ☐ Salted Butter 1 + │ +└─────────────────────────────────────┘ + + [MILK] [BREAD] [EGGS] [COFFEE] [APPLES] ← frequency chips +┌─────────────────────────────────────┐ +│ Add item (e.g. Greek Yogurt 500g) [+] │ +└─────────────────────────────────────┘ +``` + +- Quantity controls: `−` count `+` inline at the right of each row +- Frequency chips always visible above the input (not just on focus); active chip: `bg-slate-900 text-white`; inactive: `bg-slate-100 text-slate-700` + +### Shopping Session Mode (`/lists/[id]/session`) + +Full-screen takeover. No sidebar, no bottom nav. + +``` +✕ Compra semanal 👤👤 +───────────────────────────────────── +TO BUY + ☐ Whole milk 1L + ☐ Bread + ☐ Eggs 12 uds + ☐ Avocados + +CHECKED + ☑ Butter 250g (muted, struck through) + ☑ Olive Oil 1L + +───────────────────────────────────── +[ Finish shopping ● ] +``` + +- `TO BUY` and `CHECKED` are uppercase section labels, not divider lines +- Items slide from TO BUY to CHECKED on tap — no page scroll +- Minimum tap target: `min-h-[56px]` +- Presence avatars: `size-6 rounded-full`, overlapping, top-right of header +- "Finish shopping" button: full-width, `bg-slate-900 text-white`, with a subtle pulse indicator dot (●) + +### Tasks View (`/tasks`) + +Tasks grouped into named sections. Filter pills at the bottom. + +``` +PROJECT ATLAS / Tasks 👤👤 +───────────────────────────────────── +HIGH PRIORITY + ☐ Finalize Q4 strategy deck TODAY + ☐ Coordinate with design on monolith tokens URGENT + +PRODUCT DEVELOPMENT + ☐ Core API Integration + ☐ Review documentation for Auth endpoints (indented sub-item) + ☑ Map database schema to editorial structure (completed) + ☐ User testing: Editorial workflow + +BACKLOG + ☐ Update dependency libraries + +───────────────────────────────────── + [DAILY] [WEEKLY] [URGENT] ← filter pills +┌─────────────────────────────────────┐ +│ Add task to Tasks... [Save] │ +└─────────────────────────────────────┘ +``` + +- Section names are user-defined (not system-fixed) +- Priority badges on individual tasks: `text-xs uppercase text-slate-400` (TODAY, URGENT) +- Filter pills: same style as frequency chips; filter the visible tasks + +### Notes View (`/notes`) + +Notes displayed as cards in a masonry-style grid, grouped by user-defined categories. + +``` +WORKSPACE / NOTES 🔔 ⊞ 👤 +───────────────────────────────────── +CATEGORY +Meeting Notes +┌─────────────────┐ ┌─────────────────┐ +│ Q4 Product │ │ Sprint Retro: │ +│ Roadmap... │ │ Project Apollo │ +│ Oct 24, 2023 │ │ Oct 22, 2023 │ +└─────────────────┘ └─────────────────┘ + +CATEGORY +Project Ideas +┌─────────────────┐ +│ Minimalist │ +│ Portfolio... │ +└─────────────────┘ + +───────────────────────────────────── +│ Capture a new thought... # @ [SAVE] │ +``` + +- Cards: `bg-white rounded-md` on `surface-raised` background (tonal lift, no shadow) +- Category labels: uppercase section labels above each group +- Inline new-note input at the bottom (not a separate screen) + +--- + +## UX Patterns + +### Optimistic Updates + +All mutations update the local store immediately before the server responds. On error, revert and show a toast. Never block the UI waiting for a network response. + +### Offline Indicator + +A slim bar at the top of the content area (not the header) when offline: + +``` + No connection · Saving locally +``` + +`bg-slate-800 text-slate-100`, 36px tall. Mounts/unmounts immediately — no animation. + +### Frequency Chips (Add Item Input) + +Sticky input area at the bottom of list and task screens. Frequency chips are **always visible** above the input, not conditional on focus. + +``` + [MILK] [BREAD ●] [EGGS] [COFFEE] [APPLES] ← scrollable row +┌─────────────────────────────────────────────────┐ +│ Add item (e.g. Greek Yogurt 500g) [+] │ +└─────────────────────────────────────────────────┘ +``` + +- Chips: `rounded-full text-xs font-medium px-3 py-1 bg-slate-100` (inactive) / `bg-slate-900 text-white` (active/selected) +- Tapping a chip fills the input; user can still edit before confirming +- While typing, chips switch to prefix-filtered suggestions (max 10, same pill style) +- Confirm with `+` button or `Enter` + +### Swipe to Delete + +On list items and tasks. Swipe left reveals a red destructive zone: + +``` +┌────────────────────────────────────────┐ +│ ☐ Whole milk 1L ░░░░░░ │ ← item sliding left +│ [🗑] │ ← red-500 zone, trash icon only +└────────────────────────────────────────┘ +``` + +Full swipe: immediate deletion + Sonner toast "Deleted · Undo" (4 seconds). Partial swipe: snaps back. No confirmation dialog for single items. + +### Drag & Drop Reordering + +Handle (`grip-vertical` Lucide icon) on the left — always visible on desktop, revealed on long-press on mobile. Use `svelte-dnd-action`. During drag: lifted item has `shadow-md bg-white`, others compress to show insertion point. + +### Confirmations + +**Destructive, hard-to-reverse actions** (delete list, remove member, dissolve collective): `Dialog` with explicit description text. No shortcut. + +**Soft deletions** (single item, single task): swipe + Sonner undo. No dialog. + +### Avatar Component + +Three mutually exclusive states, all `rounded-full`. + +| State | Appearance | Size tokens | +|---|---|---| +| `initials` | Initials on `slate-700` bg, white 600-weight text | `size-6` presence, `size-8` lists, `size-16` settings | +| `emoji` | Emoji centered on `slate-700` bg | same | +| `upload` | ``, fallback to initials on error | same | + +Initials rule: first letter of first + last word in `display_name` ("Ana García" → "AG", "Borja" → "B"). + +### User Settings Screen (`/settings`) + +In the desktop sidebar shell. Content area: + +``` +← Settings + + ┌──────┐ + │ AG │ ← size-16 avatar + └──────┘ + Change avatar › + +DISPLAY NAME +┌────────────────────────────────┐ +│ Ana García │ ← auto-save, 500ms debounce +└────────────────────────────────┘ + Changes saved automatically ← slate-400 hint + +LANGUAGE + [ English ] [ Español ] ← pill segmented control + English selected: slate-900/white. Instant effect, no reload. + +ACCOUNT ← background shift (no line) + Email & password → ← external link to Keycloak + Managed in Keycloak ← slate-400 + +DANGER ZONE ← section label in red-500 + [ Delete account ] ← ghost red button +``` + +**Avatar sheet** — `Sheet` (bottom on mobile, dialog on desktop): + +- Segmented tabs: Initials | Emoji | Photo +- Emoji grid: 8 columns, ~40 emoji (🐸 🐼 🦊 🐨 🦁 🐯 🐻 🦋 🌵 🌻 🍀 🌈 ⭐ 🔥 💎 🎯 🎸 🎹 🎨 📚 ✈️ 🏔️ 🌊 🎭 🍕 🍜 🍣 ☕ 🍎 🥑 🍓 🎂 🏠 🚲 🛹 ⚽ 🏄 🧘 🎮 🤿) +- Photo upload: file picker → 1:1 crop in-browser → preview → confirm. Max 2MB. JPG/PNG/WebP. + +### Empty States + +Text-only, no illustrations. `text-slate-500`, centered in content area. + +``` +No lists yet. +Tap + to create one. +``` + +Two lines max. + +### Loading States + +`Skeleton` components matching the shape of the content (list rows, card grids). Spinner only for full-screen blocking operations (login, initial data fetch). + +--- + +## Stitch Mockup Inventory + +Screens in `.stitch/designs/` — project `10316144241804324155`: + +| File | Screen | Status | +|---|---|---| +| `01-lists-view.png` | Lists View | ✅ | +| `02-shopping-session.png` | Shopping Session | ✅ | +| `03-notes-view.png` | Notes View | ✅ | +| `04-tasks-view.png` | Tasks View | ✅ | +| `05-list-detail.png` | List Detail with frequency chips | ✅ | +| — | User Settings | ⏳ pending generation | +| — | Collective Management | ⏳ pending generation | diff --git a/README.md b/README.md new file mode 100644 index 0000000..66a556e --- /dev/null +++ b/README.md @@ -0,0 +1,192 @@ +# Plan de Desarrollo — App Gestión del Colectivo +> **Stack:** SvelteKit + Supabase self-hosted + PWA +> **Infraestructura:** Docker Compose (dev local + producción VPS) +> **Última actualización:** Abril 2026 + +--- + +## Índice de Fases + +| Fase | Archivo | Duración estimada | +|------|---------|-------------------| +| Fase 0 | [fase-0-infraestructura.md](fase-0-infraestructura.md) | 1–2 semanas | +| Fase 1 | [fase-1-auth-colectivo.md](fase-1-auth-colectivo.md) | 2–3 semanas | +| Fase 2a | [fase-2a-lista-compra-crud.md](fase-2a-lista-compra-crud.md) | 2 semanas | +| Fase 2b | [fase-2b-realtime-modo-compra.md](fase-2b-realtime-modo-compra.md) | 2–3 semanas | +| Fase 3 | [fase-3-tareas-notas.md](fase-3-tareas-notas.md) | 1–2 semanas | +| Fase 4 | [fase-4-busqueda-pulido.md](fase-4-busqueda-pulido.md) | 1 semana | + +**Total estimado: 9–13 semanas** + +--- + +## Stack Tecnológico Confirmado + +| Capa | Tecnología | Justificación | +|------|-----------|---------------| +| Frontend / PWA | SvelteKit 2 | Reactividad nativa, bundle pequeño, PWA de primera clase | +| Backend / BaaS | Supabase self-hosted | Realtime, RLS, Storage — Auth delegada a Keycloak | +| Identidad / Auth | Keycloak self-hosted | IdP OIDC/OAuth2, gestión de usuarios, federación futura | +| Base de datos | PostgreSQL 15 (vía Supabase) | Full-text search nativo, `pg_cron` para jobs | +| Tiempo real | Supabase Realtime (WebSocket) | Presencia + cambios en < 1s sin infraestructura extra | +| Offline | IndexedDB vía `idb` | PWA-compatible, sin dependencias nativas | +| PWA | `@vite-pwa/sveltekit` + Workbox | Service Worker, caché offline, instalable | +| Infraestructura dev | Docker Compose + Supabase CLI | Entorno local con Studio + Keycloak + usuarios de prueba | +| Infraestructura prod | Docker Compose + nginx (existente) | Reverse proxy ya operativo en el VPS, gestionado externamente | +| CI/CD | GitHub Actions | Lint, test, deploy vía SSH | +| Email transaccional | Resend | SMTP para invitaciones (Keycloak también lo usa para reset) | +| Backups | `pg_dump` → Backblaze B2 (S3) | Retención 30 días, coste mínimo | + +--- + +## Estructura del Repositorio + +``` +colectivo/ +├── apps/ +│ └── web/ # SvelteKit — web + PWA +│ ├── src/ +│ │ ├── lib/ +│ │ │ ├── supabase.ts # cliente Supabase singleton +│ │ │ ├── auth.ts # cliente OIDC (keycloak-js) + helpers de sesión +│ │ │ ├── stores/ # Svelte stores globales +│ │ │ ├── sync/ # lógica offline + cola de operaciones +│ │ │ └── components/ # componentes UI reutilizables +│ │ ├── routes/ # rutas SvelteKit +│ │ └── service-worker.ts # SW personalizado (Workbox) +│ ├── static/ +│ │ ├── manifest.webmanifest +│ │ └── icons/ # iconos PWA (512, 192, 180...) +│ └── vite.config.ts +│ +├── packages/ +│ └── types/ # tipos TypeScript compartidos +│ └── src/ +│ ├── database.ts # tipos generados desde schema Supabase +│ └── domain.ts # tipos de dominio (Colectivo, Item, etc.) +│ +├── supabase/ +│ ├── migrations/ # migraciones SQL versionadas +│ ├── seed.sql # datos de prueba para dev +│ ├── functions/ # Edge Functions (invitaciones, etc.) +│ └── config.toml # configuración Supabase CLI + OIDC Keycloak +│ +├── keycloak/ +│ ├── realm-export.json # realm "colectivo" exportado — importado al arrancar dev +│ └── themes/ # tema personalizado (opcional, v2) +│ +├── infra/ +│ ├── docker-compose.dev.yml # Supabase + Keycloak + SvelteKit dev +│ ├── docker-compose.prod.yml # producción en VPS +│ └── scripts/ +│ ├── backup.sh # backup manual de una BD concreta +│ ├── backup-cron.sh # ejecutado por cron: decide tipo (diario/semanal/mensual) y purga +│ ├── restore.sh # restaura un dump concreto en una BD +│ └── deploy.sh # SSH + docker compose pull + up +│ +├── .github/ +│ └── workflows/ +│ ├── ci.yml # lint + test en PR +│ └── deploy.yml # deploy en push a main +│ +├── .env.example # todas las variables documentadas con valores de ejemplo +├── Justfile # comandos de desarrollo frecuentes +├── turbo.json # Turborepo pipeline +└── package.json # workspaces +``` + +--- + +## Justfile — Comandos de referencia + +```bash +just dev # levanta docker-compose.dev.yml + SvelteKit dev server +just dev-stop # para el entorno local +just db-reset # drop + migrate + seed (dev) +just db-migrate # aplica migraciones pendientes (dev) +just db-types # genera tipos TS desde schema Supabase → packages/types +just kc-export # exporta realm de Keycloak → keycloak/realm-export.json +just kc-open # abre Keycloak Admin Console en el navegador (localhost:8080) +just deploy # ejecuta infra/scripts/deploy.sh (prod) +just backup supabase # backup manual inmediato de supabase +just backup keycloak # backup manual inmediato de keycloak +just restore supabase # restaura un dump concreto en supabase +just restore keycloak # restaura un dump concreto en keycloak +just logs # docker compose logs -f (prod) +``` + +--- + + + +--- + +## Decisiones Previas al Inicio + +Antes de arrancar la Fase 0, confirmar: + +| Decisión | Estado | +|----------|--------| +| Estrategia JWT Keycloak → Supabase (Opción A vs B) | Pendiente — **Opción A recomendada** | +| Base de datos de Keycloak en prod | ✅ **PostgreSQL independiente** | +| Entorno de producción dockerizado | ✅ **Ya operativo** | +| Reverse proxy en producción | ✅ **nginx existente, gestionado externamente** | +| Self-registration habilitado en Keycloak o invitación obligatoria | Pendiente | +| Proveedor SMTP para Keycloak y Resend (emails de invitación) | Pendiente — **Resend recomendado** | +| Almacenamiento de backups | Pendiente — **Backblaze B2 recomendado** | +| Dominio y DNS del VPS | Pendiente | +| Subdominio para Keycloak | Pendiente — `auth.tudominio.com` recomendado | +| Registry de imágenes Docker | Pendiente — **GitHub Container Registry recomendado** | +| OAuth Google (federated en Keycloak) | Pendiente — configurar en Keycloak, no en Supabase | +| OAuth Apple (federated en Keycloak) | Pendiente — requiere Apple Developer Program ($99/año) | + +--- + +## Advertencias Técnicas + +**Keycloak y JWT con Supabase RLS:** +- Supabase RLS usa `auth.uid()` que lee el claim `sub` del JWT. Verificar que el `sub` de Keycloak es un UUID v4 estándar — por defecto lo es, pero algunos realms lo configuran diferente. +- En la Opción A (JWT directo), la clave pública de Keycloak (RS256) debe estar en `JWT_SECRET` de Supabase. Si rotas las claves de firma en Keycloak, debes actualizar Supabase también. +- Keycloak tiene sesiones propias independientes de Supabase. Un `logout()` debe invalidar en ambos lados: llamar a `keycloak.logout()` (que invalida en Keycloak) y limpiar el cliente de Supabase localmente. + +**Keycloak self-hosted en producción:** +- Keycloak 24+ requiere mínimo 512MB de RAM asignados. En un VPS pequeño (2GB), monitorizar el consumo conjunto con Supabase. +- El endpoint `/realms/{realm}/.well-known/openid-configuration` debe ser accesible públicamente para que Supabase pueda validar tokens. No poner Keycloak detrás de autenticación básica. +- Habilitar `SSL Required: external requests` en el realm de producción. Nunca desactivarlo. + +**OAuth federado (Google, Apple):** +- Con Keycloak, Google y Apple se configuran como Identity Providers en Keycloak, no en Supabase. El flujo es: usuario → Keycloak → Google/Apple → Keycloak → app. Supabase solo ve el token de Keycloak. +- Esto simplifica la configuración de Supabase pero requiere configurar los Identity Providers en Keycloak Admin Console. + +**iOS Safari y PWA:** +- Background Sync API no está soportada en Safari. Implementar fallback manual con el evento `online`. +- Push notifications solo disponibles desde iOS 16.4 y únicamente con la PWA instalada en pantalla de inicio. +- `keycloak-js` usa iframes para el refresco silencioso del token (`checkLoginIframe`). Deshabilitarlo en la config (`checkLoginIframe: false`) porque Safari bloquea cookies de terceros en iframes — usar `updateToken()` manual en su lugar. +- Probar el Modo Compra en un iPhone real en paralelo al desarrollo, no solo en DevTools. + +**Supabase Realtime self-hosted:** +- Revisar `MAX_REPLICATION_SLOTS` en PostgreSQL y `REALTIME_MAX_CONNECTIONS` antes de producción. +- Las suscripciones de Presence consumen más recursos que Postgres Changes. Limitar a listas con sesión activa. + +**Conflictos de sincronización:** +- Last-write-wins es suficiente para este dominio. No implementar CRDT en el MVP. +- Registrar conflictos detectados en tabla `sync_conflicts` para depuración. + +--- + +## Estimación Total + +| Fase | Duración | +|------|----------| +| Fase 0 — Infraestructura | 1–2 semanas | +| Fase 1 — Auth y Colectivo | 2–3 semanas | +| Fase 2a — Lista de Compra CRUD | 2 semanas | +| Fase 2b — Realtime y Modo Compra | 2–3 semanas | +| Fase 3 — Tareas y Notas | 1–2 semanas | +| Fase 4 — Búsqueda y Pulido | 1 semana | +| **Total** | **9–13 semanas** | + +--- + +*Plan generado como base para Claude Code. Stack: SvelteKit + Supabase self-hosted + Keycloak + PWA. Cada fase puede usarse como contexto independiente para sesiones de trabajo.* + diff --git a/analysis/analisis-funcional.md b/analysis/analisis-funcional.md new file mode 100644 index 0000000..5a01349 --- /dev/null +++ b/analysis/analisis-funcional.md @@ -0,0 +1,541 @@ +# Análisis Funcional — App de Gestión del Colectivo +> **Propósito:** Base para generación de plan de desarrollo con Claude Code +> **Plataforma objetivo:** Web + Mobile (iOS / Android) — paridad desde el día uno +> **Enfoque:** Gestión colaborativa del colectivo: listas de la compra (núcleo), tareas y notas (complemento) +> **Usuarios principales:** Familias y parejas con uso compartido frecuente +> **Stack tecnológico:** A definir tras revisión del plan + +--- + +## Contexto y Objetivo + +Desarrollar una aplicación de gestión doméstica centrada en la colaboración familiar en tiempo real. El producto resuelve el problema concreto de coordinar la compra, las tareas del colectivo y las notas entre los miembros de una familia o pareja, desde cualquier dispositivo y sin fricciones. + +La unidad organizativa central es el **Colectivo**: una entidad compartida a la que pertenecen todos los contenidos, en lugar del modelo usuario-céntrico habitual. Esto refleja la realidad de uso: la lista de la compra no es de una persona, es del colectivo. + +--- + +## 1. Módulos del Sistema + +### 1.1 Colectivo y Miembros +### 1.2 Lista de la Compra +### 1.3 Tareas +### 1.4 Notas +### 1.5 Autenticación y Usuarios +### 1.6 Sincronización y Offline +### 1.7 Búsqueda + +--- + +## 2. Módulo: Colectivo y Miembros + +Este módulo es la base de toda la arquitectura. Sin colectivo no hay contenido compartido. + +### 2.1 Concepto de Colectivo + +Un **Colectivo** es el espacio compartido de una familia o grupo de convivencia. Agrupa listas de la compra, tareas y notas. Cada usuario pertenece a uno o más colectivos, aunque el caso habitual es uno. + +``` +Colectivo + └── tiene muchos Miembros (con rol) + └── tiene muchas Listas de la Compra + └── tiene muchas Listas de Tareas + └── tiene muchas Notas +``` + +### 2.2 Roles dentro del Colectivo + +| Rol | Descripción | Permisos | +|-----|-------------|----------| +| Administrador | Creador del colectivo o promovido por otro admin | CRUD completo + gestión de miembros + disolver colectivo | +| Miembro | Miembro activo del colectivo | CRUD sobre todo el contenido del colectivo | +| Invitado | Acceso temporal o de solo lectura | Solo lectura; no puede crear ni editar | + +> **Decisión de diseño:** El modelo de permisos es deliberadamente simple. En un contexto familiar no tiene sentido granularidad fina por recurso. Si se necesita privacidad, el mecanismo es crear un colectivo adicional (ej: colectivo personal). + +### 2.3 Atributos + +``` +Colectivo { + id: UUID + nombre: string + emoji: string | null ← identificador visual rápido + creado_por: UUID + creado_en: timestamp +} + +MiembroColectivo { + colectivo_id: UUID + usuario_id: UUID + rol: enum(administrador | miembro | invitado) + unido_en: timestamp + invitado_por: UUID +} + +InvitacionColectivo { + id: UUID + colectivo_id: UUID + email: string + token: string ← link de invitación único + rol_asignado: enum(miembro | invitado) + expira_en: timestamp ← 7 días por defecto + aceptada: boolean +} +``` + +### 2.4 Casos de Uso + +| ID | Caso de uso | Actor | Descripción | +|----|-------------|-------|-------------| +| CU-H01 | Crear colectivo | Usuario autenticado | Define nombre y emoji; queda como administrador | +| CU-H02 | Invitar miembro | Administrador | Introduce email + rol → se envía link de invitación | +| CU-H03 | Aceptar invitación | Invitado | Accede al link → registra cuenta si no tiene → se une al colectivo | +| CU-H04 | Cambiar rol de miembro | Administrador | Promueve a administrador o degrada a invitado | +| CU-H05 | Expulsar miembro | Administrador | El miembro pierde acceso inmediato al colectivo y su contenido | +| CU-H06 | Abandonar colectivo | Miembro / Invitado | El usuario sale del colectivo; el contenido permanece | +| CU-H07 | Cambiar nombre / emoji | Administrador | Edición del colectivo visible para todos los miembros | +| CU-H08 | Disolver colectivo | Administrador | Elimina el colectivo y todo su contenido de forma permanente | +| CU-H09 | Cambiar de colectivo activo | Usuario con varios colectivos | Selector de colectivo visible en la navegación principal | + +### 2.5 Flujo: Incorporación de nuevo miembro (CU-H02 → CU-H03) + +``` +[Administrador abre "Gestionar miembros" → "Invitar"] + ↓ +Introduce email + selecciona rol (miembro / invitado) + ↓ +Sistema genera link único con token (expira en 7 días) +Sistema envía email con link + ↓ +¿El email ya tiene cuenta? + SÍ → Al abrir el link: confirmación de unión con un tap + NO → Al abrir el link: flujo de registro simplificado → confirmación de unión + ↓ +Miembro queda activo en el colectivo +Todos los miembros reciben notificación: "[Nombre] se ha unido al colectivo" +El nuevo miembro ve inmediatamente todo el contenido del colectivo +``` + +--- + +## 3. Módulo: Lista de la Compra + +Módulo principal del producto. Debe ser la experiencia más pulida y rápida de la app. + +### 3.1 Modelo de Datos + +``` +ListaCompra { + id: UUID + colectivo_id: UUID + nombre: string + estado: enum(activa | completada | archivada) + creado_por: UUID + creado_en: timestamp + completada_en: timestamp | null +} + +ItemCompra { + id: UUID + lista_id: UUID + nombre: string + cantidad: decimal | null + unidad: string | null ← "kg", "L", "uds", "paquetes"… + marcado: boolean ← comprado durante sesión de compra + marcado_por: UUID | null + marcado_en: timestamp | null + orden: integer ← posición en la lista + creado_por: UUID + creado_en: timestamp +} +``` + +### 3.2 Estados de una Lista + +``` +Activa → En sesión de compra → Completada → Archivada +``` + +**Reglas de negocio:** +- Solo puede haber una **sesión de compra activa** por lista a la vez. +- Completar una lista no elimina los ítems; quedan como historial. +- Una lista completada puede "reiniciarse": desmarca todos los ítems y vuelve a activa. +- Los ítems marcados durante la sesión registran quién los marcó y cuándo. + +### 3.4 Modo "Ir de Compras" + +Experiencia de UI dedicada para el momento de estar físicamente en el supermercado. Requisitos: + +- **Ítems grandes y fáciles de marcar** con un tap — optimizado para pantalla de teléfono con una mano. +- **Ítems pendientes arriba, marcados abajo** — reordenamiento automático al marcar. +- **Indicador de presencia:** si otro miembro del colectivo también tiene la lista abierta, se muestra su avatar. +- **Sincronización agresiva:** cambios visibles en < 1 segundo entre dispositivos (sin esperar al intervalo normal de sync). +- **Funcional offline:** si se pierde cobertura en el súper, los cambios se sincronizan al recuperar red. +- Botón prominente **"Finalizar compra"** que marca la lista como completada. + +### 3.5 Casos de Uso + +| ID | Caso de uso | Actor | Descripción | +|----|-------------|-------|-------------| +| CU-C01 | Crear lista | Miembro | Nueva lista vacía | +| CU-C02 | Añadir ítem | Miembro | Nombre + cantidad/unidad opcional; guardado inmediato; muestra sugerencias de ítems frecuentes del colectivo | +| CU-C03 | Editar ítem | Miembro | Modifica nombre, cantidad o unidad | +| CU-C04 | Reordenar ítems | Miembro | Drag & drop manual para organizar por pasillos | +| CU-C05 | Eliminar ítem | Miembro | Borrado con confirmación por deslizamiento (swipe) | +| CU-C06 | Marcar ítem como comprado | Miembro (en sesión) | Tap en checkbox; ítem baja al fondo automáticamente | +| CU-C07 | Desmarcar ítem | Miembro (en sesión) | Tap en ítem marcado; vuelve a pendiente | +| CU-C08 | Iniciar sesión de compra | Miembro | Activa el "Modo ir de compras" con UX dedicada | +| CU-C09 | Finalizar compra | Miembro | Lista pasa a estado completada; se registra timestamp | +| CU-C10 | Reiniciar lista | Miembro | Desmarca todos los ítems; lista vuelve a activa | +| CU-C11 | Eliminar lista | Miembro | Mueve a papelera del colectivo (recuperable 7 días) | +| CU-C12 | Archivar lista completada | Sistema / Miembro | Lista completada pasa a archivo; libera espacio en vista principal | + +### 3.6 Recomendaciones de ítems frecuentes + +Al añadir un ítem a cualquier lista, el sistema sugiere ítems basándose en el historial de uso del colectivo. Sin IA — frecuencia pura de registros en base de datos. + +- **Input vacío:** chips con los 5 ítems más usados del colectivo (acceso rápido) +- **Al escribir:** lista filtrada por prefijo (máx. 10), ordenada por frecuencia +- **Alcance:** compartido entre todos los miembros del colectivo +- **Registro:** trigger en `shopping_items` — automático en cada añadido, sin intervención de la app +- **Normalización:** los nombres se normalizan (`lower(trim(name))`) antes de acumular la frecuencia, evitando duplicados por mayúsculas o espacios + +### 3.7 Flujo: Sesión de Compra (CU-C08) + +``` +[Miembro toca "Ir de compras" en lista activa] + ↓ +App entra en Modo Compra: + - UI de ítems en formato grande + - Ítems pendientes arriba / marcados abajo + - Indicador de otros miembros presentes en la lista + - Sincronización reforzada (WebSocket activo) + ↓ +Miembro va marcando ítems conforme los mete en el carro + ↓ +¿Otro miembro añade ítem desde casa? + → Aparece en la lista en tiempo real con indicador "Nuevo" + ↓ +[Miembro toca "Finalizar compra"] + ↓ +Confirmación: "¿Marcar lista como completada?" + SÍ → Lista pasa a estado completada + Se registra timestamp de finalización + Se sale del Modo Compra + Notificación a otros miembros: "La compra ha sido completada por [Nombre]" + NO → Se mantiene en Modo Compra +``` + +--- + +## 4. Módulo: Tareas + +Módulo secundario. Gestión simple de tareas del colectivo, sin complejidad de project management. + +### 4.1 Modelo de Datos + +``` +ListaTareas { + id: UUID + colectivo_id: UUID + nombre: string ← ej: "Tareas de la semana", "Reformas pendientes" + creado_por: UUID + creado_en: timestamp +} + +Tarea { + id: UUID + lista_id: UUID + titulo: string + completada: boolean + completada_por: UUID | null + completada_en: timestamp | null + orden: integer + creado_por: UUID + creado_en: timestamp +} +``` + +> **Decisión deliberada:** Sin fechas límite, prioridades ni subtareas en el MVP. El foco es la simplicidad. Si el producto crece, estas features son candidatas naturales a v2. + +### 4.2 Estados de una Tarea + +``` +Pendiente → Completada + ↓ + (puede desmarcarse → vuelve a Pendiente) +``` + +### 4.3 Casos de Uso + +| ID | Caso de uso | Actor | Descripción | +|----|-------------|-------|-------------| +| CU-T01 | Crear lista de tareas | Miembro | Nueva lista con nombre | +| CU-T02 | Añadir tarea | Miembro | Título; se guarda inmediatamente | +| CU-T03 | Completar tarea | Miembro | Tap en checkbox; registra quién y cuándo | +| CU-T04 | Descompletar tarea | Miembro | Tap en tarea completada; vuelve a pendiente | +| CU-T05 | Editar título de tarea | Miembro | Edición inline | +| CU-T06 | Reordenar tareas | Miembro | Drag & drop | +| CU-T07 | Eliminar tarea | Miembro | Swipe para eliminar con confirmación | +| CU-T08 | Eliminar lista de tareas | Miembro | Elimina la lista y todas sus tareas | +| CU-T09 | Ver quién completó una tarea | Miembro | Tooltip / detalle: "Completada por [Nombre] el [fecha]" | + +### 4.4 Reglas de Negocio + +- Las tareas completadas permanecen visibles en la lista (no se ocultan automáticamente), hundidas al fondo. +- No existe papelera para tareas individuales: la eliminación es inmediata con confirmación por swipe. +- Una lista de tareas solo puede eliminarse si está vacía o el miembro confirma borrado en cascada. + +--- + +## 5. Módulo: Notas + +Módulo de utilidad. Complementa los otros dos módulos para casos como recetas, instrucciones, información del colectivo, etc. + +### 5.1 Modelo de Datos + +``` +Nota { + id: UUID + colectivo_id: UUID + titulo: string (opcional) + contenido: string ← texto plano con Markdown básico + color: enum(8 colores predefinidos) | null + fijada: boolean + archivada: boolean + eliminada: boolean + eliminada_en: timestamp | null + creado_por: UUID + creado_en: timestamp + modificado_por: UUID + modificado_en: timestamp +} +``` + +> **Scope reducido vs análisis original:** Se eliminan tipos de nota (imagen, audio), historial de versiones y comentarios. Las notas son texto con Markdown básico. Si el producto crece, estas features son candidatas a v2. + +### 5.2 Estados de una Nota + +``` +Activa → [Fijada | Archivada | En papelera → Eliminada permanentemente] +``` + +**Reglas de negocio:** +- `Fijada` y `Archivada` son mutuamente excluyentes. +- La papelera retiene notas 7 días antes de eliminarlas permanentemente. +- Las notas en papelera no aparecen en búsquedas. +- El guardado es automático. +- Cualquier miembro del colectivo puede editar cualquier nota del colectivo. + +### 5.3 Casos de Uso + +| ID | Caso de uso | Actor | Descripción | +|----|-------------|-------|-------------| +| CU-N01 | Crear nota | Miembro | Abre editor, introduce contenido, se guarda automáticamente | +| CU-N02 | Editar nota | Miembro | Modifica contenido; se registra modificado_por y modificado_en | +| CU-N03 | Fijar nota | Miembro | Nota aparece en bloque superior del tablero | +| CU-N04 | Archivar nota | Miembro | Sale del tablero principal; accesible por búsqueda | +| CU-N05 | Mover a papelera | Miembro | Va a papelera con timestamp | +| CU-N06 | Restaurar nota | Miembro | Recupera desde papelera o archivo | +| CU-N07 | Eliminar permanentemente | Miembro | Borrado definitivo desde papelera | +| CU-N08 | Cambiar color | Miembro | Selección visual entre colores predefinidos | +| CU-N09 | Duplicar nota | Miembro | Crea copia independiente | + +--- + +## 6. Módulo: Autenticación y Usuarios + +### 6.1 Flujos de Autenticación + +- Registro con email + contraseña +- Login con email + contraseña +- OAuth — Google (requerido) y Apple (requerido en iOS) +- Recuperación de contraseña vía email +- Sesión persistente con refresh token +- Flujo de registro simplificado al aceptar invitación a colectivo (el email queda prerellenado) + +### 6.2 Atributos de Usuario + +``` +User { + id: UUID + email: string ← único, gestionado por Keycloak + display_name: string + language: enum(en | es) ← preferencia del usuario; default: idioma del navegador + avatar_type: enum(initials | emoji | upload) + avatar_emoji: string | null ← solo cuando avatar_type = emoji (e.g. "🐸") + avatar_url: string | null ← solo cuando avatar_type = upload (Supabase Storage) + created_at: timestamp + last_seen_at: timestamp +} +``` + +### 6.3 Configuración de usuario + +Pantalla `/settings` accesible desde la navegación principal. Permite al usuario editar: + +- **Display name** — nombre visible en el colectivo, presencia y avatares +- **Avatar** — tres opciones, excluyentes entre sí: + 1. *Initials* (default) — inicial(es) del display name sobre fondo slate + 2. *Emoji* — selector de emoji de una lista curada (~40 opciones) + 3. *Upload* — foto propia; almacenada en Supabase Storage (bucket privado, URL firmada) +- **Language** — selector entre English y Español; efecto inmediato sin recargar + +> **Nota:** El email y la contraseña se gestionan en Keycloak, no en esta pantalla. Incluir un enlace externo a la cuenta de Keycloak para esos cambios. + +### 6.3 Reglas de Negocio + +- Un usuario puede pertenecer a múltiples colectivos simultáneamente. +- Eliminar una cuenta no elimina el contenido del colectivo; los ítems/tareas/notas creados por ese usuario permanecen. +- Si el único administrador de un colectivo elimina su cuenta, el colectivo queda huérfano → el sistema debe promover automáticamente al miembro más antiguo como administrador antes de permitir la eliminación. + +--- + +## 7. Módulo: Sincronización y Offline + +### 7.1 Estrategia Offline-First + +El escenario crítico es estar físicamente en un supermercado con cobertura inestable. La app debe ser completamente funcional sin conexión. + +- Almacenamiento local: **IndexedDB** en web, **SQLite** en mobile. +- Todas las operaciones se ejecutan primero en local y se sincronizan en segundo plano. +- Al recuperar conexión, se sincronizan todos los cambios pendientes. +- **Resolución de conflictos:** last-write-wins con aviso visual al usuario cuando se detecta sobreescritura de cambios de otro miembro. +- Indicador de estado de sincronización siempre visible en la UI (sincronizado / sincronizando / sin conexión). + +### 7.2 Sincronización en Tiempo Real + +La sincronización en tiempo real es un requisito no negociable para la lista de la compra compartida. + +- **WebSockets** para sesiones de compra activas (presencia + cambios en tiempo real). +- **Polling o SSE** para el resto del contenido (menor frecuencia, menor coste de infraestructura). +- **Indicador de presencia:** cuando dos miembros tienen la misma lista abierta simultáneamente, se muestran los avatares de los miembros presentes. + +### 7.3 Prioridades de Sincronización + +| Tipo de contenido | Mecanismo | Latencia objetivo | +|-------------------|-----------|-------------------| +| Ítems de lista en sesión de compra activa | WebSocket | < 1 segundo | +| Listas de la compra (fuera de sesión) | SSE / polling | < 5 segundos | +| Tareas | SSE / polling | < 5 segundos | +| Notas | Polling | < 30 segundos | +| Miembros del colectivo | Polling | < 60 segundos | + +--- + +## 8. Módulo: Búsqueda + +### 8.1 Alcance + +| Ámbito | ¿Incluido? | Notas | +|--------|-----------|-------| +| Listas de la compra activas | ✅ | Busca también en nombres de ítems | +| Listas de la compra archivadas | ✅ | | +| Listas en papelera | ❌ | | +| Plantillas | ✅ | | +| Tareas | ✅ | Busca en títulos de tarea y nombre de lista | +| Notas activas | ✅ | Busca en título y contenido | +| Notas archivadas | ✅ | | +| Notas en papelera | ❌ | | + +### 8.2 Filtros + +- Por tipo de contenido (lista de compra / tareas / notas) +- Por miembro creador +- Por rango de fechas +- Por estado (activo / archivado / completado) + +--- + +## 9. Reglas de Negocio Globales + +| # | Regla | +|---|-------| +| RN-01 | Todo el contenido pertenece al Colectivo, no a un usuario individual. | +| RN-02 | Cualquier miembro activo del colectivo puede crear, editar y eliminar contenido. | +| RN-03 | Solo los administradores pueden gestionar miembros e invitaciones. | +| RN-04 | Solo los administradores pueden disolver el colectivo. | +| RN-05 | El guardado es automático en todos los módulos. No existe botón "Guardar". | +| RN-06 | Offline-first: los cambios locales se aplican inmediatamente y se sincronizan en segundo plano. | +| RN-07 | Los conflictos de sincronización se registran y son visibles al usuario afectado. | +| RN-08 | La búsqueda no opera sobre la papelera. | +| RN-09 | Eliminar una cuenta de usuario no elimina el contenido del colectivo. | +| RN-10 | Un colectivo debe tener siempre al menos un administrador activo. | +| RN-11 | Solo puede existir una sesión de compra activa por lista a la vez. | +| RN-12 | Las plantillas de lista son inmutables durante una sesión de compra activa basada en ellas. | + +--- + +## 10. Requerimientos No Funcionales + +- **Sincronización en sesión de compra:** cambios visibles en < 1 segundo entre dispositivos. +- **Sincronización general:** cambios visibles en < 5 segundos entre dispositivos. +- **Offline:** la app debe ser 100% funcional sin conexión en todos los módulos. +- **Seguridad:** datos cifrados en tránsito (TLS) y en reposo; aislamiento estricto entre colectivos. +- **Multiplataforma:** paridad de funcionalidades entre web y mobile desde el día uno. +- **Rendimiento mobile:** el Modo Compra debe funcionar con fluidez en gama media (60fps, respuesta a tap < 100ms). +- **Stack tecnológico:** a definir — el plan de desarrollo debe ser agnóstico al stack. + +--- + +## 11. Entidades del Dominio (Resumen) + +``` +Usuario + └── pertenece a muchos Colectivos (a través de MiembroColectivo) + +Colectivo + ├── tiene muchos MiembroColectivo + ├── tiene muchas ListasCompra + ├── tiene muchas ListasTareas + └── tiene muchas Notas + +MiembroColectivo + ├── referencia Usuario + ├── referencia Colectivo + └── tiene rol: enum(administrador | miembro | invitado) + +ListaCompra + ├── pertenece a Colectivo + ├── tiene muchos ItemCompra + └── puede ser Plantilla (es_plantilla = true) + +ItemCompra + └── pertenece a ListaCompra + +ListaTareas + ├── pertenece a Colectivo + └── tiene muchas Tareas + +Tarea + └── pertenece a ListaTareas + +Nota + └── pertenece a Colectivo + +InvitacionColectivo + └── referencia Colectivo +``` + +--- + +## 12. Pantallas Principales del MVP + +Esta sección orienta el plan de desarrollo sobre el alcance de UI esperado. + +| Pantalla | Descripción | +|----------|-------------| +| Onboarding | Login + creación o unión a colectivo | +| Home | Vista principal con listas activas, tareas pendientes destacadas y notas fijadas | +| Lista de la compra — vista normal | Ítems con cantidad/unidad, opción de editar, añadir y reordenar | +| Lista de la compra — Modo Compra | UI dedicada: ítems grandes, marcado fácil, indicador de presencia | +| Tareas | Lista de tareas con checkboxes | +| Notas | Tablero de notas del colectivo | +| Editor de nota | Editor de texto con Markdown básico | +| Gestión del colectivo | Miembros, roles, invitaciones | +| Búsqueda | Búsqueda global con filtros | +| Configuración de usuario (`/settings`) | Display name, avatar (initials / emoji / upload), idioma | + +--- + +*Documento generado como base para plan de desarrollo. Stack tecnológico a definir tras revisión del plan.* diff --git a/plan/fase-0-infraestructura.md b/plan/fase-0-infraestructura.md new file mode 100644 index 0000000..1c52936 --- /dev/null +++ b/plan/fase-0-infraestructura.md @@ -0,0 +1,372 @@ +### Fase 0 — Infraestructura y Entornos +**Duración estimada: 1–2 semanas** +**Objetivo: tener los dos entornos funcionando antes de escribir producto.** + +#### 0.1 Variables de entorno + +Todas las variables viven en `.env.dev` (dev) y `.env.production` (prod, fuera del repo). El fichero `.env.example` documenta cada variable con su propósito. + +**Keycloak:** +```env +# Credenciales del administrador de Keycloak +KEYCLOAK_ADMIN=admin +KEYCLOAK_ADMIN_PASSWORD=... + +# Realm configurado para la app +KEYCLOAK_REALM=colectivo + +# Client OIDC que usa SvelteKit +KEYCLOAK_CLIENT_ID=colectivo-web +KEYCLOAK_CLIENT_SECRET=... # solo en prod; en dev el client es public + +# URL base de Keycloak (usada por SvelteKit y Supabase) +# Dev: http://localhost:8080 +# Prod: https://auth.tudominio.com +KEYCLOAK_URL=http://localhost:8080 +KEYCLOAK_ISSUER=${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM} +``` + +**Supabase:** +```env +# Claves de Supabase (generadas al arrancar; rotar en prod) +SUPABASE_URL=http://localhost:54321 +SUPABASE_ANON_KEY=... +SUPABASE_SERVICE_ROLE_KEY=... # solo para Edge Functions y scripts de admin +JWT_SECRET=... # debe coincidir con la firma JWT de Keycloak en prod + +# OIDC — Keycloak como proveedor externo de Supabase Auth +SUPABASE_AUTH_EXTERNAL_KEYCLOAK_ENABLED=true +SUPABASE_AUTH_EXTERNAL_KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID} +SUPABASE_AUTH_EXTERNAL_KEYCLOAK_SECRET=${KEYCLOAK_CLIENT_SECRET} +SUPABASE_AUTH_EXTERNAL_KEYCLOAK_URL=${KEYCLOAK_ISSUER} +``` + +**SvelteKit:** +```env +# Públicas (expuestas al navegador — prefijo PUBLIC_) +PUBLIC_SUPABASE_URL=http://localhost:54321 +PUBLIC_SUPABASE_ANON_KEY=... +PUBLIC_KEYCLOAK_URL=http://localhost:8080 +PUBLIC_KEYCLOAK_REALM=colectivo +PUBLIC_KEYCLOAK_CLIENT_ID=colectivo-web + +# Privadas (solo servidor SvelteKit) +SUPABASE_SERVICE_ROLE_KEY=... +``` + +**Email (Resend):** +```env +RESEND_API_KEY=... +EMAIL_FROM=noreply@tudominio.com +``` + +**Backups (solo prod):** +```env +# Volumen local donde se guardan los dumps antes/además de B2 +BACKUP_LOCAL_DIR=/mnt/backups # ruta en el host montada como volumen + +# Política de retención — número máximo de backups de cada tipo +BACKUP_RETENTION_DAILY=7 # 7 diarios → cubre la última semana +BACKUP_RETENTION_WEEKLY=4 # 4 semanales → cubre el último mes +BACKUP_RETENTION_MONTHLY=3 # 3 mensuales → configurable, por defecto 3 meses + +# Subida a Backblaze B2 (opcional pero recomendado como segunda copia) +BACKUP_B2_ENABLED=true +B2_BUCKET=colectivo-backups +B2_KEY_ID=... +B2_APPLICATION_KEY=... +``` + +#### 0.2 Entorno de desarrollo local + +- [ ] `docker-compose.dev.yml` con todos los servicios: + - **PostgreSQL 15** — puerto `5432` + - **Supabase Auth (GoTrue)** — configurado con Keycloak como OIDC externo + - **Supabase Realtime** — puerto `4000` + - **Supabase Storage** — puerto `5000` + - **Kong API Gateway** — puerto `54321` (entrada principal de Supabase) + - **Supabase Studio** — puerto `54323` + - **Keycloak 24** — puerto `8080`, arranca con `--import-realm keycloak/realm-export.json` +- [ ] `keycloak/realm-export.json` — realm `colectivo` preconfigurado con: + - Client `colectivo-web` (public, PKCE habilitado, redirect URIs para dev) + - **5 usuarios de prueba** creados y listos (ver tabla abajo) + - Roles de realm: `app-user` (asignado a todos por defecto) +- [ ] SvelteKit dev server en puerto `5173`, apuntando a variables de `.env.dev` +- [ ] `supabase/seed.sql` — crea los usuarios de prueba en `auth.users` con los mismos UUIDs que Keycloak, un colectivo de ejemplo, listas e ítems +- [ ] Generación automática de tipos TypeScript: `supabase gen types typescript` +- [ ] `Justfile` con `just dev`, `just db-reset`, `just db-types`, `just kc-export`, `just kc-open` + +**Usuarios de prueba en Keycloak (dev):** + +| Usuario | Email | Contraseña | Rol en colectivo de prueba | +|---------|-------|-----------|---------------------------| +| `ana` | ana@dev.local | `test1234` | Administradora | +| `borja` | borja@dev.local | `test1234` | Miembro | +| `carmen` | carmen@dev.local | `test1234` | Miembro | +| `david` | david@dev.local | `test1234` | Invitado | +| `eva` | eva@dev.local | `test1234` | Sin colectivo (prueba onboarding) | + +> Estos usuarios están en el `realm-export.json` versionado. Al hacer `just dev` arrancan disponibles sin configuración manual. + +#### 0.3 Entorno de producción (VPS) + +> **El entorno de producción ya está montado y dockerizado.** Keycloak, nginx y su PostgreSQL propio ya están operativos. Esta sección cubre únicamente lo que hay que añadir. + +- [ ] Incorporar los servicios de Supabase al `docker-compose.prod.yml` existente: + - PostgreSQL dedicado para Supabase — contenedor independiente, volumen propio, **no compartir con Keycloak** + - GoTrue (Auth), Realtime, Storage, Kong en la red interna Docker existente + - Kong expuesto en un puerto interno (ej: `127.0.0.1:54321`) — nginx hace proxy hacia él, no expuesto directamente +- [ ] Añadir el contenedor SvelteKit al compose, escuchando en un puerto interno (ej: `127.0.0.1:3000`) — nginx hace proxy hacia él +- [ ] Configurar nginx externamente (fuera del repo) para los nuevos servicios — ver bloques de ejemplo abajo +- [ ] Exportar el realm `colectivo` desde dev (`just kc-export`) e importarlo en el Keycloak de producción via Admin Console — sin `--import-realm` en prod +- [ ] `.env.production` en el VPS con todas las variables (ver sección 0.1), fuera del repo +- [ ] Scripts de backup — ver sección **0.6 Sistema de Backups** para el detalle completo +- [ ] Script de deploy: `ssh vps "cd /app && git pull && docker compose -f docker-compose.prod.yml pull && docker compose -f docker-compose.prod.yml up -d --no-deps web"` + - `--no-deps web` para actualizar solo el contenedor SvelteKit sin reiniciar Supabase ni Keycloak + +##### Configuración nginx de referencia + +Tres bloques independientes: app, API/Supabase y Keycloak. Ajustar puertos y dominios según la configuración real del VPS. + +**`/etc/nginx/sites-available/colectivo-web`** — SvelteKit PWA: +```nginx +server { + listen 443 ssl http2; + server_name tudominio.com; + + ssl_certificate /etc/letsencrypt/live/tudominio.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/tudominio.com/privkey.pem; + + # Headers de seguridad + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Content-Security-Policy + "default-src 'self'; \ + script-src 'self' 'wasm-unsafe-eval'; \ + style-src 'self' 'unsafe-inline'; \ + connect-src 'self' https://api.tudominio.com wss://api.tudominio.com https://auth.tudominio.com; \ + img-src 'self' data: blob:; \ + font-src 'self'; \ + worker-src 'self' blob:;" + always; + + # Service Worker — nunca cacheado por nginx + location = /service-worker.js { + proxy_pass http://127.0.0.1:3000; + add_header Cache-Control "no-store, no-cache, must-revalidate" always; + } + + location / { + proxy_pass http://127.0.0.1:3000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + +# Redirección HTTP → HTTPS +server { + listen 80; + server_name tudominio.com; + return 301 https://$host$request_uri; +} +``` + +**`/etc/nginx/sites-available/colectivo-api`** — Kong/Supabase con WebSocket para Realtime: +```nginx +server { + listen 443 ssl http2; + server_name api.tudominio.com; + + ssl_certificate /etc/letsencrypt/live/api.tudominio.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/api.tudominio.com/privkey.pem; + + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Frame-Options "DENY" always; + add_header X-Content-Type-Options "nosniff" always; + + # Supabase Realtime — WebSocket (wss://api.tudominio.com/realtime/v1/websocket) + # CRÍTICO: sin estos headers el Modo Compra no funciona en producción + location /realtime/ { + proxy_pass http://127.0.0.1:54321/realtime/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; # ← habilita upgrade a WebSocket + proxy_set_header Connection "upgrade"; # ← mantiene la conexión viva + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 3600s; # ← evita que nginx cierre conexiones WS inactivas + proxy_send_timeout 3600s; + } + + # Resto de la API de Supabase (REST, Auth, Storage) + location / { + proxy_pass http://127.0.0.1:54321; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + +server { + listen 80; + server_name api.tudominio.com; + return 301 https://$host$request_uri; +} +``` + +**`/etc/nginx/sites-available/colectivo-auth`** — Keycloak (si aún no está configurado): +```nginx +server { + listen 443 ssl http2; + server_name auth.tudominio.com; + + ssl_certificate /etc/letsencrypt/live/auth.tudominio.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/auth.tudominio.com/privkey.pem; + + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + + # Keycloak necesita el host original para construir redirect URIs correctamente + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; # ← crítico para Keycloak + proxy_set_header X-Forwarded-Port 443; # ← crítico para Keycloak + + location / { + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + proxy_buffer_size 128k; # Keycloak genera headers grandes (tokens) + proxy_buffers 4 256k; + proxy_busy_buffers_size 256k; + } +} + +server { + listen 80; + server_name auth.tudominio.com; + return 301 https://$host$request_uri; +} +``` + +> **Notas importantes:** +> - El bloque `/realtime/` debe ir **antes** del bloque `/` en el server de API — nginx aplica la regla más específica primero. +> - `proxy_read_timeout 3600s` en el bloque de Realtime es necesario porque las conexiones WebSocket del Modo Compra son de larga duración. Sin él, nginx cierra la conexión a los 60 segundos (valor por defecto) y el cliente tiene que reconectar continuamente. +> - En el CSP de SvelteKit, `connect-src` debe incluir tanto `https://` como `wss://` para `api.tudominio.com` — sin el `wss://` el Service Worker no puede abrir la conexión WebSocket. +> - Los headers `X-Forwarded-Host` y `X-Forwarded-Port` en Keycloak son obligatorios. Sin ellos, Keycloak construye las redirect URIs con el puerto interno (8080) en lugar del 443, y el flujo OIDC falla. + +#### 0.4 CI/CD + +- [ ] `ci.yml`: lint + typecheck + tests unitarios en cada PR +- [ ] `deploy.yml`: build de imagen SvelteKit → GitHub Container Registry → deploy vía SSH en merge a `main` +- [ ] Secrets de GitHub: `SSH_KEY`, `VPS_HOST`, `GHCR_TOKEN` + +#### 0.5 Monorepo + +- [ ] Turborepo configurado con pipeline `build → test → deploy` +- [ ] `packages/types` como paquete interno +- [ ] `apps/web` consumiendo `packages/types` + +#### 0.6 Sistema de Backups + +##### Estrategia de rotación + +Se guardan tres tipos de backup, cada uno con su propia ventana de retención: + +| Tipo | Frecuencia | Retención por defecto | Variable de control | +|------|-----------|----------------------|---------------------| +| Diario | Cada día a las 02:00 | 7 copias | `BACKUP_RETENTION_DAILY` | +| Semanal | Lunes a las 03:00 | 4 copias | `BACKUP_RETENTION_WEEKLY` | +| Mensual | Día 1 de cada mes a las 04:00 | 3 copias | `BACKUP_RETENTION_MONTHLY` | + +Con los valores por defecto se conservan hasta ~3 meses de historia. Cambiando `BACKUP_RETENTION_MONTHLY=6` se extiende a 6 meses sin tocar nada más. + +##### Volumen local + +Los dumps se guardan en una estructura de directorios clara en el volumen montado en `BACKUP_LOCAL_DIR`: + +``` +/mnt/backups/ +├── supabase/ +│ ├── daily/ +│ │ ├── supabase_daily_2026-04-10.sql.gz +│ │ └── supabase_daily_2026-04-11.sql.gz +│ ├── weekly/ +│ │ └── supabase_weekly_2026-04-07.sql.gz +│ └── monthly/ +│ └── supabase_monthly_2026-04-01.sql.gz +└── keycloak/ + ├── daily/ + ├── weekly/ + └── monthly/ +``` + +##### Scripts + +**`infra/scripts/backup.sh`** — backup manual inmediato de una BD concreta: +```bash +# Uso: ./backup.sh [daily|weekly|monthly] +# Sin segundo argumento, genera un dump con timestamp libre (para backups manuales pre-deploy) +# Ejemplo: ./backup.sh supabase +# Ejemplo: ./backup.sh keycloak monthly +``` +- Conecta al contenedor Docker correcto según el primer argumento +- Ejecuta `pg_dump` con formato custom (`-Fc`) para compatibilidad con `pg_restore` +- Comprime con gzip +- Guarda en `$BACKUP_LOCAL_DIR///` +- Si `BACKUP_B2_ENABLED=true`, sube el fichero a B2 con `rclone` o `b2 cli` +- Imprime checksum SHA256 del fichero generado + +**`infra/scripts/backup-cron.sh`** — wrapper para cron, decide el tipo automáticamente: +```bash +# Lógica de decisión: +# - Si es día 1 del mes → tipo = monthly +# - Si es lunes → tipo = weekly +# - En cualquier otro caso → tipo = daily +# Llama a backup.sh para supabase y keycloak secuencialmente +# Tras el backup, llama a la función de purga para eliminar copias antiguas +# según BACKUP_RETENTION_DAILY/WEEKLY/MONTHLY +# Registra resultado en /var/log/backup-colectivo.log +``` + +**`infra/scripts/restore.sh`** — restaura un dump en una BD: +```bash +# Uso: ./restore.sh +# Ejemplo: ./restore.sh supabase /mnt/backups/supabase/daily/supabase_daily_2026-04-10.sql.gz +``` +- Pide confirmación explícita antes de ejecutar (`¿Seguro? Esto sobreescribirá la BD actual [s/N]`) +- Detiene los servicios dependientes de la BD antes de restaurar (GoTrue, Realtime, Kong para supabase) +- Descomprime y ejecuta `pg_restore` sobre el contenedor correcto +- Reinicia los servicios tras la restauración +- Imprime resumen: tablas restauradas, filas por tabla + +##### Crontab en el host + +```cron +# Backups de producción — colectivo +0 2 * * * /app/infra/scripts/backup-cron.sh >> /var/log/backup-colectivo.log 2>&1 +``` + +Un solo entry en cron. El script decide internamente si es daily, weekly o monthly. + +##### Tareas de la Fase 0 + +- [ ] Crear volumen Docker (o directorio montado) en `BACKUP_LOCAL_DIR` con permisos correctos +- [ ] Implementar `backup.sh` con soporte para supabase y keycloak, compresión y subida opcional a B2 +- [ ] Implementar `backup-cron.sh` con lógica de rotación y purga por tipo +- [ ] Implementar `restore.sh` con confirmación, parada de servicios y resumen post-restauración +- [ ] Configurar crontab en el host del VPS +- [ ] Verificar backup completo: ejecutar `backup.sh supabase`, comprobar fichero generado, ejecutar `restore.sh` en un entorno de prueba +- [ ] Verificar purga: crear backups de prueba con fechas antiguas, ejecutar `backup-cron.sh`, confirmar que se eliminan los que exceden la retención + +**Criterio de aceptación:** `just dev` levanta todo el stack en < 3 minutos. Los 5 usuarios de prueba pueden hacer login con Keycloak y llegar a la app. Deploy a producción funciona con un `git push`. Un backup manual de supabase y keycloak genera ficheros válidos que se restauran correctamente. + +--- diff --git a/plan/fase-1-auth-colectivo.md b/plan/fase-1-auth-colectivo.md new file mode 100644 index 0000000..f7246ba --- /dev/null +++ b/plan/fase-1-auth-colectivo.md @@ -0,0 +1,93 @@ +### Fase 1 — Autenticación y Colectivo +**Duración estimada: 2–3 semanas** +**Objetivo: la base del dominio. Sin esto no existe nada más.** + +#### 1.1 Base de datos + +- [ ] Migración: tabla `users` — espejo de `auth.users` de Supabase, poblada por trigger al primer login OIDC: + - `display_name`, `email` + - `language: enum(en | es)` — default `en`, sobrescrito por preferencia de navegador en primer login + - `avatar_type: enum(initials | emoji | upload)` — default `initials` + - `avatar_emoji: text | null` + - `avatar_url: text | null` — URL firmada de Supabase Storage +- [ ] Migración: tabla `collectives` con campos `id`, `name`, `emoji`, `created_by`, `created_at` +- [ ] Migración: tabla `collective_members` con `role: enum(admin | member | guest)` +- [ ] Migración: tabla `collective_invitations` con token, expiración y estado +- [ ] **RLS completo** — políticas críticas: + - Un usuario solo puede leer colectivos a los que pertenece + - Solo administradores pueden insertar en `collective_invitations` + - Solo administradores pueden actualizar roles en `collective_members` + - Un usuario solo puede actualizar su propia fila en `users` +- [ ] Trigger: `on_auth_user_created` — sincroniza el perfil de Keycloak (display_name, email) a `users` en cada login +- [ ] Trigger: promoción automática al miembro más antiguo si el último admin elimina su cuenta (RN-10) +- [ ] Función SQL: `is_active_member(collective_id, user_id)` — usada en todas las políticas RLS +- [ ] Bucket Supabase Storage: `avatars` — privado, acceso vía signed URLs, un fichero por usuario (`{user_id}/avatar`) + +#### 1.2 Integración Keycloak → Supabase + +El JWT que emite Keycloak debe ser validable por Supabase RLS. Hay dos estrategias; elegir una antes de implementar: + +**Opción A — JWT de Keycloak directo en Supabase (recomendada para self-hosted):** +- Configurar `JWT_SECRET` de Supabase con la clave pública de Keycloak (RS256) +- Supabase valida el token de Keycloak directamente en RLS via `auth.uid()` +- Sin intermediarios, latencia mínima +- Requiere que el claim `sub` de Keycloak sea el UUID que identifica al usuario en Supabase + +**Opción B — GoTrue como proxy OIDC:** +- Supabase Auth recibe el token de Keycloak, lo valida y emite su propio JWT +- Más aislamiento, pero un salto extra en cada request + +> **Decisión recomendada: Opción A.** Más simple operacionalmente en self-hosted. El `sub` de Keycloak se convierte en el `auth.uid()` de Supabase. + +- [ ] Configurar `supabase/config.toml` con Keycloak como OIDC externo +- [ ] Verificar que `auth.uid()` en las políticas RLS resuelve al `sub` de Keycloak +- [ ] Test de integración: token de Keycloak → query a Supabase con RLS activo → respuesta correcta + +#### 1.3 Edge Functions + +- [ ] `invite-member`: genera token único, inserta invitación, envía email vía Resend +- [ ] `accept-invitation`: valida token + expiración, une al usuario al colectivo, invalida el token + +#### 1.4 Internacionalización (i18n) + +Librería: **Paraglide JS** (`@inlang/paraglide-sveltekit`). Compile-time, tree-shaken por idioma, sin overhead en runtime. + +- [ ] Instalar y configurar `@inlang/paraglide-sveltekit` con el plugin de Vite +- [ ] Crear ficheros de mensajes: `messages/en.json` y `messages/es.json` +- [ ] Configurar detección de idioma: `Accept-Language` header en primer load → guardado en `users.language` tras login +- [ ] Cambio de idioma en runtime: actualizar `users.language` en Supabase + llamar a `setLanguageTag()` de Paraglide — sin recarga de página +- [ ] Traducir todas las cadenas UI de la Fase 1 (onboarding, gestión de colectivo, settings) + +> Las cadenas de UI van en `messages/`. Nunca hardcodear texto visible al usuario directamente en componentes. + +#### 1.5 Frontend + +- [ ] Integración de `keycloak-js` en SvelteKit: + - Inicialización en `+layout.ts` con `checkLoginIframe: false` (compatibilidad PWA/Safari) + - Store Svelte `auth` — expone `user`, `token`, `isAuthenticated`, `login()`, `logout()` + - `login()` redirige al login page de Keycloak; el callback vuelve a la app con el token + - Refresco silencioso del token antes de expiración (`updateToken(60)`) +- [ ] Rutas protegidas: hook `+layout.server.ts` que verifica sesión y redirige si no hay token válido +- [ ] Flujo de onboarding post-primer-login: + - Detectar si el usuario no tiene colectivo → redirigir a `/onboarding` + - Opción A: Crear nuevo colectivo (nombre + emoji) + - Opción B: Introducir link de invitación +- [ ] Pantalla `/collective/manage` — miembros, roles, botón invitar, expulsar +- [ ] Pantalla `/invitation/[token]` — aceptar invitación (el usuario debe estar autenticado en Keycloak) +- [ ] Selector de colectivo activo en la barra de navegación principal +- [ ] Store Svelte `collective` — colectivo activo, lista de miembros, rol del usuario actual +- [ ] Pantalla `/settings` — configuración de usuario: + - Campo display name (input de texto, guardado automático con debounce) + - Selector de avatar con tres modos: + - *Initials* — preview generado en tiempo real desde el display name + - *Emoji* — grid de ~40 emoji curados, tap para seleccionar + - *Upload* — input de fichero; preview antes de subir; subida a Supabase Storage bucket `avatars`; soporte para recortar en proporción 1:1 + - Selector de idioma (English / Español) con efecto inmediato + - Enlace externo a Keycloak Account Console para cambio de email/contraseña +- [ ] Componente `Avatar.svelte` — renderiza según `avatar_type`: initials (letra sobre fondo slate), emoji, o `` con fallback a initials si la URL falla + +> **Nota sobre registro de nuevos usuarios:** Con Keycloak, el registro lo gestiona Keycloak (su propia pantalla o self-registration habilitado en el realm). No hay pantalla de registro en SvelteKit. Si un usuario llega desde un link de invitación sin cuenta, Keycloak gestiona el registro y luego redirige de vuelta al callback de invitación. + +**Criterio de aceptación:** los 5 usuarios de prueba pueden autenticarse contra Keycloak, llegar a la app con sesión activa, y `auth.uid()` en Supabase resuelve correctamente para las políticas RLS. + +--- diff --git a/plan/fase-2a-lista-compra-crud.md b/plan/fase-2a-lista-compra-crud.md new file mode 100644 index 0000000..0f6b66c --- /dev/null +++ b/plan/fase-2a-lista-compra-crud.md @@ -0,0 +1,106 @@ +### Fase 2a — Lista de la Compra (CRUD) +**Duración estimada: 2 semanas** +**Objetivo: funcionalidad completa de listas sin sincronización en tiempo real.** + +#### 2a.1 Base de datos + +- [ ] Migración: tabla `shopping_lists` con `status enum(active | completed | archived)` +- [ ] Migración: tabla `shopping_items` con `name`, `quantity`, `unit`, `is_checked`, `checked_by`, `sort_order` +- [ ] RLS: solo miembros del colectivo pueden leer/escribir listas e ítems +- [ ] Índice en `shopping_items.list_id` y `shopping_items.sort_order` +- [ ] Job `pg_cron`: cada noche, mueve a `archived` las listas completadas hace > 7 días en estado borrado +- [ ] Migración: tabla `item_frequency` y trigger de recomendaciones (ver sección 2a.3) + +#### 2a.2 Frontend + +- [ ] Pantalla `/lists` — listado de listas activas del colectivo +- [ ] Pantalla `/lists/[id]` — vista normal de lista: + - Añadir ítem (input rápido: name + quantity + unit) con recomendaciones de ítems frecuentes + - Editar ítem inline + - Eliminar ítem con swipe (confirmación) + - Reordenar con drag & drop (`svelte-dnd-action`) +- [ ] Papelera de listas: retención 7 días, restauración, borrado definitivo +- [ ] Store Svelte `lists` — listas del colectivo activo, optimistic updates + +#### 2a.3 Recomendaciones de ítems frecuentes + +Registra automáticamente la frecuencia de uso de cada ítem del colectivo y la usa como sistema de sugerencias al añadir nuevos ítems. Sin IA — solo frecuencia de uso basada en registros de la base de datos. + +**Tabla:** + +```sql +CREATE TABLE item_frequency ( + collective_id UUID NOT NULL REFERENCES collectives(id) ON DELETE CASCADE, + name TEXT NOT NULL, -- normalized: lower(trim(name)) + use_count INTEGER NOT NULL DEFAULT 1, + last_used_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (collective_id, name) +); + +CREATE INDEX idx_item_frequency_collective_count + ON item_frequency(collective_id, use_count DESC); +``` + +**Trigger** — se dispara en cada `INSERT` en `shopping_items`, normaliza el nombre y hace upsert: + +```sql +CREATE OR REPLACE FUNCTION fn_record_item_frequency() +RETURNS TRIGGER AS $$ +DECLARE + v_collective_id UUID; +BEGIN + SELECT collective_id INTO v_collective_id + FROM shopping_lists WHERE id = NEW.list_id; + + INSERT INTO item_frequency (collective_id, name, use_count, last_used_at) + VALUES (v_collective_id, lower(trim(NEW.name)), 1, now()) + ON CONFLICT (collective_id, name) DO UPDATE SET + use_count = item_frequency.use_count + 1, + last_used_at = now(); + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trg_item_frequency +AFTER INSERT ON shopping_items +FOR EACH ROW EXECUTE FUNCTION fn_record_item_frequency(); +``` + +**Consulta de sugerencias** — llamada desde el cliente al escribir en el input de añadir ítem: + +```sql +-- Input vacío: top 5 más usados del colectivo (chips de acceso rápido) +SELECT name, use_count +FROM item_frequency +WHERE collective_id = $1 +ORDER BY use_count DESC +LIMIT 5; + +-- Con prefijo: filtro por lo que el usuario va escribiendo +SELECT name, use_count +FROM item_frequency +WHERE collective_id = $1 + AND name LIKE lower(trim($2)) || '%' +ORDER BY use_count DESC +LIMIT 10; +``` + +**RLS:** los miembros del colectivo solo pueden leer las frecuencias de su colectivo. Escritura solo vía trigger (el rol de la app no tiene `INSERT`/`UPDATE` directo sobre esta tabla). + +**Comportamiento en UI:** +- Input vacío → muestra los 5 ítems más frecuentes como chips de acceso rápido bajo el input +- Usuario escribe → sustituye los chips por resultados filtrados por prefijo (máx. 10) +- Tap en chip/sugerencia → rellena el input con ese nombre (el usuario puede ajustar cantidad/unidad antes de añadir) +- Al confirmar el ítem, el trigger registra la frecuencia automáticamente + +**Tareas:** +- [ ] Migración: tabla `item_frequency` con índice +- [ ] Función `fn_record_item_frequency` + trigger `trg_item_frequency` +- [ ] RLS en `item_frequency` +- [ ] Componente `ItemSuggestions.svelte` — chips + lista filtrada +- [ ] Integrar `ItemSuggestions` en el input de añadir ítem de `/lists/[id]` + +**Criterio de aceptación:** crear, editar, reordenar y eliminar ítems funciona fluidamente. Al añadir un ítem, aparecen sugerencias basadas en el historial del colectivo; añadir un ítem actualiza su frecuencia automáticamente. + +--- diff --git a/plan/fase-2b-realtime-modo-compra.md b/plan/fase-2b-realtime-modo-compra.md new file mode 100644 index 0000000..0fe0ba1 --- /dev/null +++ b/plan/fase-2b-realtime-modo-compra.md @@ -0,0 +1,40 @@ +### Fase 2b — Sincronización en Tiempo Real y Modo Compra +**Duración estimada: 2–3 semanas** +**Objetivo: el diferencial del producto. Es la fase más compleja técnicamente.** + +#### 2b.1 Sincronización Realtime + +- [ ] Suscripción a `Postgres Changes` de Supabase Realtime sobre `items_compra` filtrada por `lista_id` +- [ ] Suscripción a `Presence` de Supabase Realtime para indicador de miembros presentes en lista +- [ ] Store Svelte `realtimeSync`: + - Gestiona el ciclo de vida de las suscripciones (subscribe/unsubscribe al entrar/salir de la lista) + - Aplica cambios remotos al estado local sin causar bucles + - Detecta conflicto last-write-wins: si el `updated_at` remoto > local, aplica remoto y muestra aviso visual + +#### 2b.2 Offline-First + +- [ ] Instalación y configuración de `idb` (wrapper de IndexedDB) +- [ ] Esquema local en IndexedDB: `pending_ops`, `listas_cache`, `items_cache` +- [ ] Cola de operaciones pendientes: cada mutación se escribe en `pending_ops` antes de llamar a Supabase +- [ ] Service Worker (`@vite-pwa/sveltekit` + Workbox): + - Cache-first para assets estáticos + - Network-first con fallback a caché para rutas de la app + - Background sync para flush de `pending_ops` al recuperar conexión +- [ ] Fallback manual de sync: al detectar `online` en el evento del navegador, flush de la cola (cubre Safari que no soporta Background Sync API) +- [ ] Indicador de estado de sincronización en la UI: `sincronizado | sincronizando | sin conexión` + +#### 2b.3 Modo Compra + +- [ ] Ruta `/listas/[id]/compra` — UI dedicada al Modo Compra: + - Ítems en formato grande (mínimo 64px de altura), optimizado para pulsar con el pulgar + - Reordenamiento automático al marcar: pendientes arriba, marcados abajo (animación fluida con `flip`) + - Indicador de presencia: avatares de miembros que tienen la lista abierta simultáneamente + - Banner `sin conexión` cuando no hay red (los cambios se guardan en local) + - Botón prominente **"Finalizar compra"** — confirmación modal → lista pasa a `completada` + - Notificación push (si el usuario ha dado permiso) a otros miembros al finalizar +- [ ] Rendimiento: 60fps en gama media, tap < 100ms (validar en Chrome DevTools con throttling) +- [ ] Prueba de sincronización: dos dispositivos, misma lista, cambios visibles en < 1 segundo + +**Criterio de aceptación:** dos personas en el mismo supermercado, con cobertura inestable, pueden marcar ítems desde sus móviles y verse los cambios mutuamente en tiempo real. Si se pierde la conexión, los cambios locales se sincronizan al recuperarla. + +--- diff --git a/plan/fase-3-tareas-notas.md b/plan/fase-3-tareas-notas.md new file mode 100644 index 0000000..e466148 --- /dev/null +++ b/plan/fase-3-tareas-notas.md @@ -0,0 +1,34 @@ +### Fase 3 — Tareas y Notas +**Duración estimada: 1–2 semanas** +**Objetivo: completar los módulos secundarios.** + +#### 3.1 Tareas + +- [ ] Migración: tablas `listas_tareas` y `tareas` con RLS +- [ ] Pantalla `/tareas` — listado de listas de tareas +- [ ] Vista de lista de tareas: + - Añadir tarea (input rápido) + - Checkbox de completado (registra `completada_por` + `completada_en`) + - Tooltip "Completada por [Nombre] el [fecha]" + - Edición inline del título + - Swipe para eliminar + - Drag & drop para reordenar + - Tareas completadas al fondo (no se ocultan) +- [ ] Sincronización vía polling a intervalos de 5 segundos cuando la vista está activa + +#### 3.2 Notas + +- [ ] Migración: tabla `notas` con `color`, `fijada`, `archivada`, `eliminada`, `eliminada_en` + RLS +- [ ] Job `pg_cron`: borrado definitivo de notas en papelera con `eliminada_en` > 7 días +- [ ] Pantalla `/notas` — tablero tipo masonry: + - Notas fijadas en bloque superior + - Notas activas en rejilla de colores +- [ ] Editor de nota (`/notas/[id]`): + - Editor de texto con Markdown básico (negrita, cursiva, listas, cabeceras) + - Guardado automático con debounce de 500ms + - Selector de color (8 opciones predefinidas) + - Opciones: fijar, archivar, mover a papelera, duplicar +- [ ] Archivo y papelera accesibles desde `/notas/archivo` y `/notas/papelera` +- [ ] Sincronización vía polling a intervalos de 30 segundos + +--- diff --git a/plan/fase-4-busqueda-pulido.md b/plan/fase-4-busqueda-pulido.md new file mode 100644 index 0000000..79f84a1 --- /dev/null +++ b/plan/fase-4-busqueda-pulido.md @@ -0,0 +1,36 @@ +### Fase 4 — Búsqueda y Pulido Final +**Duración estimada: 1 semana** +**Objetivo: completar el MVP con calidad de producción.** + +#### 4.1 Búsqueda global + +- [ ] Función PostgreSQL `buscar_en_colectivo(colectivo_id, query, filtros)` usando `tsvector` + `tsquery` +- [ ] Columnas `tsvector` en `items_compra`, `tareas`, `notas` con índice GIN +- [ ] Trigger para mantener los vectores actualizados en cada INSERT/UPDATE +- [ ] Pantalla `/buscar`: + - Input de búsqueda con debounce 300ms + - Resultados agrupados por tipo (listas, tareas, notas) + - Filtros: tipo de contenido, miembro creador, rango de fechas, estado + - La búsqueda no opera sobre papelera (RN-08) + +#### 4.2 Perfil de usuario + +- [ ] Pantalla `/perfil`: + - Editar nombre y avatar (Supabase Storage) + - Lista de colectivos a los que pertenece con rol + - Opción de abandonar colectivo + - Eliminar cuenta (con aviso sobre promoción de admin si aplica) + +#### 4.3 PWA — Validación final + +- [ ] Lighthouse PWA score ≥ 90 en mobile +- [ ] Instalable en iOS (Safari → Añadir a pantalla de inicio) y Android (Chrome → Instalar app) +- [ ] Notificaciones push configuradas para iOS 16.4+ (requiere PWA instalada) +- [ ] Offline completo verificado: desconectar red, operar en todos los módulos, reconectar y verificar sync + +#### 4.4 Seguridad y hardening + +- [ ] Auditoría de políticas RLS: verificar aislamiento total entre colectivos con tests de integración +- [ ] Rate limiting en Kong para endpoints de auth y invitaciones +- [ ] Headers de seguridad en nginx: CSP, HSTS, X-Frame-Options — gestionados externamente en la config de nginx del VPS +- [ ] Rotación de `JWT_SECRET` y `ANON_KEY` para producción (no usar los valores por defecto de Supabase)