From f5903ef442dbbe8c64c5abc5ec2ccfa9efa749b5 Mon Sep 17 00:00:00 2001 From: Oier Bravo Urtasun Date: Sun, 12 Apr 2026 06:35:07 +0200 Subject: [PATCH] Add planning docs, design system, and Stitch mockups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .stitch/designs/01-lists-view.png | Bin 0 -> 22680 bytes .stitch/designs/02-shopping-session.png | Bin 0 -> 14499 bytes .stitch/designs/03-notes-view.png | Bin 0 -> 50969 bytes .stitch/designs/04-tasks-view.png | Bin 0 -> 28450 bytes .stitch/designs/05-list-detail.png | Bin 0 -> 23115 bytes CLAUDE.md | 152 +++++++ DESIGN.md | 442 +++++++++++++++++++ README.md | 192 +++++++++ analysis/analisis-funcional.md | 541 ++++++++++++++++++++++++ plan/fase-0-infraestructura.md | 372 ++++++++++++++++ plan/fase-1-auth-colectivo.md | 93 ++++ plan/fase-2a-lista-compra-crud.md | 106 +++++ plan/fase-2b-realtime-modo-compra.md | 40 ++ plan/fase-3-tareas-notas.md | 34 ++ plan/fase-4-busqueda-pulido.md | 36 ++ 15 files changed, 2008 insertions(+) create mode 100644 .stitch/designs/01-lists-view.png create mode 100644 .stitch/designs/02-shopping-session.png create mode 100644 .stitch/designs/03-notes-view.png create mode 100644 .stitch/designs/04-tasks-view.png create mode 100644 .stitch/designs/05-list-detail.png create mode 100644 CLAUDE.md create mode 100644 DESIGN.md create mode 100644 README.md create mode 100644 analysis/analisis-funcional.md create mode 100644 plan/fase-0-infraestructura.md create mode 100644 plan/fase-1-auth-colectivo.md create mode 100644 plan/fase-2a-lista-compra-crud.md create mode 100644 plan/fase-2b-realtime-modo-compra.md create mode 100644 plan/fase-3-tareas-notas.md create mode 100644 plan/fase-4-busqueda-pulido.md diff --git a/.stitch/designs/01-lists-view.png b/.stitch/designs/01-lists-view.png new file mode 100644 index 0000000000000000000000000000000000000000..bd798cad1dfbaaea8e4995818ec613e2b1126359 GIT binary patch literal 22680 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
m%W+HaroRK+& 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

qwL^LRov|nZ*KcvM0<>Z&5cncsDjJ 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 literal 0 HcmV?d00001 diff --git a/.stitch/designs/04-tasks-view.png b/.stitch/designs/04-tasks-view.png new file mode 100644 index 0000000000000000000000000000000000000000..59220c44e54458007a81195b1c8b96015936fca5 GIT binary patch literal 28450 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 F{{lko#Fqd7 literal 0 HcmV?d00001 diff --git a/.stitch/designs/05-list-detail.png b/.stitch/designs/05-list-detail.png new file mode 100644 index 0000000000000000000000000000000000000000..9fdaaae4fa3c56345bcf141ff3775a41ddcb69a4 GIT binary patch literal 23115 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 # 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)