Ctrl K
ComunidadGuías…

CHANGELOG.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Changelog

All notable changes to this project are documented here, one section per
version. The roadmap lives on
[feedback.loomkeep.app/roadmap](https://feedback.loomkeep.app/roadmap). The
`version` field in `package.json` (root + every `apps/*`/`packages/*`, kept
in lockstep) is the record of where the app stands; the only tagged releases
are the Android app's (`android-v<version>`). See `CLAUDE.md` → Conventions
for the versioning rule.

This file is updated on each version bump; it does not reconstruct the
history from before it was started.

## [Unreleased]

### Added

- Person pages: open a film's director, a series' creator or a cast member
  to see everything they've made, role by role, with your status on each
  title. Appearances on talk shows and in documentaries are listed apart. On
  a computer, resting the pointer on their name gives a quick look first.
- Author pages: open a book's author to see their books, series first, the
  best known on top, with your reading status on each.
- Admin › Users: the account panel shows the reason given when a deletion is
  scheduled, and each library tile (movies, series, anime, games, books,
  albums) opens the list of works behind its counter.
- Messages on a phone: press and hold a conversation to mark it read, mute
  it, pin it to the top, open the friend's profile or delete it from your
  list (it comes back, history included, with the next message).
- Same on a work's discussion: mark read, mute, pin, go to the title, or
  stop following it until you write in it or someone mentions you.
- Rewatching: mark an episode of a show you've finished as seen again and
  Loomkeep asks whether you're rewatching it. If so, the show goes back to
  in progress and into To watch with its ticks cleared (rewatch counts
  stay), picking up after the episode you've just seen, until every episode
  is seen again. A movie you've seen can go back to To watch from its "…"
  menu.

### Changed

- Android app: 18+ films, anime and games show no poster, cover, backdrop
  or trailer, as Google Play requires. The website is unchanged.
- Video page: a seen movie shows how many times you've watched it; a
  finished title no longer offers "Drop", and a dropped title never started
  offers "Put back in To watch" instead of "Resume".

- Video library: sorted by recent activity, a title not watched yet (a
  planned movie) now sits where it was added among the series you're
  watching, instead of after all of them.
- Video, game and book libraries: the status pills of what shows by default
  come first, then, past a divider, Finished and Dropped.
- Video, game and book libraries now show only what's still ahead (in
  progress, to watch, paused, ghost…) until a status is picked: finished and
  dropped titles appear once their own pill is selected. Music still shows
  everything.
- Sagas view: the search sits beside the Works/Sagas switch as in the works
  view, and type and sort move into the same "Filters" panel.
- Self-hosting: the nightly achievements sweep now covers accounts active
  in the last two days, and every account on Sundays.
- Self-hosting: the games and books catalogue refreshes ask much less of
  IGDB and Open Library.
- Self-hosting: an ended or canceled series is refreshed once a week
  instead of every day.
- Landing page: a questions section answers what people ask before signing
  up (what if the project stops, imports, ads, servers, phone, deleting an
  account).
- Docs: Guide › Your data explains how to have an account deleted when you
  can't sign in anymore.
- Landing page: a clearer promise under the title, "free · no ads · full
  export" next to the main button, and shorter, plainer section texts.
- Onboarding: albums join the welcome screen, and release alerts are
  described as what they are, a digest at the pace you choose. An empty
  library now also offers to import your history.
- Game, book and album pages take the video page's layout: full-width image
  header, the same sticky action bar (with the discussion), then the
  tracking. Sessions show as one progress line with the session button and
  timer; the week, totals and history open in "My sessions".
- Android app: a larger system font now grows the text without swelling
  bars and spacing; tab labels stop at 130% and page titles wrap instead of
  being cut.
- Profile: counters on one line (comments dropped), labelled Edit and Share
  buttons, and "Member since" moved to the bottom of the page.
- Settings › Visibility: who sees each domain's library and activity is one
  table, with an "All domains" row that sets a whole column at once.
- Title pages: the tagline and ratings stay readable on a bright backdrop,
  and the trailer button moves to the top corner.
- GitHub releases are named `X.Y.Z`; the CHANGELOG heading's subtitle opens
  the notes.
- Settings › Security: the app lock now sits above "Go further".
- Android app lock: a redrawn lock screen, touch the fingerprint to unlock.
- Phone navigation: a central Library tab fans out your domains, Alerts and
  Messages move to a movable edge tab, and the menu opens by pulling the bar up.
- Libraries: status pills on one sticky bar, everything else (favorites,
  sort, saved views, display) in a single Filters panel; active filters stay
  removable from the bar.
- Libraries: borderless posters, three per row on a phone (two or three, small
  or large on desktop); a long press opens favorite, status and review.
- First-run welcome: full screen on phone and desktop, swipeable steps, a new
  optional profile step (photo, username, visibility), streaming services and
  notifications that ask the device's permission, and a membership card that
  flies into your avatar as the home lands. Settings › Help can replay it.
- The phone bar's Profile tab shows your avatar; pins are drawn as a pushpin.
- After sign-up, "Check your email" sits on the poster wall with a sealed
  envelope, opens your webmail (Gmail, Outlook…) in one tap, and turns to
  "Address confirmed" by itself once the link is opened, on any device.
- Phone layout: pages open on a compact title bar (no back arrow on a tab's
  own page), sections become filled blocks with their title above, and
  subtitles that only repeated the page are gone.
- Empty screens show an amber line drawing, one sentence and what to do
  next, instead of a dashed box; small text no longer drops below 11px.
- Phone home: its own page, apart from the desktop grid — a poster shelf per
  domain, then the week's releases, with your level and streak up top. Its
  editor is a list: drag to reorder, add any widget, choose posters or rows.
- A new account's phone home opens on one screen: find a title or import
  your history.

### Fixed

- The open notifications panel no longer refreshes itself non-stop.
- The home's "To watch" strip no longer scrolls a few pixels vertically.
- A slow IGDB answer no longer fails the whole games catalogue refresh.
- A review or list set to public on a private profile is visible to friends
  again, as a friends-only one is; strangers still don't see it.
- Passkeys created or used in the Android app are no longer rejected with
  "Security key verification failed" (the API now reads `ANDROID_APP_LINKS`
  too).
- Selecting episodes by dragging scrolls at full speed near the list's edges,
  without a jump when the finger lifts.
- Email confirmation and account-deletion cancel links no longer turn to
  "invalid link" after they worked.
- Empty video, game and music libraries name what to add in your language
  instead of always in French.

## 1.13.0 — Home-screen widgets, actionable notifications, and a phone-first app

### Breaking changes

- **Self-hosting: drop your reverse proxy's `Content-Security-Policy`.** The
  web app now enforces its own (nonces, no `'unsafe-inline'` scripts); a
  proxy's header stacks with it. Integration origins are read from the env
  already set (`PUBLIC_API_URL`, `PUBLIC_UMAMI_SCRIPT_URL`, …).
- **Mobile bottom bars reset to the defaults**: the `mobileNavShortcuts`
  column is dropped, shortcuts now live in each device's `localStorage`.
- `POST /api/security/csp-report` is removed.

### Added

- **Home-screen widgets** (Android, Glance): To watch (✓ marks the next
  episode), Releases, Chrono (drives the session timer), Streak and level,
  Search; posters or ticket list, 2 × 2 to 5 × 5. Fed by
  `/api/home-screen-widgets` with a session-bound token.
- **Actionable notifications** (Android): reply, mark read, accept a follow,
  follow back, add a sequel, from the notification
  (`POST /api/notifications/actions`, per-device signed tokens).
- **App lock** (Android 10+, `BiometricPrompt`) in Settings › Security.
- **Self-hosted instances open in the app** through `loomkeep://open`, after
  confirming the instance switch.
- **Episodes rework**: seasons as swipeable tabs (100 episodes per tab),
  one-tap marking, multi-selection for bulk mark/rewatch/unmark with a 5 s
  undo. New `POST /library/episodes/watches/batch` and `/undo`.
- **Card-less sign-in, sign-up and two-step pages**, with a poster wall on
  wide screens fed by the public `GET /api/showcase` (trending covers,
  computed daily). Two-step codes get one box per digit and submit on
  completion.
- Recent searches (six, stored on the device), a "System" theme, back
  buttons on phone pages, haptic feedback on marking and unlocks.
- Passkeys in the Android app (WebView WebAuthn + Credential Manager
  `assetlinks.json` grant), and a one-time passkey offer after a password
  re-confirmation.
- Admin › Communications › Push sends any alert as a sample.

### Changed

- **Phone-first web**: menus and filters as bottom sheets, swipe between
  tabs, pull to refresh, compact header bar, tab positions kept, larger touch
  areas, pressed states, page transitions.
- Bottom bar: up to six shortcuts, Messages and Alerts included.
- Work pages lead with progress, synopsis and episodes; streaming services
  show as a row of logos. The calendar lists only days with releases, one row
  per release.
