From cb223a0c2943176b70080bb30b66291f31ef629f Mon Sep 17 00:00:00 2001 From: Oier Bravo Urtasun Date: Tue, 19 May 2026 01:11:57 +0200 Subject: [PATCH] feat(fase-17): CHANGELOG.md backfill + Keep-a-Changelog linter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds repo-root CHANGELOG.md in Keep-a-Changelog format with a single [v0.0.0-beta] section that catalogues every shipped feature across Fases 0–16 (58 bullets, [new]/[fix]/[tweak] prefixes, grouped by area). The companion linter at scripts/check-changelog.mjs enforces (a) the Keep-a-Changelog header, (b) the `## [vX.Y.Z] — YYYY-MM-DD` heading shape on every release, and (c) the three-prefix rule on every bullet. Wired into `just check` so a malformed CHANGELOG fails the check gate without blocking commit hooks. Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 96 +++++++++++++++++++++++++++++++ Justfile | 3 +- scripts/check-changelog.mjs | 109 ++++++++++++++++++++++++++++++++++++ 3 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md create mode 100755 scripts/check-changelog.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..38d9b71 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,96 @@ +# Changelog + +All notable changes are documented here. Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versions follow [Semantic Versioning](https://semver.org/). + +Each bullet is prefixed with `[new]` (added capability), `[fix]` (resolved +defect) or `[tweak]` (behaviour or implementation change with no new +surface). Areas group related entries; section bullets ending in `:` are +labels for the area, not log entries. + +## [Unreleased] + +## [v0.0.0-beta] — 2026-05-19 — MVP + MVP2 (Fases 0–16) + +Single rollup for everything shipped before tagging started. Covers the +entire MVP cycle (Fases 0–7) plus the prod auth hardening pass (Fase 8) +and the eight MVP2 fases (9–16). Reads as a feature catalogue, not a +day-by-day diff — the per-fase docs in `docs/history/` and the per-fase +plans in `plan/` are the source of record. + +- Auth + identity: +- [new] Keycloak → GoTrue → Supabase OIDC integration (PKCE), with `auth.users` pre-seed for the 5 dev users (Fase 1). +- [new] Self-registration via Keycloak; invitations are link-only (no email), generated on `/collective/manage` (Fase 1). +- [new] Multi-collective per user with sidebar switcher; active collective persisted to `localStorage` (Fase 1). +- [new] Atomic `create_collective(name, emoji)` RPC so onboarding works in a single round-trip without tripping `is_admin` / `is_member` policies (migration 012, Fase 7). +- [new] Public `/logged-out` route + RP-initiated Keycloak end-session on logout, so the SSO cookie is actually cleared and the next login re-prompts (Fase 8). +- [new] Account hard-delete (`delete_account()` RPC) with sole-admin auto-promote and a Danger-zone modal (migration 021, Fase 10.5). +- [fix] `auth.users.role`/`aud` default-fill trigger so PostgREST routing survives the Keycloak provisioning path; INSERT + UPDATE both covered to defeat GoTrue's pop-ORM re-clobber (migrations 013–014, Fases 7–8). +- [fix] `isLoggingOut` guard in `(app)/+layout.svelte` to suppress the auth-gate `$effect` during the logout navigation, closing a "PKCE code verifier not found in storage" race on prod (Fase 8). +- [fix] `pendingInvitationToken` restored from `sessionStorage` after login so the user lands on `/invitation/` instead of `/onboarding` or `/lists` (Fase 7). + +- Shopping lists + items: +- [new] Lists overview (featured + grid + completed + trash) and detail view with checkbox toggle, qty stepper, inline edit, drag-reorder (Fase 2a). +- [new] Frequency-driven suggestion chips on the add-item form, backed by the `item_frequency` table + trigger (Fase 2a; migration 006). +- [new] Realtime "Modo Compra" full-screen session view with 56 px toggles, flip animation, complete-list → redirect; postgres_changes subscription per list with echo-deduplication (Fase 2b; migration 007). +- [new] Offline-first sync queue (`SyncQueue` IDB-backed, FIFO flush, PK-conflict-as-success) with `online`-event auto-flush; offline/syncing banner mounted in detail + session views (Fase 2b). +- [new] Selection mode with bulk delete / move / mark-checked, long-press entry on mobile + desktop toggle button (Fase 5.11). +- [new] Reset-from-detail button on completed lists; "Crear nuevo colectivo" entry in sidebar + drawer (Fase 10). +- [new] Trash drawer restore + hard-delete via `restore_list()` / `hard_delete_list()` RPCs (migration 020, Fase 10.4). +- [new] Collective-scoped item tags (chips + picker + filter bar + realtime resort) backed by `item_tags` + `shopping_item_tags` (migration 022, Fase 11). +- [new] Common-items management on `/collective/manage` — 3-way Hide / Normal / Boost segmented control writing `weight` via admin-only RPCs; suggestion strip reorders by weight + use_count + last_used_at (migration 026, Fase 15). +- [new] Suggestion dropdown excludes names already on the active list (`excludeNames` capped at 200 to respect Kong URI limits) (Fase 15). +- [tweak] Item row redesigned to `drag-handle | name-button | qty stepper` with swipe-to-(un)check; double-tap opens an edit overlay; `unit` column dropped because it became unreachable (migration 011, Fase 5.10). +- [tweak] Add-form moved inline as the last child of the unchecked section; sticky bottom bars retired (Fase 5.14). +- [tweak] List rename inline at the top of the detail view with 500 ms debounced autosave; "New list" button moved to the masthead (Fase 5.8). + +- Tasks + notes: +- [new] Task lists + tasks with completion-consistency CHECK (`is_completed ⇔ completed_by ⇔ completed_at`) and drag-reorder (migration 008, Fase 3). +- [new] Notes with 8-colour palette, pinned + archived (mutually exclusive), 7-day trash window enforced via RLS + `pg_cron` purge; editor with 500 ms debounced autosave (migration 009, Fase 3). +- [new] Sync-conflicts review route at `/settings/sync-conflicts`, paginated, side-by-side local vs remote payloads, per-device dismissal via `localStorage` (Fase 14.2). + +- Search: +- [new] Cross-section search via `search_in_collective()` SECURITY INVOKER RPC over generated `tsvector` columns on items/tasks/notes; debounced input, filter chips, deep-link per result type (migration 010, Fase 4). + +- Mobile UX + accessibility: +- [new] Mobile shell — top bar with hamburger + collective name + avatar, slide-in drawer, bottom tab bar with safe-area inset, sticky contextual headers on detail routes (Fase 5). +- [new] `UndoToast` for destructive actions (4 s TTL, auto-commit, mounted globally), with `scheduleUndoable()` queue (Fase 5). +- [new] Spinner component (`sm/md/lg`, `currentColor`, `motion-reduce:animate-none`, sr-only label) integrated at 7 loading sites (Fase 16). +- [tweak] Desktop reading width capped at `max-w-2xl` for content detail routes; mobile and `/lists/[id]/session` excluded (Fase 9.2). + +- PWA + offline: +- [new] Installable PWA — Workbox precaching via `@vite-pwa/sveltekit`, manifest, iOS meta tags + apple-touch icons, placeholder slate-and-"C" icons (Fase 6.2). +- [new] Service-worker auto-registration via `useRegisterSW` + 15 min `reg.update` poll; `UpdateToast` with "Reload" action when a new build is available; "Ready to use offline" auto-dismiss toast (Fase 14.1). +- [new] `OfflineChip` in `MobileTopBar` + `DesktopSidebar` with a pending-ops badge when the queue is non-empty (Fase 14.2). +- [new] Build-time version constants (`__APP_VERSION__` / `__APP_COMMIT__` / `__BUILD_TIME__`) exposed via `$lib/version`; "Acerca de" chip on `/settings`; `GIT_SHA` baked through the Dockerfile build arg by the deploy script (Fase 14.3). +- [tweak] PWA strategy on prod switched to `generateSW` because `@vite-pwa/sveltekit` 0.6.8 hardcodes the source SW filename under `injectManifest` (Fase 6 → revisit in Fase 14). +- [fix] `` added to `app.html` so Android Chrome surfaces the install prompt (Fase 14 follow-up). + +- Collective management: +- [new] Rename collective + change emoji (CU-H07) on `/collective/manage` (Fase 10.2). +- [new] Leave-collective flow (CU-H06) with sole-admin auto-promote via `leave_collective()` RPC; sole-member rejected with P0001 (migration 017, Fase 10.1). +- [new] Dissolve-collective flow (CU-H08) with type-the-name confirmation, via `dissolve_collective()` RPC (migration 018, Fase 10.3). +- [new] Section visibility — per-user + per-collective `feature_flags jsonb` + `section_enabled()` STABLE SQL helper using COALESCE precedence (migration 023, Fase 12). +- [new] Auto-detect language on first sign-in from `navigator.languages`, gated to fresh accounts (`<60 s old` + `language = 'en'`) (Fase 10.6). +- [tweak] Every `created_by`/`updated_by` FK relaxed from `NOT NULL RESTRICT` to `NULL SET NULL` so a user delete cascades cleanly (migration 019, Fase 10.5). + +- Server admin: +- [new] Global `server_admin` role (`server_admins` table) orthogonal to `collective_members`; `is_server_admin()` SECURITY DEFINER gate; bootstrap from `SERVER_ADMIN_EMAIL` on first DB init (migration 024, Fase 13). +- [new] `/admin` route group with 9 SECURITY DEFINER RPCs (collective lifecycle, member removal, server-level section defaults) + append-only `admin_actions` audit log + last-admin guard (migration 025, Fase 13). +- [new] `section_enabled()` extended with a server layer prepended to the user/collective precedence chain (Fase 13.2). + +- i18n: +- [new] Paraglide JS messages for `en` + `es`, compiled at build time; `setLanguageTag()` flips the active locale without reload; selector on `/settings` writes to `public.users.language` (Fase 0). +- [new] Search + collective-management + PWA copy fully localised in both languages across every fase that added user-facing strings (Fases 4, 10, 14). + +- Infra + deploy: +- [new] Local dev stack — Docker compose with Postgres / GoTrue / PostgREST / Realtime / Storage / Kong / Keycloak; `setup.sh` for idempotent first-time bring-up; `Justfile` task runner (Fase 0). +- [new] Production stack on ambrosio (OVH VPS) at https://erosi.limonia.net — `infra/scripts/deploy-erosi.sh` (rsync + first-deploy secret generation + every-deploy migration apply + pre-deploy backup + `.deploys.log` append) and `infra/scripts/rollback-erosi.sh` (paired code + DB rollback) (Fase 6 + later iterations). +- [new] Kong rate-limit on `/rest/v1/collective_invitations` (20/hour per Authorization header) with `request-transformer` URI rewrite (Fase 6.3). +- [new] JWT rotation script (`infra/scripts/rotate-jwt.sh`) + production env template + `rls-audit.sh` curl sweep + `just lighthouse` build-and-score recipe (Fase 6.4 + 6.5). +- [new] Internal Caddy on `:3000` fans `/rest|/auth|/realtime|/storage|/graphql|/pg` → Kong, everything else → app; external TLS terminator (off-stack) forwards plain HTTP to ambrosio:3000 (Fase 6 → revision after prod cutover). +- [new] Theme tokens stored as RGB triplets so `dark:` variants resolve consistently; three-mode preference (`light` / `dark` / `system`) persisted to `users.theme` + mirrored to `localStorage` with inline anti-FOUC script (migration 015, Fase 9.1). +- [new] `sync_conflicts` append-only log + queue-side divergence detection so last-write-wins conflicts are auditable (migration 016, Fase 9.3). +- [new] Pre-deploy `pg_dumpall` snapshot kept in `backups/predeploy-*.sql.gz` (newest 10) with paired code/DB rollback via `rollback-erosi.sh --previous` / `--to=` / `--code-only` (Fase 14 deploy work). +- [fix] `docker compose exec -T` calls inside SSH heredoc deploys now redirect stdin per call so the surrounding heredoc isn't drained — silently skipped migrations no longer happen (Fase 8 deploy hardening). +- [fix] PostgREST schema cache reload (`NOTIFY pgrst, 'reload schema'`) appended after every deploy and rollback so freshly-added RPCs are visible without a container restart (Fase 15 deploy work). +- [fix] WebKit Playwright project gated behind `RUN_WEBKIT=1` so hosts without the GTK libs (libicu74, libwoff1, …) keep `just test-all` green; `*.webkit.test.ts` files excluded from the chromium project (Fase 9.4). diff --git a/Justfile b/Justfile index b0f6eae..1772b95 100644 --- a/Justfile +++ b/Justfile @@ -94,8 +94,9 @@ serve: build {{dc_dev}} up -d PORT=3000 node apps/web/build/index.js -# Type-check all packages +# Type-check all packages (also lints CHANGELOG.md — Fase 17.4.3) check: + node scripts/check-changelog.mjs pnpm turbo run check # Lint all packages diff --git a/scripts/check-changelog.mjs b/scripts/check-changelog.mjs new file mode 100755 index 0000000..8ef2e00 --- /dev/null +++ b/scripts/check-changelog.mjs @@ -0,0 +1,109 @@ +#!/usr/bin/env node +/** + * Fase 17.4.3 — CHANGELOG.md linter. + * + * Validates three things and exits non-zero on the first failure: + * + * 1. Keep-a-Changelog header line is present. + * 2. Every release heading matches `## [vX.Y.Z] — YYYY-MM-DD` or the + * placeholder `## [Unreleased]`. Pre-release / build-metadata + * suffixes are allowed on the version (e.g. `v0.0.0-beta`, + * `v1.0.0-rc.0`). + * 3. Every bullet inside a release section starts with one of the + * three prefixes `[new] / [fix] / [tweak]`. Bullets that introduce + * a sub-grouping header (lines ending in `:`) are exempted so the + * area-by-area structure of the beta backfill can use plain bullets + * as section titles. + * + * Wired into `just check` and runnable standalone: + * node scripts/check-changelog.mjs [path] + * + * Exit codes: + * 0 — file passes all three checks + * 1 — usage error (missing file) + * 2 — validation failure (one or more issues printed to stderr) + */ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const repoRoot = resolve(__filename, '..', '..'); + +const target = resolve(process.argv[2] ?? resolve(repoRoot, 'CHANGELOG.md')); + +let raw; +try { + raw = readFileSync(target, 'utf8'); +} catch (err) { + console.error(`check-changelog: cannot read ${target}: ${err.message}`); + process.exit(1); +} + +const lines = raw.split('\n'); +const errors = []; + +// Check 1 — Keep-a-Changelog header. +if (!raw.includes('Keep a Changelog')) { + errors.push('Missing Keep-a-Changelog reference in the header.'); +} + +// Walk through lines. Track when we are inside a release section. +const RELEASE_HEADING = /^## \[(.+?)\](?:\s+—\s+(.+))?$/; +const VERSION_RE = /^v\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/; +const DATE_RE = /^\d{4}-\d{2}-\d{2}/; +const BULLET = /^[-*]\s+(.*)$/; +const ALLOWED_PREFIXES = ['[new]', '[fix]', '[tweak]']; + +let insideRelease = false; +let currentRelease = null; + +for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const m = line.match(RELEASE_HEADING); + if (m) { + const [, label, rest] = m; + currentRelease = label; + insideRelease = true; + if (label === 'Unreleased') { + // no further checks + continue; + } + if (!VERSION_RE.test(label)) { + errors.push( + `Line ${i + 1}: release label "${label}" does not match vX.Y.Z (semver-with-optional-prerelease).` + ); + } + if (!rest || !DATE_RE.test(rest)) { + errors.push( + `Line ${i + 1}: release heading "${label}" is missing the "— YYYY-MM-DD" date segment.` + ); + } + continue; + } + + if (!insideRelease) continue; + + const bm = line.match(BULLET); + if (!bm) continue; + + const body = bm[1].trim(); + if (!body) continue; + + // Sub-grouping bullets end with ":" and carry no prefix (e.g. "- Auth + identity:"). + if (body.endsWith(':')) continue; + + const ok = ALLOWED_PREFIXES.some((p) => body.toLowerCase().startsWith(p)); + if (!ok) { + errors.push( + `Line ${i + 1} (in "${currentRelease}"): bullet missing [new]/[fix]/[tweak] prefix — "${body.slice(0, 80)}".` + ); + } +} + +if (errors.length > 0) { + for (const e of errors) console.error(`check-changelog: ${e}`); + process.exit(2); +} + +console.log(`check-changelog: ${target} OK`);