- Start Web UI before the blocking initial sync so the dashboard is
reachable during the (rate-limited, potentially multi-minute) first
sync; fold the immediate sync into startPeriodicSync's overlap guard
so it can never race a concurrent tick over the shared DB / MB client.
- Make MarkNotificationSent idempotent: INSERT OR IGNORE for same-second
PK collisions, and explicitly swallow FK violations when a release was
pruned by a concurrent re-sync. Prevents a single vanished/duplicate
release from aborting the digest mark-sent loop and re-sending.
- Do not abort NotifyOnce's mark-sent loop on a single failure; log and
continue so every release in the batch is marked.
- NULL-safe reads: COALESCE(type,''), COALESCE(release_date,'') in the
external_releases and unnotified readers to match the cache reader.
- Update/extend tests for the new idempotency and startup contracts.
- Honor ignore_singles/ignore_compilations at scanner read time so toggles
take effect immediately on the dashboard, artist page, and digest instead
of waiting for the MusicBrainz cache to expire and prune rows.
- Run notifier notify synchronously in the scheduler loop to avoid overlapping
read-send-mark runs double-sending the digest.
- Show artist name (with ID fallback) on the archive page instead of raw IDs.
- Select last_synced in GetAllArtistSettings for contract consistency.
- Fix stale startPeriodicSync comment and remove redundant error var.
- Remove dead ignored-branch from the artist template (never rendered).
- Add tests: CSRF sameOrigin, ArtistCacheFresh, secondary_types round-trip,
and scanner type-toggle filtering.
- Update Specification.md schema/config to reflect mbid, last_synced,
secondary_types, sync.interval, and server.public_url.
Task 8's ignore/restore/ignore-singles actions live on the artist detail
page, but the dashboard rendered artist names as plain text with no link,
making those actions unreachable through normal UI navigation. Wrap the
name in an anchor to /artist/{id} and assert the link in the dashboard test.
- Preserve MBID and last_synced across periodic artist syncs (INSERT OR
REPLACE was wiping them, forcing MusicBrainz re-resolution every cycle)
- Exclude unmonitored artists from unnotified-release digest query
- Require telegram.cron_schedule when telegram.enabled is true
- Add same-origin CSRF defense to state-changing web POST routes
- Skip WAL/busy_timeout pragmas for :memory: databases (no-op there)
- Scan mbid as sql.NullString in GetAllArtistSettings to tolerate NULLs
- notifier: show artist display names (not internal IDs) in digest; resolve
names from artist_settings and fall back to ID when unavailable
- notifier: skip sending an empty digest to avoid daily spam
- config: require telegram token/chat_id when enabled
- web: warn loudly when auth is disabled on a non-loopback bind; add HTTP
server timeouts
- web: treat SetReleaseIgnored "release not found" as benign redirect (0 rows)
- musicbrainz: reject low-score/name-mismatched MBID resolutions instead of
silently caching the wrong artist
- database: remove dead duplicate err check; harden DSN param appending
- musicbrainz: check rows.Err() after iterating existing releases
- Fix duplicate Telegram notifications: SyncArtistDiscography no longer wipes
notifications_sent for the whole artist on every cache-miss re-sync; only
markers for releases that disappear are pruned (FK-safe via INSERT OR REPLACE
+ rgid NOT IN (...)).
- Cache empty MusicBrainz discographies via a new artist_settings.last_synced
column (migration 009) so zero-release artists honor the TTL instead of being
re-fetched every cycle.
- Wire the Web UI server and Telegram notifier scheduler into main.run/NewApp.
- Guard startPeriodicSync against overlapping syncs with a done-channel slot.
- Add server.public_url config; NewServerWithConfig derives reachable links
and no longer advertises the 0.0.0.0 bind address.
- Web handlers: use scanner.ScanArtist per artist, drop always-false
releaseIgnored lookup and dead endsWith, thread configured threshold.
- Limit :memory: DB pool to one connection so migrations and queries share the
same in-memory store.
Add a scan->notify->web end-to-end smoke test against a seeded :memory: DB,
fix .gitignore so the cmd/naviwatcher source dir is not excluded by the
root binary rule, and document the new sync.interval field in config.yaml.example.
Implements Task 8: artist detail page (local albums + found-missing
with ignore buttons), ignored-releases archive with restore, and POST
handlers toggling ignore flags and ignore_singles. Adds ErrArtistNotFound
sentinel so callers can distinguish missing artists, and wires routes via
the enhanced ServeMux path wildcard.
Replace compute-only run() with an immediate sync+scan followed by a
ticker-driven periodic loop, add the sync.interval config field (default
6h) with defaults and validation, and add tests for the scheduling logic.
Add ResolveArtistMBID to query the MB artist search endpoint and return the
first matching artist ID, with httptest-backed tests for match, no-match,
HTTP error, and invalid-JSON paths.
Additive migration 008 adds an mbid column to artist_settings, extend the
ArtistSettings struct and persistence functions (SaveArtistSettings,
GetArtistSettings, GetAllArtistSettings, UpdateArtistSettings) to carry the
MusicBrainz ID, and add round-trip tests covering empty and set MBID values.
- Strip standalone reissue keywords (remaster/remastered/remix/deluxe/
expanded/edition/reissue/anniversary/bonus) regardless of brackets so
non-parenthesized remasters still match the plain local title above the
0.85 threshold (was falsely reported missing).
- Fix TestAppRun_ScanLogsMissingReleases to verify run() performs the
scan itself (capture its log output) instead of re-running ScanAll
independently, which passed even if run() were a no-op.
- Fix TestSimilarity misleading cases that downgraded to a <0.85 range
check with dead want/epsilon fields; assert actual computed scores.
- Fix pagination to not trust MusicBrainz count for cutoff (prevents
silent discography truncation / hidden missing releases)
- Add idx_external_releases_artist_id index for hot per-artist queries
- Dedupe cached_at layout constant in external_releases.go
- Store cached_at in canonical UTC layout so the cache TTL cutoff comparison is a valid time ordering (previously go-sqlite3 serialized time.Time as RFC3339, making the space-separated cutoff match only by ASCII accident; same-day expired entries were falsely served as fresh).
- Remove stale no-op scanner config keys (ignore_bootlegs, include_compilations) from config.yaml.example and docs; these fields were removed from ScannerConfig but left in configs, silently doing nothing.
- Fix artist-ID namespace mismatch in MusicBrainz provider: SyncArtistDiscography
now stores the canonical Navidrome artist ID (artist_settings.id) as
external_releases.artist_id instead of the MusicBrainz MBID. Previously the
MBID was stored, which violated the FK to artist_settings and broke the
scanner join (local_albums.artist_id is the Navidrome ID), causing every
external release to be falsely reported as missing and the sync insert to
fail at runtime. getArtistFilterOptions now also resolves by the Navidrome ID.
- Resolve threshold in FindMissingReleases so the exported primitive honors the
same zero-means-default contract as ScanArtist/ScanAll.
- Remove dead maxLen==0 guard in scanner.Similarity.
- Inline trivial buildPath helper; drop unused url import in client.go.
- Replace hand-rolled itoa with strconv.Itoa in tests.
- Rewrite SyncArtistDiscography tests to seed artist_settings with the Navidrome
ID (tests previously seeded the MBID to mask the FK mismatch).
- Fix TestFuzzySmoke to exercise the real dependency (fuzzy.LevenshteinDistance /
scanner.Similarity) instead of an unused API.
- Fix TestAppRun_ScanLogsMissingReleases to run the scan against a live context
and assert the missing release is found.
- Document cached_at column in Specification.md and note startup scan / required
musicbrainz.user_agent in README.
- Stop tracking .serena/ tooling config; add it to .gitignore.
Implements ScanArtist/ScanAll in internal/scanner loading local/external
releases via the database layer with ctx-cancellation checks, plus a
compute-only run() hook that logs missing-release counts. Add table-driven
tests using in-memory SQLite fixtures.
Add FindMissingReleases and MissingRelease to internal/scanner: skips
IsIgnored external releases, scopes matches per ArtistID, and reports
external releases with no local album above the fuzzy threshold.
- Fix FK constraint violation in SyncArtistDiscography: delete
notifications_sent rows before external_releases to prevent
constraint failure when re-syncing artists with prior notifications.
- Implement per-artist type filtering: FilterReleaseGroups now accepts
FilterOptions with IgnoreSingles/IgnoreCompilations flags, read from
artist_settings table via getArtistFilterOptions.
- Fix inconsistent error wrapping: GetExternalRelease now wraps errors
with fmt.Errorf like all other functions in the package; updated test
to use errors.Is for sql.ErrNoRows check.
- Add tests: FilterReleaseGroups ignore singles/compilations,
SyncArtistDiscography per-artist type filtering, and FK-safe resync.
- Prevent infinite pagination loop when API returns empty release groups page
- Move NormalizeString regexes to package level to avoid recompilation on every call
- Use UTC consistently for cached_at timestamps to avoid DST-related TTL skew
- Add context cancellation check between pagination pages in GetArtistReleaseGroups
for responsive graceful shutdown during large discography fetches.
- Eliminate double DB query on cache hit by having GetCachedReleases return
[]ExternalRelease directly instead of just a count, avoiding a redundant
second query in SyncArtistDiscography.
- Update cache_test.go to match new GetCachedReleases return type.
- Format main.go (pre-existing whitespace issue).
- Add CacheTTL default (24h) in config applyDefaults — without this,
omitting cache_ttl from config silently defeats the entire caching
mechanism (TTL=0 means cached data is never served)
- Fix pagination to use total Count instead of checking if last page
was short — avoids wasting a rate-limit token when total count is
an exact multiple of 100
- Preserve user-set IsIgnored flags across re-syncs — previously,
DELETE+INSERT in the sync transaction reset all ignore flags to
false, losing user preferences on every cache-expiry re-sync
- Check context cancellation on cache-hit code path — previously,
ctx.Err() was not checked between cache check and returning cached
data, violating the cancellation contract
Remove dead code: duplicate ExternalRelease/Artist/ParsedArtist structs in
model.go, ParseArtist/mbArtist/mbArtistData in client.go, ArtistTypeFilter
and related filtering functions in api.go, SyncArtistDiscographyWithFilter
in sync.go, and CacheStats/IsArtistCacheValid in cache.go.
Fix bugs: SaveExternalRelease now stores NULL instead of empty string for
zero CachedAt; sync upserts are now transactional with stale release cleanup;
getCachedReleases returns int instead of *CacheStats; doGet uses url.Values
for proper query encoding of MBID.
Fix tests: removed duplicate TestRun_GracefulShutdown, removed dead code
(_ = dbPath) from TestNewApp, fixed assertions in httptest handler goroutine
to avoid data race, increased rate limiter timing tolerance, removed
Client.Close() calls (no-op removed), fixed sync test cache expiry to use
UPDATE instead of 0 TTL races.
Fix formatting: cancel()}() formatting in main.go, error format string in sync.go.