- Android shell: continuous launch screen, branded offline page, native
  pickers in Séance colours, grouped notifications with avatars and posters,
  portrait on phones, haptics through Android's own feedback (needs the
  1.13.0 APK).
- Statistics: a single locked block for free accounts instead of one per
  section.
- web-audit runs Pa11y, Lighthouse and ZAP over every page; every component
  test ends with an axe pass.

### Fixed

- Desktop home: no more empty scroll under a long activity feed.
- Phone menu keeps Home, Search, Messages and Alerts reachable when they're
  out of the bottom bar.
- Android: no gap or black band above the keyboard, back button in Messages
  and side panels, context-appropriate keyboard Enter actions.
- Long works no longer widen the page past the bottom bar (carousel dots).
- Newsletter unsubscribe page no longer reports a server error on success.
- Screen-reader fixes found by axe and Pa11y (names, table headers, skip
  link, relative dates), and better contrast in the mobile navigation.
- Releases get their APK again: created as drafts, published once the APK is
  attached (1.12.0 has none).

### Security

- Content-Security-Policy enforced (see Breaking changes); `img-src` narrowed
  to the catalogs' hosts and the instance's API.
- `glob` 14.0.2 in the images (CVE-2026-118773, CVE-2026-118764).

## 1.12.0 — The Android app, imports from every tracker, and commands in Messages

### Before upgrading a self-hosted instance

- **Ports stay on the machine.** Without the HTTPS override, ports 8080 and
  3000 are published on `127.0.0.1` only: Docker's published ports bypass
  `ufw`, so `0.0.0.0` exposed a plain-HTTP API on any server with a public
  address. An instance reached from another device (a NAS) needs
  `LOOMKEEP_BIND_ADDRESS=0.0.0.0` in `.env`.
- **`latest` is now the last release**, no longer every commit on `main`.
  Images are also tagged `1.12.0`, `1.12` and `1`; `edge` follows `main`.
  `IMAGE_TAG` picks one (docs › Upgrades & backups).
- **Out of search engines by default.** `robots.txt` answers `Disallow: /`
  and pages carry `noindex` unless `PUBLIC_SEARCH_INDEXING=true`.
- **The API container runs as `node`**, not root; its entrypoint hands the
  existing backup volume over at startup. The web container runs as `node`
  too.
- **`PUBLIC_IS_BETA` is read as a boolean**: only `"true"` shows the beta
  badge, where compose's default `"false"` used to show it everywhere.
- **arm64 images exist for the API too.** Each platform is built natively;
  the emulated arm64 API build had always failed, silently.

### Android app

- **A Capacitor shell for Android** (`apps/mobile`) that loads the
  instance's `/app` rather than bundling the web: back button, icon and
  launch screen, share sheet for exports and links, App Links for
  loomkeep.app URLs, app icon shortcuts, status bar in the app's theme.
  Its data stays out of Android backups and device transfers.
- **Native push** through Firebase Cloud Messaging (MOB-04/05), and
  **native ISBN and profile QR scanning**
  with Google's code scanner (MOB-07/19).
- **Simkl connects in an in-app tab** whose redirect returns to the app
  (MOB-09), and other apps can share links to Loomkeep (MOB-18).
- **Another instance than loomkeep.app** can be picked at runtime, from
  the login page or the offline page (MOB-15). The offline page's Retry
  only follows an `http(s)` address: a link opening it with a
  `javascript:` one ran script in the page (CodeQL `js/xss`).
- **Releases**: a signed APK per version, attached to the GitHub release
  with a Sigstore provenance attestation; `android-dev-<n>` pre-releases
  of `main` for testing, followable in Obtainium. The landing page and the
  docs present the app. The web only calls a native plugin once
  `Capacitor.isPluginAvailable()` says the installed APK has it.

### Imports

- **Letterboxd and IMDb imports**, and an **AniList** list by user name.
- **Trakt and TV Time bring everything they hold**: ratings (season and
  episode ones included, and titles rated but never watched), favourites,
  custom lists, statuses, memos and add dates. The preview shows the rated
  seasons, episodes and lists before committing. TV Time shut down on
  2026-07-15: its import reads an export made before then.

### Messages and social

- **Commands**: `/review` and `/list` attach a review or a list,
  `/poll` asks the conversation, `/saga` shows a series in order, `/pick`
  rolls through the pile before landing on a work, `/where` finds where to
  stream a film or series, and `:emoji:` completes emoji.
- **Live formatting in the composer** (SOC-09): a CodeMirror field whose
  markdown markers only show while the caret is inside them. Emoji alone
  show large, and comments complete emoji too.
- **Friends on a work's page** (SOC-03): who tracks it and where they stand.

### Library and works

- **Unified work search** across every enabled domain, as a "Tout" tab
  first in search and in the home quick search, behind `/reco` and `#` in
  Messages, and on a list's page to add a work without tracking it.
- **Episodes and seasons rework**: stills, synopses, scores and
  translations stored per episode; a seasons strip, richer rows and an
  episode panel with a timeline and arrow-key navigation.
- **Completion estimates** for series and games, from the viewer's pace.
- **A note on each list item** (UX-05) and **the platform a game is played
  on** (UX-03).
- **Paused works are recalled** after a while, by an alert of their own.
- The media library ranks and filters on progress kept by Postgres
  triggers (`LibraryEntry.watchedEpisodes`/`lastWatchedAt`,
  `MediaItem.episodeCount`) instead of aggregating every watch per page.
- Book series are kept in the database, and film sagas' translated titles
  too, instead of asking Open Library or TMDB again.

### Account, security and notifications

- **Account deletion is deferred**: a self-deletion is scheduled and can be
  cancelled from the email it sends, before it runs.
- **Confirm it's you** (TRU-05): sensitive actions ask a recent sign-in
  check (`@RequireRecentAuth`) in a modal.
- **Notifications**: a reworked bell and a history page; read
  notifications are kept 90 days instead of deleted. Admins are alerted
  when a newer Loomkeep version is out.
- Two tabs refreshing at once keep their session, and a sign-in no longer
  ends another machine's session. Passwordless login options answer
  stable decoys for unknown accounts, reported content changed before
  moderation is flagged, and a self-host's mail replies no longer default
  to Loomkeep's mailbox. Caddy's admin API stays on loopback, metrics move
  to `:2020`. The nightly backup streams from `pg_dump` through `age`.
- `/.well-known/security.txt` lists where to report a vulnerability.

### Site, accessibility and SEO

- The legal pages declare their French text as French, the admin area no
  longer nests a second `<main>`, the app has a skip-to-content link, the
  landing's rotating verb pauses on hover and reads as one sentence to
  screen readers, and the light theme's amber reaches 4.5:1 on every
  surface.
- Italian is kept from the boot script on (`<html lang>`, manifest).
- The maintenance page is in English with a French switch, without
  JavaScript.
- Each public page has its own description, canonical URLs drop their
  query string, the landing carries `SoftwareApplication` structured data
  and credits the catalogues with TMDB's notice, the comparator's facts
  are dated, the poster wall weighs half as much, and the Latin fonts are
  preloaded.
- Documentation search (Pagefind).

## 1.11.0 — Messages, sagas, and a unified notifications registry

- **Friends can exchange private messages.** Conversations exist between
  two accounts that follow each other, behind the `chatEnabled` instance
  setting (`CHAT_ENABLED`). Messages carry restricted markdown, can be
  edited, deleted (tombstoned), reacted to and reported; typing, presence
  and read receipts travel over the socket, the last two as reciprocal
  per-account settings. A push goes out only when the recipient has the app
  open nowhere, and never carries the text. Reported messages reach the
  moderation queue with their surrounding context and can be taken down
  (`MESSAGE_REMOVED`); a deleted account's messages are erased and
  conversations are part of the data export. Messages can carry work cards
  (a `MessageEmbed` snapshot, so display costs no lookup) attached by hand or
  resolved from up to three links, never for 18+ titles. A work's "…" menu
  gains Share (friends first, system share sheet, link, QR code), which
  sends through `POST /chat/recommendations` after checking every recipient.
  Conversations also support pins (fifty at most), forwarding without naming
  the author, mark-as-unread with a "New" line, search, a ledger of shared
  works, `#` work mentions and episode codes. A feedback launcher now needs
  `PUBLIC_QUACKBACK_URL` and can be hidden per account.
- **Work discussions are followed from Messages.** The "Œuvres" tab lists
  the discussions a member wrote in or was mentioned in, with unread counts
  kept in `CommentThreadRead`; discussions can be muted, marked unread from
  a comment, and searched (`GET /comments/:type/:id/search`). Comments reuse
  the Messages composer (`CommentComposer`, spoilers, `#` mentions, `@`
  people), render through the same message text component, and gain the
  same reaction and action pills. Comment reactions and review upvotes get
  their own push switch, off by default, sent once at the threshold.
