Skip to content

Release v1.6.0 — Weekly Roundup, Library Seed, persistence hardening - #106

Merged
retardgerman merged 107 commits into
mainfrom
dev
Sep 2, 2026
Merged

Release v1.6.0 — Weekly Roundup, Library Seed, persistence hardening#106
retardgerman merged 107 commits into
mainfrom
dev

Conversation

@retardgerman

@retardgerman retardgerman commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Brings v1.6.0 from dev to main. Minor rather than patch: this release adds three user-facing features and replaces the EMBED_SHOW_OVERVIEW config key, which is outside what a patch bump covers under the SemVer commitment in CHANGELOG.md.

Weekly Roundup

  • Scheduled Discord post summarizing new Jellyfin content from the past 7 days. Channel, weekday, hour, and embed color are configurable in the dashboard.
  • Items are grouped by library, and episodes of the same series collapse into one line (e.g. "My Show — Seasons 1 & 2 (12 episodes)"). Titles link back to Jellyfin.
  • Optional role mention on post, picked from a dashboard dropdown. The test button never pings.
  • Idempotent across restarts: an hourly scheduler tick plus a post timestamp in config/dedup-roundup-state.json guarantee one post per week, even if the container restarts on the trigger day.
  • Sonarr/Radarr quality upgrades are filtered out via a stable-identity first-seen map, so re-imported files do not show up as new content.
  • The embed discloses its own limits, with separate footer notes for unresolved library names, sections trimmed to Discord's 25-field cap, and windows cut short by the per-library item cap.

Library Seed

  • On first boot, Anchorr scans the entire Jellyfin library and records what already exists, so pre-existing content never triggers a "new item" notification.
  • A daily prune scan removes records for items deleted from Jellyfin.
  • New "Re-Seed Library" button in the dashboard to re-run the scan manually.

Other changes

  • The embed overview toggle is split into separate settings for movies/series and episodes. The old EMBED_SHOW_OVERVIEW value migrates automatically.
  • Jellyseerr v3.3.0+ compatibility: user sync handles the discordIds array alongside the legacy discordId string.
  • Quota errors from Jellyseerr now surface the specific reason instead of a generic failure message.

Config and persistence hardening

  • Config writes are atomic (tmp + rename), and updateConfig refuses to write when an existing config is unreadable instead of silently replacing it with a partial one.
  • PersistentMap reports whether state actually reached disk. A failed flush no longer lets the seeder mark the library as seeded, which would otherwise skip the seed on the next boot and re-announce the whole library.
  • A read error at load blocks flushing, so an unreadable dedup file is never overwritten with empty in-memory state.
  • Temp files are chmod'ed before the rename, so a leftover .tmp cannot carry loose permissions onto the real file.

Dependency and security updates

Review status

Three review rounds ran against this branch. The first surfaced a critical first-run regression: updateConfig refused to create config.json when none existed, so fresh installs never persisted JWT_SECRET or WEBHOOK_SECRET. That invalidated sessions on restart and broke the webhook secret users had already copied into Jellyfin. That finding and the silent-failure findings alongside it are fixed in ac5115c, 07feee8, 51b0256 and 35773a5. The observability follow-ups tracked in #127 are done.

Known and deferred to #126: buildIdentityKey() uses an item's own TMDB id as the series identity in its Season/Episode branches. Episode dedup is unaffected in practice, since episode TMDB ids are unique and the key stays stable even though its meaning is wrong. Only season-granularity suppression is affected. The fix changes the key format and needs a startup migration, so it is scheduled for 1.6.1 rather than landed right before a release.

Not verified: no live Jellyfin or Discord run, and this project has no automated test suite.

AI was used for the written material in this release only: this description, the changelog entries, and the user-facing strings. The code was written and reviewed by hand, line by line. Each change traces back to a concrete cause, went through three review rounds, and was reworked where the review found something. The deferred item in #126 is listed above rather than quietly shipped for the same reason.

- Add Weekly Roundup config section to dashboard (enable toggle,
  channel select, weekday, hour, embed color)
