# 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`). 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-` 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 `
`, 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 (``, 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 ``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.