- **Sagas group the works of a franchise.** A film's TMDB collection, an
  anime's main line (prequels and sequels walked from the viewed work), a
  game's IGDB series (main games only) and a book's Open Library series
  (whole numbered volumes only) resolve to one saga model (`Saga`,
  `GameSaga`, `MediaItem.sagaKey`, `BookItem.seriesKey`), served by
  `GET /media/:type/:id/saga` and `GET /library/sagas`. Work pages show a
  saga block and offer the sequel on finishing; libraries gain a
  Works/Sagas switch listing sagas in progress, waiting on an announcement,
  then finished. The stale-media refresh re-reads tracked sagas, and a newly
  announced sequel raises `SAGA_SEQUEL_ANNOUNCED` (bell, push, email opt-in)
  for those who finished a work of the saga. The saga block and view are
  domain-agnostic components fed by a per-domain mapping.
- **Upcoming releases are tracked precisely.** Movies get regional release
  reminders. IGDB's `date_format` gives each game a release precision (day,
  month, quarter, year, TBD) dated to the start of its period instead of
  December 31st; an unreleased game is held in "À venir", keeps its status,
  ownership and ratings, and offers a release reminder (`NEW_GAME` joins the
  release digest, the calendar, the `.ics` feed and the RSS/Atom release
  feed, which now also lists movies and games). Calendar day tiles count
  releases rather than episodes. A not-yet-aired AniList anime is treated
  like an unreleased film, refused for completion, rating and reviews with
  `library.anime_not_aired`. A Ghost filter (180+ days) separates abandoned
  shows from paused ones, and every domain shares one status icon badge.
- **Every alert is declared once.** `ALERTS` (`packages/shared/src/alerts.ts`)
  lists each alert's channels (bell, web push, email), which ones an account
  can switch and their defaults, stored in `User.alertPrefs`. Settings ›
  Communications and the docs' Notifications page are drawn from it, and
  tests fail on a notification type or mail template it omits. Push is now
  per alert and per device; existing accounts keep their activity pushes on
  while new ones start with them off. New alerts cover security changes
  (second factor, security key, passwordless, recovery codes, lockout), an
  account-deletion confirmation, API key expiry, finished imports, accepted
  invitations and reviews reaching 10 upvotes; administrators receive
  reports, failing jobs, quotas and new accounts by email and push. Digest
  pushes are written in the recipient's language and name each show once;
  the VAPID subject falls back to the instance's HTTPS `WEB_ORIGIN` instead
  of a maintainer address. Séance emails and the admin email gallery were
  reworked alongside.
- **XP is auditable and sized by what was done.** The level card opens a
  day-by-day XP history; revoked entries are stamped `revokedAt` instead of
  deleted and each entry snapshots what earned it. First finishes of games
  and books are now credited and revoked per cycle, which un-finishing used
  to miss. The daily cap is a rolling 24 hours rather than a calendar day.
  Finishes pay by size (book pages, IGDB time to beat, episodes per season,
  seasons per series, half for replays), `SAGA_COMPLETED` and
  `READING_GOAL_REACHED` pay progressively, and new achievements cover sagas,
  pages read, hours played and finishing a work three times. Behaviour
  achievements only read activity dated after account creation, so an
  imported history no longer unlocks them.
- **Moderation covers lists and profiles; audiences are applied
  consistently.** Lists and profiles can be reported, with a "noncompliant
  account" category and new offence and image-rights motifs. Moderators can
  edit or delete a reported list, and on a profile remove the picture, clear
  the bio, rename, suspend (`suspendedUntil`, sign-in refused with the end
  date, sessions and API keys cut) or delete; each measure is its own
  decision row under one notice. `LEGAL_VERSION` is bumped and the terms and
  privacy policy updated. On the privacy side, list activity is gated on the
  list's owner rather than the editor who added the item, a review's rating
  must pass the review's own audience, season and episode reviews resolve to
  their show, profile activity facets gate their own domain's share, and
  only "hide my progression" hides XP. A viewer's disabled domains are
  respected on the profile, lists, reviews and the bell, with a shared
  domain-off mark instead of a dead link. The profile's "En chiffres" card
  and its queries were removed, the streak staying.
- **Account deletion and export were audited.** Deletion now closes the
  account's sessions, clears the notifications it caused for others, strips
  emails, device names and import details from what it leaves, and is shown
  as a step-by-step timeline. The export adds film rewatches, consents,
  photo, activity, progression, sign-in methods, every playthrough and
  reading, sessions, pending changes and items added to others' lists. The
  inactivity scan no longer deletes a dormant account whose warning email
  did not actually go out.
- **Admin tooling gains search, status and detail.** The admin shell has a
  shared search, an attention summary, filter bars and a reworked drawer.
  The jobs page shows running, next and overdue runs from the scheduler
  registry; imports record their selection and outcome (`details`) and list
  in-flight runs; invitations filter by state and text. The reviews page
  gains search, sorting and domain filters, and the report, add-to-list,
  followers and collaborators modals were reworked.
- **AniList episodes are named correctly.** Streaming episode titles are
  matched by the number parsed from their title instead of list position,
  a later cour's absolute numbering is shifted back, a single-season entry
  is named after the entry, and names a franchise copied across entries are
  dropped.
- **Outbound requests are bounded and the audit's findings are resolved.**
  Turnstile, OMDb, Simkl, Steam, Healthchecks, Quackback and Cover Art
  Archive calls share a deadline instead of hanging a worker. Following
  `docs/audit-refacto.md`, date helpers, token generation, pagination
  (`parsePageQuery`/`toPagedResult`), field-length limits, session mappers
  and web-origin handling were unified across API, web and
  `packages/shared`, fixing the copies that had diverged. Production error
  reporting moves to Sentry v11 with safe GlitchTip handling, and the
  report-only CSP allows the feature-flag and feedback hosts.
- **Platform, CI and docs.** The web moves to SvelteKit 3, adapter-node and
  MSW 3; images are published for linux/arm64 as well, with the amd64 push
  no longer blocked by it. CI cuts the web job roughly in half (per-locale
  Paraglide module, Lighthouse moved to the weekly web audit, cached OpenAPI
  document, Trivy scanning the images CI already built). The READMEs' setup
  guides moved to docs.loomkeep.app, which the app now links, counts with
  Umami when configured, and exposes the public API under readable
  `operationId`s. Backloggd is no longer a planned import. Wizard steps
  animate, and routine dependency bumps landed.

## 1.10.0 — Customizable libraries, tracking sessions, and the public API

- **The home page is an account-persisted widget grid.** `User.homeLayout`
  stores a 12-column layout edited from Settings > Appearance, with drag,
  resize, widget settings and mobile stacking. A null layout derives the
  default from enabled domains. Widgets include quick search, favorites,
  lists and saved library views, dormant shows, a daily watchlist pick,
  "on this day", reading goals, notes and social/gamification summaries.
- **Libraries gain display modes and bulk editing.** Table, wall and
  compact layouts share the same filtering and sorting; display mode and
  visible columns persist per device. Rows support inline status,
  ownership and rating actions. Bulk endpoints accept explicit ids or the
  current filters, reuse single-entry lifecycle operations for XP and
  activity consistency, and expose selection controls with keyboard
  navigation and a timed undo before deletion. Named saved views retain
  filters and order, can feed a home widget, and are included in exports.
- **Games and books now keep dated sessions and tracking cycles.** Manual
  sessions and timers record duration, reading progress and notes, with
  editable history and weekly summaries. `GamePlaythrough` and
  `BookReading` separate successive playthroughs and rereads; deliberately
  history-only sessions retain a null cycle relation. Tracking panels and
  resume controls share the resulting session-based presentation.
- **Remaining-work estimates use richer catalogue data.** Episode runtime,
  overview and still are persisted from TMDB; watch-time calculations use
  the episode length, title average, then domain default. Series with no
  provider average use the median of known episode lengths. IGDB completion
  estimates are stored only with at least three submissions, and stale
  tracked games refresh through a six-hourly job. Library pile endpoints
  follow the active filters and count unwatched aired episodes, remaining
  game time, book pages or album duration. Book refreshes backfill ISBNs;
  anime format distinguishes films for migration exports.
- **Discovery and the release calendar became more personal.** Regional
  streaming-provider preferences prioritize selected services without
  hiding other offers, including free and ad-supported tiers. The calendar
  adds a weekly overview, filters, tonight's cards and episode backlog.
  Per-series alert muting leaves calendar entries visible and prevents
  muted episodes from resurfacing in a later digest. Shared or pasted
  catalogue links resolve to a quick-add panel; supported ids are parsed
  without fetching arbitrary URLs, and Loomkeep links resolve within the
  current instance.