- Wire WEEKLY_ROUNDUP_CHANNEL_ID into Discord channel loader
- Bump package.json to 1.5.4 and add CHANGELOG entry
- Add utils/i18n.js: minimal Node-side loader that reads
  locales/<LANGUAGE>.json with fallback to en.json, exports t(key, vars)
  with dot-notation lookup and {placeholder} interpolation
- Add roundup.* namespace to en/de/sv/template locales
- Replace all user-visible strings in bot/weeklyRoundup.js with t() calls
  (embed title, season/episode labels, footer, fallbacks)
- Logger messages remain in English by convention
Must-fix from code-review + silent-failure-hunter:
- Scheduler: now.getHours() === targetHour (was <, caused re-posts
  throughout the target hour+)
- Failure counter scoped per-week with weekKey, resets automatically
  on entering a new week (was process-global, permanently died after
  3 lifetime fails)
- fetchRecentlyAdded throws on error instead of returning []; roundup
  and poller wrap it properly so a Jellyfin outage no longer looks
  like a quiet week
- Rename Jellyfin filter param: MinDateLastSaved -> MinDateCreated
  (LastSaved changes on metadata refresh, wrong semantics)
- escapeMd now also escapes * _ ~ ` to prevent titles like *Batman*
  from breaking bold formatting inside the link label
- sendWeeklyRoundup: channel-fetch failure now logs a warn, no more
  silent return
- runTick wrapper catches rejected promises from setTimeout/setInterval
- markPosted: on persistence failure, do NOT set process.env so the
  next tick retries (was in-memory-only success)
- resolveLibraryNames: propagates errors to caller instead of silently
  returning {} (caller now fails the tick and bumps failure count)
- parseIntInRange for WEEKDAY/HOUR rejects NaN/out-of-range values
- i18n: whitelist LANGUAGE env var against ^[a-zA-Z]{2,3}([_-]...)?$
  so a malicious or typoed value cannot point fs.readFileSync at an
  arbitrary path
- formatDate: normalize LANGUAGE to primary BCP-47 subtag before
  passing to Intl

Also: buildJellyfinUrl: log fallback at error level so malformed
JELLYFIN_BASE_URL is loud in ops logs.
If the Discord post succeeded but writing lastPostedAt to config failed,
the previous logic bumped the weekly failure counter. After 3 of those
the whole week was skipped despite users having already seen the post.

Set the in-memory env var unconditionally so the same process won't
re-post, and log the persistence failure without touching the counter.
- resolveLibraryNames: catch fetchLibraries errors and fall back to the
  generic library label instead of letting a names-only blip nuke the
  whole weekly post
- formatDate: log the Intl failure before falling back to ISO date
- buildJellyfinUrl: ensure the concat fallback is a syntactically valid
  URL (prefix with http://invalid.local/ sentinel when JELLYFIN_BASE_URL
  lacks a scheme) so downstream ButtonBuilder.setURL surfaces the
  misconfig clearly rather than with an opaque validation error
The weekly_roundup_* and weekday_* data-i18n keys were present in the
HTML but never added to the locale files. Users on non-English locales
saw raw keys like 'config.weekly_roundup_title' instead of translated
labels. Added all keys to en, de, sv, and template.
Mirrors the existing 'Test Random Pick' pattern — posts the weekly
roundup immediately without waiting for the scheduled weekday/hour.

sendWeeklyRoundup gains an options.test flag. In test mode it rethrows
errors (so the HTTP handler can surface them) and skips all state
side effects (lastPostedAt, failure counter) so a test run never
masks or replaces the real scheduled post.
Log the raw Jellyfin item count and the post-library-filter count on
every fetch so the actual filtering stage is visible.

In test mode, distinguish three failure cases when nothing would be
posted:
- no notification libraries configured at all
- Jellyfin returned items but the library filter dropped all of them
- Jellyfin returned nothing in the 7-day window

The scheduled path is unchanged — a library mismatch is still a normal
quiet week for that user, not a failure to count.
Jellyfin libraries have a CollectionId (referenced by Item.ParentId /
AncestorIds) and a separate VirtualFolderItemId (stored in
JELLYFIN_NOTIFICATION_LIBRARIES via the dashboard). Comparing AncestorIds
directly against the config keys never matched, so the roundup filtered
every item out even when Jellyfin returned 200.

fetchWindowItems now calls fetchLibraryMap() and runs every candidate ID
through resolveConfigLibraryId() before checking membership — the same
translation the webhook flow already does. The resolved id is stashed
on each item as _configLibraryId so groupItems can reuse it without
redoing the lookup.
When Jellyfin returns items but every one is filtered out, dump the
configured library ids, the known Jellyfin library ids in both forms,
and the first item's ParentId/AncestorIds (raw + translated) so we can
see exactly where the comparison diverges.
…ering

The previous approach fetched the 200 most recent Jellyfin items globally
and then filtered by AncestorIds. That breaks when items live in libraries
that /Library/VirtualFolders doesn't return (BoxSets, Collections,
unmapped folders) — every item gets dropped even though they're really
inside a configured library.

Now we issue one /Items query per configured library id with
ParentId + Recursive, which delegates membership resolution to Jellyfin
itself. No more two-form-id dance, no more ancestor walks.

Also defensively skip non-hex-32 entries in
JELLYFIN_NOTIFICATION_LIBRARIES — observed an 'on' string leaking in
from somewhere upstream, will need a separate fix for the source.
Sonarr quality upgrades delete and re-import the same episode file,
which Jellyfin can register as multiple new items with the same
SeriesId + ParentIndexNumber + IndexNumber within the same week. The
previous counter would then report '3 new episodes' for what was
actually the same episode imported three times.

seasons is now Map<seasonNum, Set<episodeKey>>, where episodeKey is
'e{IndexNumber}' for normal episodes and falls back to the Jellyfin
item id for unnumbered specials (which keeps per-item dedup but lets
specials still appear).
Episode dedup key now prefers, in order: IndexNumber + IndexNumberEnd
(2-parters), IndexNumber alone, lowercased Name (Sonarr re-imports keep
the title), then item id as last resort. The previous version fell
straight to item id when IndexNumber was missing, which made every
re-import look like a different episode.

Also log the raw identity fields for the first 30 episodes per run so
we can see exactly what Jellyfin returns and confirm where dedup
breaks if a case still slips through.
- Corrupt WEEKLY_ROUNDUP_LAST_POSTED_AT now warns and skips the tick
  instead of silently falling through and re-posting.
- Drop the redundant outer try/catch in runTick — the setInterval
  wrapper already catches and logs, and the inner try only masked
  design intent (no path inside throws synchronously today).
- Empty-week branch logs at warn (with a diagnostic) when no items
  match because of misconfig (no notification libraries, or N items
  all filtered out by library mismatch). A genuinely quiet week
  still logs at info.
- Preflight JELLYFIN_BASE_URL in sendWeeklyRoundup so a malformed value
  no longer makes it into the embed as http://invalid.local sentinels.
  Logs error, bumps the failure counter, aborts the post.
- resolveLibraryNames now returns { map, failed }; embed footer notes
  "library names unavailable" when fallback was hit and there is more
  than one library section, so Discord viewers see why headers look
  generic.
- Embed field truncation builds entry-by-entry up to the 1024-char
  budget instead of byte-slicing the joined string. Adds a translated
  "… and N more" line when entries had to be dropped, so a markdown
  link is never cut in half.
- Hourly tick uses now.getHours() >= targetHour. If the bot was down
  or the tick drifted past the boundary, the digest catches up later
  the same day rather than skipping the entire week. The 6-day
  idempotency guard prevents duplicates on re-tick.
- Episode raw-fields diagnostic dump downgraded from info to debug —
  no longer noise during normal operation.
- utils/i18n.js: resolve LOCALES_DIR relative to the module via
  fileURLToPath so the loader works regardless of cwd. Drop the
  existsSync/readFileSync race window (single readFileSync, treat
  ENOENT as "missing locale", log other errors).
- /api/test-weekly-roundup error response: coerce error.message to
  string and slice to 500 chars so a future axios/discord upgrade
  can't leak unbounded data into the dashboard.

Locale keys added: roundup.library_names_unavailable, roundup.field_more
in en, de, sv, template.
…-seen map

Roundup queried Jellyfin /Items?MinDateCreated, which surfaces re-imported
files (quality upgrades) as if brand-new — fresh ItemId AND fresh DateCreated.
Now records each item under a stable key (TMDB for movies/series,
SeriesId+S/E for episodes/seasons) the first time it's seen, and filters out
items whose firstSeenAt is older than the 7-day window.
…ponse

- weeklyRoundup: JELLYFIN_BASE_URL preflight now also enforces http(s)
  scheme, matching the SSRF guard used by the config-test routes
- app.js /api/test-weekly-roundup: strip URL-shaped substrings from the
  error message before returning, so axios-embedded URLs (which can
  carry api_key query params) never leak into the dashboard response
## Summary

Adds an optional **Weekly Roundup** that posts a weekly digest of new Jellyfin content to a Discord channel. Disabled by default; configurable via the dashboard.

## Changes

- **`bot/weeklyRoundup.js`** (new): hourly scheduler tick, 7-day rolling window fetch from Jellyfin, per-library grouping, series/season/episode collapsing (e.g. _"My Show — Seasons 1 & 2 (12 episodes)"_), embed builder with Jellyfin deeplinks, 3-strike back-off on consecutive failures
- **`bot/botManager.js`**: wires `scheduleWeeklyRoundup(client)` into the `clientReady` handler next to the daily random pick
- **Config** (`lib/config.js`, `utils/validation.js`): adds `WEEKLY_ROUNDUP_{ENABLED,CHANNEL_ID,WEEKDAY,HOUR,EMBED_COLOR,LAST_POSTED_AT}` keys and Joi validators
- **Dashboard** (`web/index.html`, `web/script.js`): new "Weekly Roundup" section with enable toggle, channel select, weekday dropdown, hour input, embed color; `WEEKLY_ROUNDUP_CHANNEL_ID` is populated via the existing Discord channel loader
- **`utils/i18n.js`** (new): minimal server-side i18n loader. Reads `locales/<LANGUAGE>.json` with fallback to `en.json`; exports `t(key, vars)` with dot-notation lookup and `{placeholder}` interpolation
- **Locales** (`locales/{en,de,sv,template}.json`): new `roundup.*` namespace covering embed title, season/episode labels, footer, and fallbacks — all user-visible strings in the roundup go through `t()`
- **Helpers**: `utils/jellyfinUrl.js` extracted from `jellyfinWebhook.js` so the roundup can build Jellyfin deeplinks without duplicating logic; `api/jellyfin.js::fetchRecentlyAdded` gained an optional `minDateCreated` param (maps to Jellyfin's `MinDateLastSaved`) to support the rolling 7-day window

## Idempotency

The scheduler ticks every hour and gates on a persisted `WEEKLY_ROUNDUP_LAST_POSTED_AT` timestamp, so:

- Docker restarts don't cause duplicate posts within the same week
- The hour/weekday gate is checked on every tick — a missed window (e.g. container down at the exact hour) posts on the next matching hour
- `ALREADY_POSTED_MIN_AGE_MS` is 6 days (not 7) to tolerate small scheduler drift

## Version

Bumps `package.json` to `1.5.4` and adds a `CHANGELOG.md` entry.

> AI-assisted documentation. Code logic manually verified.
Jellyfin's /Items endpoint silently ignores MinDateCreated, so passing
it returned the most recent N items regardless of the requested cutoff.
Switch to the supported MinDateLastSaved param and add StartIndex-based
pagination with an early-break once results fall below the cutoff.
Three fixes that together stop bloated digests:

- installedAt floor: persists the bot's first-start timestamp to
  config/dedup-roundup-state.json. Items with DateCreated older than
  installedAt are dropped — the first roundup after install/upgrade no
  longer pulls in the back-catalogue.
- Client-side DateCreated cutoff: MinDateLastSaved is a superset of
  recently added (advances on metadata refresh too), so enforce the
  7-day window in code before grouping.
- TZ-aware scheduler: now.getHours() returned UTC inside default Docker
  images. New optional WEEKLY_ROUNDUP_TZ env var pins weekday/hour to a
  specific timezone via Intl.DateTimeFormat; absent, falls back to host
  time (which already respects the TZ env var).
…s, silent caps

- fetchRecentlyAdded: per-page try/catch so a transient error on page N
  returns the items already collected from pages <N instead of throwing
  the whole call away. Tag every break path with a stopReason for
  diagnosis.
- maxTotal cap is no longer silent: warn when hit so an operator knows
  older items in the window were truncated.
- weeklyRoundup filter: items with missing/unparseable DateCreated are
  now dropped explicitly (with their own counter) instead of slipping
  past both the installedAt floor and the 7-day cutoff. Filter summary
  always logs at debug level.
- WEEKLY_ROUNDUP_TZ validation runs once at scheduler start with a loud
  warn; per-tick fallback drops to debug to avoid hourly log spam.
- roundupState: log on every installedAt stamp (first-ever or restamp
  after PersistentMap rejected a corrupt value).
- fetchRecentlyAdded: add isAxiosError guard and response shape
  validation, consistent with fetchAllLibraryItems
- pruneLibrary: set/clear scanInProgress via new setScanInProgress()
  export so a re-seed triggered during a prune run is also blocked
Previously all 403s showed a generic or auth-focused message. Jellyseerr
returns { message: "Series Quota exceeded." } in the response body —
this is now detected and surfaced to the user directly.

Extracts getSeerrErrorMessage() helper used in all three request catch
blocks (handleSearchOrRequest, button handler, daily pick handler) so
quota detection is consistent and not duplicated.
@retardgerman
retardgerman marked this pull request as ready for review July 7, 2026 14:03
Resolve dev/main merge conflict (package-lock version)
- libraryPruner: run once shortly after boot, not only on a 24h
  setInterval, so frequent restarts (e.g. config saves) don't skip
  every prune cycle and let seeded dedup keys expire
- seen-items TTL raised to match roundup-first-seen's ~5yr backstop;
  actual removal for deleted items still happens via the daily prune
  scan, not TTL expiry
- roundupScheduler: rebind to the new Discord client on bot restart
  instead of ignoring the second start() call, matching the existing
  daily-pick scheduler pattern
- configFile: migrate legacy EMBED_SHOW_OVERVIEW to the new
  _MOVIES/_EPISODES split so users who disabled it (often to avoid
  episode spoilers) don't get overviews silently re-enabled
- interactions: move the Seerr request error messages (including the
  new quota message) through locales/ instead of hardcoded English
- weeklyRoundup: log the underlying parse error instead of an empty
  catch block in the JELLYFIN_BASE_URL preflight
- roundupScheduler: clearTimeout alone doesn't stop a chain whose tick
  already fired and is in flight when start() is called again on
  restart — its .finally(scheduleNext) would still re-arm using the
  stale client/closure. Add a generation token so a superseded chain
  becomes a no-op instead of quietly ticking against the destroyed
  client.
- i18n: t() now falls back to the English value for a key missing in
  the active locale (instead of leaking the raw key string into
  Discord messages), for locale files that exist but lag behind on
  newer keys.
- locales/fr.json: add the seerr_request_errors block directly too,
  consistent with de/sv/template.
- CHANGELOG: fix the 1.5.6 EMBED_SHOW_OVERVIEW entry, which told users
  to manually re-configure the setting — it's now migrated
  automatically.
- roundupScheduler: add stop() (bumps generation, clears the pending
  timer) and call it from both discordClient.destroy() sites
  (routes/botRoutes.js stop-bot handler, app.js config-triggered
  restart). Without this, stopping the bot (not restarting it) left
  the scheduler ticking against a destroyed client until the failure
  circuit opened for the week — the same failure mode the previous
  round's restart fix addressed, just for a different trigger.
- locales/fr.json: drop the seerr_request_errors block added last
  round — it was the English source text copy-pasted in, not an
  actual French translation, and it silently suppressed the new
  per-key en fallback warning. Leaving the keys absent lets the
  fallback (and its logger.warn) do its job until someone translates
  them for real.
- utils/i18n.js: minor cleanup — reuse the already-loaded en.json
  fallback instead of reading it from disk twice, and log an error
  if en.json itself fails to load while another locale is active
  (previously a silent no-fallback-left edge case).
npm audit --audit-level=high started failing on the fix branch with
two newly-published high-severity advisories, unrelated to the review
fixes in this PR:

- body-parser <1.20.6 (GHSA-v422-hmwv-36x6): DoS via invalid limit
  value silently disabling size enforcement. body-parser is a
  transitive dep of express, not a direct one, so pinned via
  overrides like the existing undici/follow-redirects entries.
- axios 1.0.0-1.17.0: several newly-disclosed advisories (prototype
  pollution, DoS via recursion, maxBodyLength bypasses). Resolved by
  `npm audit fix`, which bumped the resolved version to 1.19.0 — still
  satisfies the existing `^1.17.0` range in package.json, so no
  version constraint change needed there.

`npm audit --audit-level=high` now reports 0 vulnerabilities.
- renderFieldGroup: truncate oversized entries and never flush an empty
  field value, which Discord rejects with a 400 and which would burn the
  scheduler's weekly failure budget
- librarySeeder: build episode series/season keys from SeriesId, not from
  the episode's own ProviderIds.Tmdb, which never matched a real Series or
  Season key
- libraryPruner: stop pruning "id:" keys — they can originate from item
  types fetchAllLibraryItems does not enumerate, so the daily scan would
  drop them and the next poll would re-notify
- app.js: clear the initial prune timeout on SIGTERM/SIGINT
- i18n: warn once per missing key instead of on every t() call
- weeklyRoundup: return an explicit stats object instead of array expandos,
  rename the `t` shadow in groupItems, escape "|", drop the episode debug dump
- de/sv: add missing reseed_library keys
- CHANGELOG: correct release date, drop the stale
  WEEKLY_ROUNDUP_LAST_POSTED_AT description, add role mention and the
  undici/axios/body-parser/joi security bumps
Critical, from the code review:
- updateConfig() refused to write when config.json did not exist yet, not
  just when it was corrupt. On a fresh install that stopped JWT_SECRET and
  WEBHOOK_SECRET from ever being persisted: both stayed memory-only, were
  regenerated on restart, and the X-Webhook-Secret the user had already
  copied into Jellyfin no longer matched. Now only an existing-but-unreadable
  config blocks the write, and auth.js/secrets.js expose the generated secret
  via process.env on the failure path too.

From the silent-failure audit:
- PersistentMap.flush() returns whether state reached disk. librarySeeder
  throws instead of setting LIBRARY_SEEDED=true on a failed flush, which
  otherwise skipped the seed on the next boot and re-announced the whole
  library. libraryPruner reports the same condition.
- A read error in _load() now blocks flushing, so the unread file is no
  longer overwritten with empty in-memory state on the first set(). The
  existing comment claimed this invariant; nothing enforced it.
- Persistent flush failures escalate to error instead of fading to debug.
- fetchRecentlyAdded() returns { items, complete } like fetchAllLibraryItems.
  fetchWindowItems throws on an incomplete fetch rather than posting a
  truncated roundup or reporting "no new items" when the API errored.

Cleanups: drop the unreachable onError branches in weeklyRoundup, remove the
dead HOUR_MS constant, correct fr.json completion metadata, drop async from a
handler with no await.

Version bumped to 1.6.0 (minor, not patch): this release adds the Weekly
Roundup, library seed and prune features and replaces the EMBED_SHOW_OVERVIEW
config key.
From the re-run review agents.

- fetchRecentlyAdded() reports `truncated` separately from `complete`.
  Hitting the per-library item cap previously returned success-shaped, so a
  bulk import produced a digest missing its oldest entries with only a warn
  in the log. Routing it through the existing `!complete` throw would be
  worse — a retry truncates identically, so the week would fail three ticks
  and open the circuit — hence a distinct flag, an error log, and a footer
  note in the embed.
- resolveLibraryNames() no longer reports success when it resolved nothing.
  A CollectionId/ItemId form mismatch made every section header fall back to
  the generic label with no log line and no footer note.
- writeConfig() and PersistentMap.flush() chmod the temp file before the
  rename. `mode` in writeFileSync only applies on creation, so a leftover or
  pre-created .tmp kept its old mode and carried it onto the real file.
- msUntilNextHour() floors at 1s so a clock step cannot produce a tight tick
  loop.
- fetchAllLibraryItems() requests ProductionYear explicitly; buildIdentityKey
  uses it for TMDB-less movies and the pruner must rebuild the same key.

Adds roundup.items_truncated to en/de/sv/template.
…locales

- Circuit breaker warns once on the transition into open instead of only
  logging skip:circuit-open at info, so a week-long stop is visible.
- parseIntInRange reports a rejected WEEKLY_ROUNDUP_WEEKDAY / _HOUR value
  instead of silently substituting the default. Named only from start() so
  the hourly tick log stays quiet.
- Embed field trimming now annotates the footer, matching how the existing
  library-names fallback already discloses itself.
- t() logs an error when a key is missing in the active locale AND in
  English — that path renders the raw key to users and was silent.
- /seed/reset checks its preconditions synchronously and answers 409 with
  the real reason rather than confirming a scan that never starts. Extracted
  as checkSeedPreconditions() so seedLibrary() and the route agree.
- The roundup "why was nothing posted" diagnostics were hardcoded English
  but reach the dashboard through the test route; moved to locales/ per the
  project i18n rule.

Adds roundup.sections_trimmed and five roundup.diag_* keys to
en/de/sv/template.

Closes #127
@retardgerman retardgerman changed the title Release v1.5.6: Weekly Roundup & Library Seed Release v1.6.0: Weekly Roundup & Library Seed Aug 11, 2026
retardgerman and others added 2 commits August 11, 2026 12:32
From the final review pass.

The "no items" diagnostic derived its count from `beforeFilter - items.length`,
which lumps all four drop reasons together, then asserted the cause was
"seen before (Sonarr/Radarr upgrade or older import)". On the first roundup
after install every item is dropped by the installedAt floor, so users were
told their dedup store had suppressed content that had simply been added
before Anchorr existed — and the test route threw that same wrong string to
the dashboard. The diagnostic now branches on the specific counters and has
dedicated messages for the pre-install and missing-DateCreated cases.

Also:
- sendWeeklyRoundup returns whether it posted; the scheduler only stamps
  lastPostedAt when it did, so a misconfigured week no longer blocks retries
  for six days after the user fixes it.
- resolveLibraryNames no longer reports failed:true for an empty id list.
- stop() resets circuitOpenWarned so the warning re-fires after a restart.
- fetchRecentlyAdded requests ProductionYear, matching fetchAllLibraryItems,
  and its JSDoc documents the complete/truncated distinction.

Adds roundup.diag_all_pre_install and roundup.diag_no_date_created to
en/de/sv/template.
@retardgerman retardgerman changed the title Release v1.6.0: Weekly Roundup & Library Seed Release v1.6.0 — Weekly Roundup, Library Seed, persistence hardening Aug 25, 2026
Adds the config/state persistence hardening, the roundup limit
disclosure, and the known season-dedup-key issue (#126), which were
implemented but never listed. Reorders the Added section by impact.
@retardgerman
retardgerman merged commit beff0dc into main Sep 2, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants