CHANGELOG.md
Rama main · réplica de lectura del repositorio original.
# 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.