- **Social progression and data portability expand.** Achievement rarity
  is snapshotted per tier among eligible active members, with minimum
  population and holder thresholds to avoid identifying individuals.
  `SEASON_FINISHED` records a season's actual completion transition.
  A token-protected, entitlement-checked Atom feed exposes the owner's
  activity. Migration exports produce Letterboxd diary/watchlist CSVs and
  Goodreads-compatible book CSVs, including optional reviews but never
  private notes.
- **A scoped, read-only public API is available under `/api/v1`.** Personal
  keys are hashed at rest and accepted only on explicitly opted-in routes.
  Public DTOs cover libraries and history, calendar, lists, statistics,
  reviews, profile, achievements, notifications and the complete export.
  Account-level request quotas span all keys, return rate-limit headers
  and typed 429 responses, and limit the full export once per hour.
  Key creation/revocation enters the security log; maintenance warns before
  expiry and removes keys unused for a year. Checksummed keys reported by
  signed GitHub secret-scanning alerts are revoked and their owners notified.
  Public API CORS permits bearer-key requests from other origins while
  internal routes retain their existing cookie/origin rules.
- **Operators gain invitations and persistent instance settings.** Closed
  registration accepts address-bound or multi-seat invitations, redeemed
  atomically during account creation and manageable from the admin UI.
  `InstanceSettings` centralizes social, gamification, registration, public
  API availability and quotas; initial values come from the environment,
  whose overrides still take priority and lock the corresponding controls.
  Job failures and recovery notify admins without repeating every failure,
  and the services page covers more instance dependencies. Docker now
  forwards `ADMIN_EMAIL` and instance-setting variables to the API.
  A social-gated public transparency page reports moderation by year.
- **Documentation now has its own site at `docs.loomkeep.app`.** Starlight
  hosts user, import, security, self-hosting and contributor guides; Scalar
  renders the v1-only OpenAPI reference with descriptions, examples,
  response errors and quota headers. Generation rejects undocumented
  public fields. The site includes integration recipes, link validation,
  theme/reduced-motion support and GitHub Pages deployment. Scalar assets
  are served locally with hosted extras disabled, and an rxjs peer
  declaration fixes API startup after the docs joined the workspace.
- **Italian joins the shipped locales across web and API copy.** Language
  pickers derive from the supported-locale list, API copy uses a shared
  fallback rule, and catalogue tests enforce locale and file consistency.
  Settings, site, admin and gamification messages were reorganized and
  duplicate/dead keys removed; Italian covers emails, notifications and
  feeds as well as the interface.
- **Component tests and accessibility fixes harden the web UI.** A Vitest
  happy-dom project exercises components through Testing Library, MSW and
  a reactive navigation fake. It caught spaced-code paste truncation,
  saved-view selection loss, unsafe login redirect prefix checks and a
  tooltip that ignored reduced motion. Shared components improve focus,
  keyboard behavior, semantics and long-content layouts. Library lists
  rank light rows before loading a page, push applicable filtering/sorting
  into SQL, escape literal search wildcards and aggregate media progress.
  Email and Simkl links now use the first configured web origin.
  Newsletter rendering recognizes the new `electric_plug`, `books` and
  `house` aliases in HTML and plain text; dependency and CI updates round
  out the release.

## 1.9.0 — Real-time push, the open-core `ee/` split, and passkeys

- **Real-time push over WebSocket.** A single socket.io `EventsGateway`
  replaces the notification badge's 20s poll and the comment/import polling:
  a per-user room for private pushes (notifications, admin report counts,
  achievement unlocks, import progress, onboarding, follow requests) and
  joined rooms for shared resources (comment threads, collaborative lists),
  gated by the same rules as their REST endpoints. The gateway lives under
  `/api/socket.io` so the `Path=/api` access cookie reaches the handshake,
  runs websocket-only (Fastify never completes engine.io's polling upgrade),
  and the global `JwtAuthGuard`/`ThrottlerGuard` now no-op outside HTTP
  contexts. Session revocation and list-editor removal evict live sockets;
  Prometheus gets `ws_connections_active` and `ws_connection_rejections_total`.
- **The first premium features move to `ee/` under `LICENSE-EE`** (option B
  of ADR 0001). The iCal subscription, the Dock/Board navigation styles and
  the advanced/temporal/social stats now live in `apps/api/src/ee` /
  `apps/web/src/lib/ee`; the core never imports them (ESLint-enforced) and
  ee/ hands itself to the core at startup, so routes and DTOs are unchanged.
  An offline-signed annual `LOOMKEEP_LICENSE_KEY` is required only once
  `premium-features` is on — without it ee/ routes 404 and the web locks
  their screens — and a self-host (`--instance`) key makes every account of
  its instance premium. Contributors now sign a CLA (CLA Assistant) so
  contributions can ship under both licenses.
- **Accounts gained passkeys and an activity history.** WebAuthn joins TOTP
  and email as an MFA method, with optional passwordless sign-in once a key
  is registered. Auth tokens moved to encrypted `HttpOnly` cookies, session
  revocation now takes effect immediately (sid checked against a cached live
  session) instead of after the access token expires, and a new "Account
  activity" settings screen lists every sensitive action of the last year
  with its device and IP (IPs purged after a year and on account deletion).
- **Settings became an index plus one route per section**, driven by a single
  `nav.ts` model with keyword search (⌘K), a shared `SettingRow`, new
  `GET /library/domain-counts` and `GET /import/last-run` endpoints, and
  redirects for legacy `/app/settings#…` anchors.
- **Social and community surfaces grew.** Shared lists are collaborative
  (per-item author, editor notifications with per-list mute, editors picked
  among friends); reviews were redesigned (rating slider, community summary,
  spoiler tags, reporting, and a `REVIEW_REMOVED` moderation measure);
  comment discussions were redesigned and go live; the activity feed filters
  by domain (and now hides disabled/locked domains everywhere); the XP
  leaderboard gains an all-time tab; a gamified onboarding checklist ends on
  the "première séance" achievement.
- **More ways in and out of the library.** MyAnimeList (XML) and Babelio
  (CSV) imports; a manual, language-based Open Library edition selector on
  book pages (`GET /books/:source/:sourceId/editions`); and RSS/Atom feeds
  of recent episodes (`/library/releases.{atom,rss}`) behind the iCal token.
- **Operators got new levers.** A `NEWS_BANNER` Unleash flag drives a live,
  translated maintenance/degraded-service (or custom) banner; admins are
  emailed (and GlitchTip alerted) when OMDb, Simkl, Steam or SMTP reach 80%
  and 100% of their daily quota, with counters now counting every HTTP retry;
  admins can adjust a user's XP by hand (`ADMIN_ADJUSTMENT` ledger entries);
  every registered admin job can actually be run; Postgres metrics and
  `pg_stat_statements` join the observability stack.
- **Mobile audit cleared end to end.** One JS-selected app shell instead of
  two CSS-hidden trees, `viewport-fit=cover`, `svh`/`dvh` full-height
  surfaces, pinned modal action footers, scrollable bottom sheets, an 11px
  floor for running text, per-screen `<title>`s, theme-following status bar,
  no logout on a dropped connection, and a French catalogue settled on "tu"
  and "critique" (spec-enforced).
- **Four internal audit rounds** fixed provider-throttle and XP daily-cap
  races, batched episode sync and XP reconciliation, turned the hourly
  notification scan episode-driven, pushed stats filtering into Postgres,
  re-encode uploaded avatars through sharp (dropping EXIF/GPS), release import
  parse models once committed, require library ownership to record a watch,
  keep the Twitch secret out of query strings, and slim `/api/health`. The
  four library domains now share one entry lifecycle
  (`common/entry-lifecycle.util.ts`), persisted API copy follows the
  recipient's locale (operator-facing strings are English), argument-less
  validation failures return 400 instead of 500, and knip plus type-aware
  promise rules became blocking CI gates. The e2e suite was split per flow
  (MFA, import, social) with a shared bootstrap.

## 1.8.0 — Gamification, complete localization, and security hardening

- **Gamification is now a broad product surface.** An append-only
  XP ledger and materialized score power the shared level curve and the full
  cross-domain reward schedule, with live awards at every supported action
  and a nightly reconciliation job as an integrity backstop. Progress appears
  on profiles with the same privacy controls as activity, and the entire
  module remains gated by `GAMIFICATION_ENABLED` for self-hosters.
- **Achievements span evaluation, presentation, and reward feedback.** The
  code-defined catalogue contains 66 tier entries collapsed into roughly 42
  cards, masks locked secrets server-side, and has its own responsive
  `/app/achievements` screen. Unlocks and level-ups now run through an ordered
  animation queue; users can also explicitly equip up to three non-secret
  achievement tiers on their profile without automatic or privacy-sensitive
  defaults.
- **A new XP leaderboard** ranks the current month or year globally or among
  friends, with shared ranks for ties, a top-100 cut plus the viewer's own row,
  and privacy-aware avatar handling. It is gated by both the social and
  gamification feature switches.
- **French/English coverage is complete across the frontend and transactional
  email.** Translation catalogues were consolidated and audited, PWA startup
  now honors the selected locale, and generated manifest screenshots show
  real product views. The domain search control was also redesigned as
  keyboard-accessible tabs, with studio/franchise search for games and ISBN
  search for books.
- **The generated API contract now covers substantially more of the app.**
  Response DTOs and typed web requests were expanded across auth, config,
  notifications, reviews, social, lists, comments, admin, catalog, and the
  library domains, including typed query strings and convention tests that
  keep the OpenAPI boundary honest.
- **Security-sensitive flows were hardened after an internal audit.** Push
  endpoints are restricted to known providers to close an SSRF path;
  password checks now protect sensitive account and admin actions; login and
  MFA verification avoid timing and recovery-code abuse; sensitive endpoints
  have dedicated throttles; malformed ZIP offsets and oversized imports are
  rejected; backup restores call the runtime Prisma binary correctly; and
  both SvelteKit and Caddy emit additional isolation/security headers.
- **CI and dependency gates were modernized.** Lighthouse, Pa11y, and ZAP run
  against disposable stacks and publish useful results, Codecov combines the
  API unit/E2E and web suites by flag, Vitest moved to v5, vulnerable
  transitive dependencies were constrained, Corepack was pinned, and
  workflow permissions were narrowed.
- Fixed the library-list navigation loop, concurrent list reordering, broken
  cross-origin avatar delivery, reward-context rendering, and health-check
  regressions; reusable component extraction and smaller presentation helpers
  reduced duplication without changing unrelated behavior.

## 1.7.0 — MFA, premium entitlements, and translatable API errors

- **MFA for all users (LK-C17)**: TOTP and email one-time codes, independently
  toggleable in Settings, backed by bcrypt-hashed recovery codes shown once
  on activation. Login gains an intermediate challenge step (method picker
  when both are active). Admin routes 403 with `MFA_REQUIRED` when the
  account has no active method, checked live with no grace period — skipped
  outside `NODE_ENV=production` to avoid dev/staging friction.
- **Premium becomes a real seam, not just a flag.** New `UserEntitlement`/
  `Subscription` tables back `EntitlementService` (self-healing FREE default
  on every read), replacing the never-read `User.entitlements` Json field —
  see [docs/adr/0001-open-core-agpl.md](docs/adr/0001-open-core-agpl.md) for
  the managed-service positioning this unblocks. Gated behind it: iCal export
  and early-access domains (MUSIC/PODCASTS/BOARDGAMES), advanced stats
  (redacted server-side, with a blurred fake-data preview via a new
  `PremiumTeaser` component instead of just visually hiding real numbers),
  daily notification digests (weekly stays free), and a premium nav/dock
  styling option. An admin plan selector (`PATCH /admin/users/:userId/plan`)
  is the only way to grant premium today — no billing wired up yet.
- **API errors are now translatable codes, not raw English strings** — closes
  out a 4-phase migration across every domain (auth/MFA, library/import,
  catalog/providers, then the remaining tail) plus per-field validation
  errors (`class-validator` messages translated and shown under the
  relevant input instead of dumped in one banner). `AppException` +
  `ErrorCode` (`packages/shared/src/error-codes.ts`) replace bare NestJS
  exceptions API-wide; a `no-restricted-syntax` ESLint rule now errors on a
  regression. 5xx bodies never leak internals — the detail stays in logs,
  keyed by `requestId`.
- **Centralized API layer**: `createApiQuery`/`createApiMutation`/
  `createApiInfiniteQuery` (thin TanStack Query wrappers) replace hand-rolled
  `try/catch` + local `error`/`loading` state across nearly the entire web
  app, fixing several real unhandled-rejection bugs uncovered along the way
  (ProfileView's follow/block actions, review casting, stats drill-downs).
  Every paginated list endpoint now shares one `page`/`limit` +
  `PagedResult<T>` contract.
- **Notification digests** replace instant push/email on new episodes with a
  per-channel cadence (weekly free, daily premium) timed at each user's
  local hour.
- **Catalog detail pages enriched and made locale-aware** across all four
  domains: TMDB (native rating, tagline, director/creator, trailer, content
  rating — plus a new `MediaItemTranslation` per-locale cache so a page
  browsed in French no longer shows a stale cached English title), AniList
  (Japanese voice actor pairing with a split cast photo and its own cast
  modal, director, trailer, studios, related titles, tags), Open Library
  (ISBN, series, language-matched edition, first sentence, external links),
  and IGDB (franchise, age ratings, trailer, multiplayer modes).
- **Compliance pass**: traceable CGU acceptance with a re-acceptance prompt
  on version changes (LK-C03), an illicit-content notice procedure (LK-C10),
  an inactive-account retention policy — reminder at 24 months, deletion at
  36 (LK-C06), reporter notifications on report outcome (LK-C07), an
  exhaustive GDPR data export (LK-C11), anonymised security/import audit
  identifiers on account deletion (LK-C04), and several documentation
  corrections (retention durations, age certification, a11y).
- **Backups are now encrypted at rest and shipped offsite**: every dump is
  age-encrypted before touching disk (only the public key lives on the
  instance), plus a VPS-cron script shipping encrypted dumps of every
  datastore to Cloudflare R2.
- Admin dashboards (Grafana/Portainer/GlitchTip/Unleash/Homepage) now sit
  behind a Cloudflare Tunnel + Access (GitHub SSO + MFA) instead of a
  directly-reachable origin fronted by Authelia (kept for self-hosters
  without Cloudflare).
- Proactive rate limiting for IGDB and AniList providers, instead of only
  reacting to a 429 after the fact.
- Motion/transition polish across the app (page transitions, modals,
  loading states) and free-account import quota management.

## 1.6.0 — Trakt & Simkl import, Open Library, and the new landing page

- **Trakt and Simkl join TV Time as import sources.** Trakt started out as an
  API-based import, but Trakt now gates creating an API application behind
  VIP (confirmed live, contradicting the docs) — a dead end for a self-hosted
  app, so it was replaced with a file-based import off Trakt's own account
  data export, reverse-engineered against a real populated export (no docs
  exist for that ZIP shape). Simkl has no such export in its free tier, so it
  goes through a real OAuth authorization-code flow instead — the
  `simkl.com/oauth/authorize` link is now built and followed client-side
  after a same-origin `/api/simkl/connect` redirect turned out to be silently
  swallowed by the browser (SvelteKit's link handling or the PWA service
  worker are the prime suspects). Shared analyze/resolve/commit mechanics
  were extracted into `MediaImportSource` (TvTimeImportSource now only
  supplies CSV parsing), and `GET /import/availability` greys out
  Steam/Trakt/Simkl with an "Indisponible" badge when their required env key
  isn't set, instead of leading into a wizard that can only fail.
- **Movie rewatches, favorites and ratings now import too.** A new
  `MovieReplay` model (mirrors `GameReplay`/`BookReplay`) lets a movie be
  rewatched more than once; `ImportMovie` gains a required `watchedAt: Date |
null` so every parser makes a real decision instead of silently omitting
  it, and `lists-favorites.json`/`ratings-*.json` feed `LibraryEntry.favorite`
  and `Review` from Trakt's export.
- **Reworked Séance landing page shipped to production**, replacing the
  marketing homepage assembled from several `/home-test` prototypes: full
  paraglide i18n (fr/en), auth-aware CTAs, SEO/OG head and Umami event
  tracking matching the page it replaces. Also adds a themed `+error.svelte`
  so unmatched routes stop falling back to SvelteKit's bare default page.
- **Library and search cards uniformized across all four domains** (movies/
  shows, books, games, music): a hover-to-favorite star replaces the
  always-static one, movies get a watched-status label and books a
  reading-progress bar (both previously tracked but never surfaced), and
  search result cards share one top-right +/check affordance. Filters, sort
  and search state now persist across back navigation — first attempted via
  `replaceState` from `$app/navigation`, which patches the raw history entry
  but not SvelteKit's own router state, so `page.url` stayed stale; fixed by
  switching to `goto(url, { replaceState: true, noScroll, keepFocus })`.
- **Provider attribution (LK-P01)**: a new `provider-brands.ts` +
  `ProviderMark.svelte` render official logo marks for TMDB, Open Library,
  OMDb, AniList, IGDB and MusicBrainz, plus a new Settings → "Sources de
  données" section centralizing their full legal notices — TMDB and Open
  Library require a logo/notice wherever their data shows, OMDb's CC BY-NC
  4.0 license requires attribution next to the scores it feeds.
- Added an Unleash feature-flag service (backend + frontend) for gradually
  rolling out future features.
- Adds trackable CTAs to previously link-less transactional emails
  (password-changed, new-device-login, email-changed, welcome) via
  configurable Umami Link short-URLs, and replaces per-send newsletter
  unsubscribe tokens with a stable per-user one plus a public `/unsubscribe`
  page reachable without logging in. Also adds Umami analytics to the app
  itself.
- Docker images now pull pre-built GHCR images instead of building on the
  VPS, and were shrunk further on top of that.
- DSA art. 17: comment takedowns and admin account deletions now persist a
  `ModerationDecision` record and notify the sanctioned user by email (+
  in-app when the account still exists); CGU §9 now only lists the two
  sanctions actually implemented.
- Smaller items: a logout button on the desktop nav rail and mobile menu, a
  hover bookmark shortcut on library poster cards, a beta badge next to the
  app version, the Ko-fi donation modal switched from an iframe embed to a
  plain outbound link (the iframe let Ko-fi deposit cookies before consent),
  and several privacy-policy disclosure updates (Cloudflare/CDN/push/
  donation flows, email tracking, a newsletter-consent timestamp).

- **Books now come from [Open Library](https://openlibrary.org/)** instead of
  the Google Books API. The new `OpenLibraryProvider` uses `/search.json`
  (Solr) for both search and a work's aggregate metadata — page count,
  publishers, subjects, rating — and `/works/{id}.json` for the description
  Solr doesn't carry. Merged works are served as redirect stubs, so details
  follow them and key off the canonical work id rather than the alias that was
  requested. Same-author suggestions now resolve through `author_key` instead
  of matching a display name, and crowd-sourced subjects are filtered down to
  genre-shaped ones (machine tags, sentence-long entries and case variants are
  dropped).
- **No API key, no quota, no branding requirement.** `GOOGLE_BOOKS_API_KEY` is
  gone — one less mandatory key for self-hosters — along with the free tier's
  1,000 requests/day ceiling and the "Powered by Google" marks the Google
  Books API terms required next to every result; plain courtesy attribution
  takes their place. `MUSICBRAINZ_CONTACT` becomes `API_CONTACT`, shared by
  both keyless providers (Open Library and MusicBrainz) for the identifying
  `User-Agent` their usage policies ask for.
- `BookSource.GOOGLE_BOOKS` → `OPEN_LIBRARY`, addressed by work id
  ("OL893414W") rather than a Google volume id. Since `BookItem` is an
  on-demand cache, the migration drops every cached book instead of converting
  it, together with everything referencing one through a polymorphic (FK-less)
  target — book reviews, comments, list items, activity events and the
  moderation rows resolved through them. **Breaking for self-hosters**: book
  library entries do not survive the upgrade (they hang off the cache through
  `BookEntry.bookItemId`); the other domains are untouched.
- **Books lose their adult signal**: Open Library exposes no equivalent of
  Google's `MATURE` maturity rating, so `BookItem.isAdult` is always false for
  now. The column and its per-account gate stay in place for the day a source
  provides one.
- Fixes the seed writing its book call counter under `google_books` while
  `QuotaTrackerService` wrote `googleBooks`, which left the row unlabelled on
  /admin/stats.

## 1.5.0 — Onboarding wizard, collaborative lists & account security

- **Mandatory first-run onboarding wizard**: guides new (and existing,
  pre-feature) users through picking their content domains, appearance and
  notification settings, and a domain-conditional import CTA, gated behind a
  new `User.onboardedAt` field. Introduces a reusable Wizard/Stepper shell
  and a `dismissable` escape hatch on Modal/Drawer for the one-shot,
  non-closable flow. Accounts now start with `enabledDomains=[]` (previously
  all domains enabled by default), set by the wizard itself.
- **Collaborative lists**: adds `ListMember`, a directed grant mirroring
  Follow/Block — the owner can add editors by username, who can add/remove/
  reorder items and edit title/description, but never delete the list,
  change its visibility, or manage members. Reorder takes an optimistic lock
  on `List.updatedAt` so concurrent editors get a 409 instead of clobbering
  each other's order. Ownership transfers to the earliest-added editor if
  the owner deletes their account, editors can leave a list themselves, and
  the invited user gets a notification.
- **Annual reading goal**: users can set a yearly target ("30 books in
  2026") and track progress via a gauge chip beside the `/books` title and a
  card on the home dashboard, computed on read from books finished that
  year (rereads included). `BookEntry.finishedAt` is now synced when a book
  is marked READ, which it previously wasn't — the goal would otherwise
  have silently undercounted.
- **New-device login alerts**: a durable per-user `UserDevice` table
  (independent of `RefreshToken`, which gets pruned) keyed on a normalized
  browser+OS label emails the account and logs a `NEW_DEVICE_LOGIN`
  `SecurityEvent` the first time a device is seen.
- **Breached-password check**: new passwords are checked against Have I
  Been Pwned's range API (k-anonymity, only a 5-char SHA-1 prefix ever
  leaves the server) on register, password reset and password change; fails
  open on any network error so an HIBP outage or offline self-host never
  blocks auth.
- **Account deletion made non-destructive for shared content**:
  `Review.userId`, `Comment.authorId` and `Report.reporterId` are now
  nullable with `onDelete: SetNull` instead of `Cascade`, so deleting an
  account detaches authorship instead of destroying content other users can
  still see or reply to — the UI falls back to a "Utilisateur supprimé"
  placeholder. A new `GET /users/me/deletion-summary` endpoint powers a
  collapsible per-category breakdown (deleted vs. anonymized) in the danger
  zone's confirmation modal.
- Settings/auth UX polish: login/register errors render in a `Banner` with
  clearer French copy, username availability check gets fuller messages and
  a check/cross glyph, email verified/unverified status moves to a small
  icon, and the adult-content toggle is fully hidden (not just disabled)
  when the account isn't eligible.
- Admin: per-domain library stats added to the users section.
- Fixed the Collection grid's delete button being hover-only, leaving touch
  users no way to remove an item — long-press now opens a focus overlay
  (same pattern as comment threads) with a confirmation step.
- Button styles refactored across components for consistency, with a new
  compact modifier.
- Dead-code cleanup: unused shared DTOs, an unused `getMyLists` helper,
  `knip` config, and various imports/constants consolidated across
  components and routes.

## 1.4.0 — Public landing page & app routing overhaul

- **Public landing page** at `/`: a prerendered marketing page (hero, the six
  libraries, feature grid, hosted-instance vs self-hosting) replacing the
  redirect-to-login that used to greet anonymous visitors. Copy lives in
  `messages/{fr,en}.json` under `landing_*`; the only session-dependent part
  is the CTA, which becomes "Ouvrir l'app" once bootstrap resolves.
- **Routes restructured around an `/app` prefix**: every signed-in screen
  moved under `routes/app/` (`/app`, `/app/media/…`, `/app/settings`,
  `/app/admin/…`, `/app/u/:username`). Auth is now enforced by layout
  nesting instead of the hand-maintained `PUBLIC_ROUTES`/`AUTH_ROUTES`
  arrays in the root layout — `app/+layout.svelte` guards the app and owns
  its chrome, `(auth)/+layout.svelte` does the mirror redirect for
  already-signed-in visitors, and `(verification)/` holds the two
  email-verification screens that are reachable in either state. Auth URLs
  (`/login`, `/register`, `/forgot-password`, `/reset-password`,
  `/verify-email`) and `/legal/*` deliberately stay at the root.
- `ssr = false` moved off the root layout onto the three signed-in groups, so
  the landing page and the legal documents are prerendered (indexable, no
  blank frame) while the app itself stays a pure SPA. The one-shot
  session+config bootstrap moved out of the root layout into
  `src/lib/bootstrap.svelte.ts`, which the nested layouts gate on.
- API-emitted web paths (`Notification.url`, activity/report `href`, admin
  digest and preference links in emails), the PWA manifest `start_url` and
  shortcuts, and the service worker's notification-click fallback all carry
  the `/app` prefix now.
- Fixed a pre-existing type error in `SupportSection.svelte` (`recommended`
  was missing from three of the four support tiles, so the union type didn't
  expose it).
- Fixed a deep link being lost on login: the redirect to `/login` now carries
  the original path as `redirectTo`, and a successful login sends the user
  back there instead of always landing on `/app`.

## 1.3.0 — Calendar sync, feedback board & legal pages

- **Calendar subscription (.ics)**: `CalendarSubscribeModal` plus new API
  endpoints to fetch and regenerate a per-user calendar token — subscribe to
  your Loomkeep release calendar from Google/Apple Calendar rather than
  checking the in-app one.
- **Feedback board (Quackback)**: self-hosted Quackback instance
  (feedback.loomkeep.app) wired into the app — `WidgetIdentify.svelte`
  authenticates the visitor via a signed SSO token so the widget knows who's
  asking. Three boards (Feature Requests, Bug Reports, both public;
  Internal Roadmap, team-only for business-sensitive backlog), two roadmaps
  and a changelog feed mirroring this file in user-facing language — see
  "Feedback board (Quackback)" in `CLAUDE.md` for the full setup and
  conventions.
- **Help & Feedback**: a new `/settings` section and home page card
  surfacing the feedback board (suggest an idea, report a bug, view the
  roadmap, see what's new) plus a button to open the support chat directly
  — the always-available entry point now that the floating launcher is
  hidden on mobile (see below).
- **Legal pages**: CGU (terms of service) and privacy policy documents
  added, legal links updated to open in a new tab.
- **Password requirements**: a new `PasswordRequirements` component and
  centralized password-validation module show live strength feedback on
  signup/change forms instead of a plain reject-after-submit.
- `NewBadge` component: a reusable "New" badge (paired with
  `feature-badges.ts`'s time-based `isFeatureNew()`) now used consistently
  across nav items, settings toggles and home cards instead of one-off
  markup per feature.

## 1.2.0 — Multilingual, release changelog & push onboarding

- **Paraglide i18n**: the web app is now translatable — English ships as the
  second locale alongside French, `User.locale` persists the choice across
  devices, and a language picker lives in Réglages → Apparence. Migrated
  component by component (transverse/nav/auth, settings, profile/social,
  admin layout/jobs/schema/services, admin stats) rather than in one pass,
  extracting shared `common_*` keys along the way.
- **Release changelog & newsletter**: a new `ChangelogEntry` model (version/
  title/highlights, admin-authored via `/admin/changelog`) feeds a public
  `/changelog` page — linked from the `Loomkeep vX.Y.Z` footer on `/settings`
  and `/admin` — and an opt-in `notifyNewsletter` email. `MailService` gained
  a `newsletter` template (admin gallery, new `multiline` field support for
  the highlights textarea) and `sendNewsletter()`; sending is a deliberate
  per-entry admin action (`POST /admin/changelog/:id/send`, resendable), never
  automatic on entry creation.
- **Push notification onboarding**: a one-time explanation prompt on app open
  (root layout), gated on `Notification.permission === "default"` plus a
  per-device localStorage flag — the two together distinguish "never asked"
  from "our own intro already shown and answered", since the browser
  permission alone can't tell those apart.
- **Notification bell popover**: `/notifications` is gone, replaced by a
  global bell popover reachable from anywhere (bottom-right on mobile); the
  last `readAt`/`notifyInApp` remnants from the old page were removed.
- **Self-host**: a file-toggled maintenance mode in Caddy, a
  `REGISTRATION_ENABLED` flag to close signups on an instance, and Cloudflare
  Turnstile bot protection on registration (installs without a Turnstile key
  see no widget, and the API no-ops the check the same way).

## 1.1.0 — Profile pictures & profile sharing

- **Profile picture upload**: `User.avatar` (bytes, stored in-row so it rides
  along with `pg_dump` backups rather than needing a separate volume) plus
  `avatarMimeType`/`avatarUpdatedAt`. New endpoints `PATCH`/`DELETE
/users/me/avatar` (size + magic-byte validated) and a public `GET
/users/:id/avatar` (no auth — a plain `<img src>` can't send the JWT kept
  in localStorage; cuids are unguessable enough). The web client resizes to a
  square (canvas, WebP) before upload. `avatarUrl` is now carried on
  `UserDto`/`UserSummaryDto`/`SocialProfileDto`/`ActivityActorDto`/
  `AdminUserDto`; a Figurant (Ghost mode) never exposes their real avatar —
  same anonymization path as their username/pseudonym.
- **Profile sharing**: a "Partager" action (Paramètres → Profil, and on your
  own public profile page) opens the native OS share sheet
  (`navigator.share`) where supported, falling back to a link-copy + QR code
  modal otherwise. A separate always-available "QR code" action covers the
  in-person case (someone scanning your screen) on every device, mobile
  included. The QR encodes the plain profile URL (`/u/username`), so it's
  scannable by any camera, not just the app. New dependency: `qrcode`
  (client-side SVG generation, no external calls).

## 1.0.1 — Rebrand: Tracklore → Loomkeep

The project is renamed to **Loomkeep** (loom — weaving together everything you
watch, read, play and listen to; keep — safekeeping), matching the
`loomkeep.app` domain. No behaviour changes: this release is a rename across
every surface, verified by a full build, the API unit suite and
`svelte-check`.

- **Workspace scope** renamed `@tracklore/*` → `@loomkeep/*` (root package
  `tracklore` → `loomkeep`), touching all three workspaces and every
  cross-package import, plus the `--filter` invocations in both Dockerfiles,
  the CI workflow, `.husky/pre-push` and the `/admin/schema` command hints.
- **Defaults** in `docker-compose.yml`, `docker-compose.prod.yml`,
  both `.env.example` files, and the CI Postgres service: database user/name
  are now `loomkeep`, and `SMTP_FROM` defaults to
  `Loomkeep <noreply@loomkeep.app>`. Safe to change wholesale — no
  production instance exists yet, so there is nothing to migrate. An existing
  local `tracklore-dev-db` container keeps its own name and credentials until
  it is recreated.
- **User-facing strings**: page title, sidebar wordmark, PWA manifest
  `name`/`short_name`, the version line in `/settings` and `/admin`, admin
  push/broadcast placeholders, and all transactional email copy (subjects,
  bodies and the shared mail header/footer).
- **Stored keys and generated filenames**: the localStorage token key
  (`tracklore.tokens` → `loomkeep.tokens` — logs existing sessions out once,
  acceptable pre-launch), the service-worker notification tag, the
  MusicBrainz `User-Agent`, the Swagger title, and the `loomkeep-*` prefixes
  on data exports and database backups.
- **Docs** (`README.md`, `CLAUDE.md`, `SECURITY.md`, `MONETIZATION-IDEAS.md`)
  updated. Historical changelog entries are deliberately left untouched —
  each documents what shipped under the name in use at the time. The GitHub
  and Codecov badge URLs still point at `Logan2234/tracklore` and will be
  updated once the repository itself is renamed.

## 1.0.0 — Stats overhaul: /stats, /profile, /admin/stats & ops pages

The roadmap has now shipped P1 through P4 with the app stable enough to call
finished — this is the first release reserved for that milestone (see
`CLAUDE.md` → Conventions), not a specific commit.

- New personal **`/stats` dashboard** ("La Régie"): a sticky domain/period
  filter console drives a cross-domain overview (totals, completion/abandon
  rates, rating distribution and decade-of-release histograms — both
  clickable through to the underlying works) plus a dedicated deep section
  per domain — Vidéo (total time, movie/series/anime split, longest binge,
  ghost/paused series), Jeux (playtime, never-launched, replays, rating by
  platform/genre), Livres (pages read, top authors by pages, rereads),
  Musique (listen duration — a new `MusicItem.durationMin` field captured
  from MusicBrainz — release-type split, top artists). A "Vidéo — activité
  dans le temps" section adds a GitHub-style calendar heatmap, day-of-week
  and hour-of-day curves, and monthly/yearly totals, since `EpisodeWatch` is
  the only true per-event log in the app today. A **Social** section (gated
  `SOCIAL_ENABLED`) covers reviews/comments/lists written, your rating vs the
  community's on works you both rated, new followers and social activity per
  month, and a contribution streak.
- **`/profile` "Intégré"**: per-domain totals and favorites, a streak badge
  (🎬N, shown wherever a pseudo appears — profile header, reviews, comments —
  never for a Figurant), first/last activity, a stats strip (time watched /
  most active year), top genres, inline social counters, and a mini
  consumption-heatmap teaser linking to the full `/stats` page.
- **`/admin/stats` rebuilt** as "Salle des machines": a KPI strip (accounts,
  active 24h/30j, cached works, DB size, pending reports) atop four dense
  sections — Comptes & engagement (new-account trend, retention cohorts,
  domains-enabled split, profile-privacy split, account health), Catalogue &
  cache (per-domain cache table with growth sparklines, most-popular works
  across the whole instance, cache mutualization/orphans), Social (totals,
  a reviews+comments activity trend, instance-wide rating distribution, top
  contributors, the reports queue's pending/resolved/median-delay/founded
  rate), and Système (DB size per table, API calls per provider, notification
  read rate, push subscriptions, login failures, last backup). The former
  `AdminStatsService`/`AdminStatsDto` — a single endpoint mixing four
  unrelated aggregate blocks for two unrelated pages — is gone, replaced by
  this dedicated page plus a small `AdminOverviewService` serving just the
  handful of counters the `/admin` dashboard and `/admin/communications`
  actually need.
- Every remaining operational admin page (Jobs, Import, Services, Cache,
  Signalements, Sécurité, Communications, Sauvegarde) gained a small
  summary-metrics header: failure rate/avg duration per job, import
  success rate and items-by-source, quota-bound vs unlimited service
  providers, top reporters and founded-rate on the reports queue, login
  failures by 24h/7j/30j, active push subscriptions by browser family, and
  backup regularity (median interval between dumps). Cache's own page
  already exposed staleness/orphans and was left untouched.
- Reviews and comments now show a streak badge next to any non-anonymized
  author's pseudo, computed with a single batched query per request rather
  than one per author.

## 0.8.0 — Reviews: votes & season/episode critiques, admin polish

- Reviews can now be upvoted/downvoted by other users (Reddit-style: one
  active vote per person, re-voting the same direction removes it). The net
  score shows on every review, including your own; voting is a social
  interaction (requires `SOCIAL_ENABLED`) and you can't vote on your own
  review.
- A season or episode can now get its own critique (rating + text + audience,
  votes included) directly from its row in the episodes list — the same
  "Mes reviews" component already used on the 4 work-detail pages, opened in
  a discreet icon-triggered modal (same pattern already used for its comment
  thread).
- Automatic daily backups (admin "Sauvegarde"): dumps are now persisted to
  disk (a dedicated Docker volume in self-host) instead of a one-off ad-hoc
  download, kept as the 7 most recent, listed/downloadable/deletable from the
  admin page, with a scheduled 3h job (also triggerable on demand from
  `/admin/jobs`).
- Admin dashboard redesign ("Poste de contrôle"): a status strip of the 4
  numbers an admin actually checks at a glance (accounts, services, jobs,
  backups), each linking to its page, plus every admin destination grouped by
  concern instead of one flat grid.
- `/admin/emails` and `/admin/push` merged into a single `/admin/communications`
  page (two tabs — preview/test-send a template, test/broadcast a push —
  instead of two separate nav entries for the same admin gesture).
- Home page redesign: a responsive card-grid dashboard (one card per enabled
  domain, always shown — even empty, with its own "voir plus" shortcut — so
  the layout never silently drops a section), a "Bientôt" teaser card for
  Podcasts/Jeux de société once opted into, and the activity feed teaser
  folded into the grid instead of sitting above/below it.
- Fixed: a Figurant's derived pseudonym avatar was seeded on their real,
  constant user id — the same identicon resurfaced on every thread they
  posted in even though the displayed pseudonym differed, defeating the
  point of not being able to correlate their activity across threads. Now
  seeded on the pseudonym itself (caught by a `frontend-design` pass).
- Dev/self-host: `WEB_ORIGIN` now accepts a comma-separated list (e.g. to
  allow both `localhost`).

## 0.7.0 — P4: lists & Figurant mode

- Figurant/ghost mode (P4, increment 6 — closes P4): switching a profile to
  `GHOST` now applies immediately — incoming followers removed, outgoing
  follows/requests toward non-public profiles cancelled, shared lists
  downgraded to Privé, all behind a confirmation modal showing live counts of
  what will change. A Figurant's comments and reviews are shown to others
  under a derived pseudonym ("Figurant n°…", computed at render, nothing
  stored) instead of being hidden — a Figurant can no longer follow a
  non-public profile, share a list, or be `@mentioned`. Moderation
  (`/admin/reports`) still sees the real identity.
- Lists (P4, increment 5): cross-domain `List`/`ListItem` (a work can be a
  movie, game, book or album, mixed freely in one list), two kinds — `RANKED`
  (drag-to-reorder "top 10", numbered) and `COLLECTION` (unordered grid).
  Own explicit visibility (Privé/Amis/Public, defaults to Privé) — like
  reviews, never inherited from the per-domain visibility facets; managing
  your own lists works with social off, only reading someone else's shared
  list is social-gated. An "Ajouter à une liste" button on every work page
  opens a checklist of your lists; a "Listes" carousel appears on profiles
  (own and others', visibility-filtered) linking out to `/lists` to manage
  your own. New activity events for list creation, items added (aggregated)
  and sharing a previously-private list. Drag-and-drop reordering uses a new
  dependency, `svelte-dnd-action`, also adopted for reordering the mobile
  bottom-bar shortcuts in Réglages (previously up/down buttons).
- Fixed: a movie/series' comment thread stayed permanently spoiler-blurred
  even after finishing it — `LibraryEntry.finishedAt` was never set
  automatically (only an explicit field nothing in the UI ever sent). It now
  tracks completion automatically (movies follow the status flip to
  "vu", series/anime follow watch progress reaching the end).
  - Activity feed: episode watches now emit an aggregated `PROGRESS` event,
    and logging a game/book replay emits `REWATCHED` (both were modelled but
    never wired to an emitter).
- Comments no longer dominate a work's detail page: the thread is now
  collapsed behind a toggle button showing the comment count, matching how
  discreet the reviews section is. New comments are rate-limited to one per
  5 seconds per user.

## 0.6.0 — P4: activity feed & comments

- CI pipeline (GitHub Actions): lint/build/test/e2e/Docker build gates,
  Dependabot, CodeQL, Codecov.
- Social follow-ups (P4): private profiles now return a locked identity-only
  preview (with a request/cancel affordance) instead of a 404 — GHOST/blocked
  still 404, and no content leaves the server. A "Mon profil" nav entry opens
  your own profile as others see it. "Mes reviews" rows link to the work and
  support bulk delete / audience changes; the revision history shows each
  version's text. `/notifications` gained a followers/following network view.
- Social notifications: a generic `Notification` model powers in-app alerts for
  new followers, follow requests and approvals (alongside new-episode alerts).
- Reviews on detail pages: rate + write a critique (with audience) and read the
  community's reviews directly on each media/game/book/album page. The default
  review audience is now configurable from "Mes reviews".
- Profile bio is now editable from account settings (it was displayed but had
  no input).
- Activity feed (P4, increment 3): a materialised `ActivityEvent` log records
  library milestones (started/finished/dropped/added/favourited) and reviews
  across all four domains. A home feed at `/feed` (people you follow, binge-
  aggregated), a "Dernières activités" teaser on the home page, and a "Activité
  récente" timeline on each profile (visibility-filtered by the Activité facet).
- Comments (P4, increment 4): threaded discussion (flat + one reply level) on
  any work and, for series/anime, their individual seasons/episodes. Automatic
  spoiler masking compares the viewer's own progress (per-episode for
  season/episode targets, a binary "finished" gate for movies/games/books —
  albums are never masked, nothing to spoil), stacked with a manual tag; the
  reader can reveal a single blurred comment or turn off masking for the whole
  thread. Fixed 6-emote reactions (one per user/comment), @mentions and reply
  notifications reuse the existing `Notification` model and respect blocks.
  Reports are a polymorphic `Report` model (comments today, reviews/profiles
  ready for later) feeding a new `/admin/reports` moderation queue with a
  pending-count badge. The web app's first shared data-fetching layer,
  `@tanstack/svelte-query`, drives the thread (5s poll on the open thread only,
  paused when the tab is backgrounded).

## 0.5.0 — P4: social foundation & reviews

All of it is gated behind the runtime `SOCIAL_ENABLED` flag (off by default; a
single Docker image serves both modes), exposed to the web via `GET /api/config`.

- **Foundation**: `User.bio`/`profileAccess` (PUBLIC/PRIVATE/GHOST) and a
  `VisibilitySetting` matrix (per-domain × facet audience).
- **Social graph & profiles**: `Follow` (with private-profile approval) + `Block`,
  a central `VisibilityService`, public profiles at `/u/:username`, member search,
  and the account "Confidentialité" screen. Reusable identicon `Avatar`.
- **Reviews**: a `Review` model (mandatory /10 rating + optional text + audience)
  with edit history is now the single source of truth for ratings — the `rating`
  column was removed from every entry and projected from `Review` instead
  (rating your own items still works with social off). A "Mes reviews" management
  screen (`/reviews`) lists, edits and deletes them.
- Deferred within P4: social notifications (needs a generic `Notification` model),
  bio-editing UI, and the activity feed / comments / lists / Figurant increments.

## 0.4.0 — P3: games, books & music

- Games module (IGDB catalogue): library, statuses, playtime, Steam import,
  community ratings.
- Books module (Google Books catalogue): library, reading progress,
  StoryGraph/Goodreads import.
- Music module (MusicBrainz catalogue): library, listen status.
- Unified global search across all domains; `enabledDomains` enforced
  server-side on search/stats, and filters notifications.
- Unified async import framework — all sources (TV Time, StoryGraph,
  Goodreads, Steam) on one job engine with manual match-correction.

## 0.1.0 — P1 MVP, P1.5 TV Time import, P2 notifications

- P1: auth, search, tracking, episode progress, PWA, Docker self-host.
- P1.5: interactive TV Time import (analyze → review collection by
  collection → commit), matched through TVDB IDs, source-agnostic pipeline.
- P2: in-app notifications, periodic scan of tracked shows, Web Push,
  transactional/notification email via SMTP, PWA mobile access.
  Native (Capacitor) wrapper still outstanding.