Commit Graph

1845 Commits

Author SHA1 Message Date
zarzet 87b46a9694 perf(dedupe): make the ISRC index incremental, cover-free, and batch-stable
The index was rebuilt from scratch every 5 minutes even while
AddToISRCIndex kept it write-consistent after every download, and each
rebuild parsed the FULL metadata of every FLAC in the library —
including multi-megabyte embedded cover art — sequentially, with the
triggering download blocked behind the build lock. For a few thousand
files that is gigabytes of flash I/O per rebuild, repeated throughout
a long batch.

- Indexing now reads only the Vorbis comment block (block headers are
  walked and picture/padding payloads seeked past, never loaded)
- A per-file (size, mtime) -> ISRC cache carries across rebuilds, so a
  rebuild is normally a stat walk that re-reads only changed files
- Add() refreshes the index timestamp, so the TTL no longer forces a
  rebuild in the middle of the workload the index exists to serve
- Cold builds parse with 4 workers, matching the library scanner
2026-07-10 13:08:55 +07:00
zarzet 364b2aa138 fix(metadata): stop the endless FLAC-write failures on misnamed files
When a provider delivers a lossy/unknown stream, the native finalizer
preserved the container but kept the requested .flac name, so every
metadata/ReplayGain write treated the file as FLAC and failed with
"failed to write FLAC metadata: failed to parse FLAC file: fLaC head
incorrect" — on the first download, on every retry (the file is never
deleted from public storage), on every manual edit attempt, and the
history backfill re-probed the same file on every app launch forever.

- Finalizer now renames a preserved lossy/unknown container away from
  its .flac name to match the real content (aac/MP4 -> .m4a, mp3, opus),
  mirroring the Dart pipeline, so the correct tag writer is picked
- EditFileMetadata sniffs MP4 content before taking the .flac branch
  and reports what is actually wrong (rename to .m4a) instead of the
  cryptic parse error
- The history audio-metadata backfill remembers paths whose probe
  failed permanently (unparseable content) and stops reselecting them
  on every launch; transient failures (missing file, unmounted volume)
  still retry
2026-07-10 12:07:11 +07:00
zarzet df1ac30e07 Merge Crowdin translation updates (l10n_main, #428)
Three-way per-key merge: Crowdin wins for keys it updated, locally
added keys that Crowdin has not exported yet are kept. Also normalizes
@@locale to underscore form (es_ES, pt_PT, zh_CN, zh_TW) and repairs
two translations whose placeholders diverged from the template
(ar extensionsInstallPartialSuccess, es_ES cueSplitSplitting).
2026-07-10 11:37:53 +07:00
zarzet 196fd0f651 feat(ios): run verification and OAuth through ASWebAuthenticationSession
The captcha/OAuth flow relied on the OS routing the spotiflac://
callback back into the app. In sideload containers (LiveContainer)
the guest app's URL scheme is never registered with iOS, so after
solving the challenge the redirect went nowhere and the user could
not return to the app (LiveContainer#242/#162 — unresolved upstream).

ASWebAuthenticationSession intercepts the callback scheme in-process,
so no OS-level scheme registration is involved: the session sheet
closes itself on completion and the callback URL is fed into the same
deep-link handler the OS path uses. Verification challenges, the help
dialog's open-browser action, and extension OAuth logins all prefer
the session on iOS, falling back to url_launcher if it fails to start.
Safari's cookie store is shared so captcha providers see an
established browsing context.
2026-07-10 11:25:55 +07:00
zarzet b28e8a83a8 fix(queue): keep create-playlist label at natural size
Making the button flexible let a tight layout squeeze its label away
entirely. Only the count text needs to give way — it ellipsizes while
the action buttons keep their full labels.
2026-07-10 11:13:58 +07:00
zarzet a8cc42ed30 fix(queue): prevent header row overflow on narrow screens
The track/album count header rows sized their text and action buttons
at natural width; on narrow layouts (or long localized labels) the
row overflowed by a few pixels. The count text and the create-playlist
button label now flex and ellipsize instead.
2026-07-10 10:36:02 +07:00
zarzet b4f66ff4eb chore(backend): align merged test file and fd stubs with lint conventions 2026-07-10 10:25:12 +07:00
zarzet b1f8224310 test(extensions): skip POSIX permission assertion on Windows
The signed-session test from #462 asserts the session file is 0600,
but Windows does not preserve Unix permission bits, so the suite failed
on Windows dev machines while passing in CI.
2026-07-10 10:24:25 +07:00
zarzet f808a6a369 fix(android): stop double-destroying the shared audio_service engine
MainActivity only overrode provideFlutterEngine, so the activity's
fragment delegate treated the plugin-cached engine as its own
(createFlutterFragment forwards shouldDestroyEngineWithHost, default
true) and destroyed it when the activity finished, while the engine
stayed registered in AudioServicePlugin's FlutterEngineCache. When
AudioService stopped afterwards, disposeFlutterEngine() called
destroy() on the already destroyed engine and crashed the app with
"Cannot execute operation because FlutterJNI is not attached to
native".

Mirror audio_service's own AudioServiceFragmentActivity: expose the
plugin engine as a cached engine (getCachedEngineId), never destroy it
with the host (shouldDestroyEngineWithHost = false), and pre-create it
in onCreate. The plugin remains the engine's sole owner and disposes
it only once, when the service stops with no activity attached.
2026-07-10 10:22:39 +07:00
zarzet 7d73e77f9b fix(renovate): target main instead of nonexistent dev branch
baseBranches pointed at a dev branch that does not exist on the remote,
so Renovate has been silently opening no update PRs at all.
2026-07-10 10:18:04 +07:00
zarzet 79e37769e5 chore(deps): upgrade Gradle to 9.6.1, Kotlin to 2.4.0, lifecycle to 2.11.0
AGP stays at 9.2.1 (already the latest stable). Verified with a full
debug APK build against a freshly bound gobackend.aar.
2026-07-10 10:18:04 +07:00
zarzet 9b7f1631f0 fix(native-worker): keep batches and retries intact across seam races
- Verification challenge no longer abandons the batch: batch mates the
  worker cancel would mark skipped are requeued and fenced from later
  snapshot applications, and the adoption loop restarts the queue for
  requeued items when the run ends
- Pause vs cancel race: a cancelled result with no pause flag set waits
  up to 1.5s for the pause intent to land on the main looper before
  being classified as a permanent skip
- Stale Go cancel flag: new ResetDownloadCancel export (wired through
  Android and iOS bridges) drops a pre-registered cancel with no active
  download; retryItem/retryAllFailed call it so the first retry of a
  cancelled-before-start item no longer aborts instantly
- Reconcile-loop errors now cancel the native worker and clear the run
  id before marking items failed, instead of leaving the service
  downloading a batch the UI already wrote off
2026-07-10 10:18:03 +07:00
zarzet d819878ec7 fix(downloads): stage direct-mode outputs and serialize same-path writes
The non-SAF filesystem path (Android direct mode and all of iOS) had
none of the safeguards the SAF path gained: extension downloads
streamed straight into the final filename, every error path stranded
the partial file there, and the ISRC duplicate check (exists + size>0
with a head-only FLAC parse) then accepted the truncated file as
complete forever. On iOS this was routine, not rare: the background
task grace is ~30s, after which the process is suspended mid-stream.

- fileDownload/fileDownloadChunked now stream into a .partial sibling
  and promote to the final name with an atomic rename on success;
  failures remove the staged file and never touch the final name
- fileWrite (full-content) uses the same stage-and-rename protocol
- writes are serialized per final output path, so concurrent queue
  items that resolve to the same filename can no longer interleave
  bytes into one file

The staged suffix keeps partials invisible to extension duplicate
checks, which match on the final audio extension.
2026-07-10 10:18:03 +07:00
zarzet 823b050744 chore(deps): update Go modules
go get -u across the backend: goja 2026-07-01, golang.org/x/* latest,
brotli 1.2.2, klauspost/compress 1.19. Toolchain stays on go1.25.9;
CI and docs now pin 1.25.9 explicitly.
2026-07-10 10:18:02 +07:00
zarzet d7df859f1e chore(deps): upgrade Flutter packages
Notable: ffmpeg_kit_flutter_new_full 2.1.0 -> 2.2.2 (new native FFmpeg
binaries on both platforms), file_picker 12.0.0-beta.5 -> beta.7,
audio_service 0.18.19, video_player platform packages 2.11.0.
2026-07-10 10:18:02 +07:00
zarzet 8313f3c827 fix(saf): stage all SAF downloads, serialize same-name writes, safe replace
- All SAF downloads now write under a staged .partial name; the handler
  itself promotes to the final name for legacy callers (the foreground
  Dart queue), so a killed process can never leave a half-written file
  that the exists check then accepts as complete forever. Stale partials
  are removed before reuse since fd truncation is best-effort on some
  providers.
- The exists-check/create/write/publish sequence is serialized per
  target file (tree URI + dir + name); two concurrent downloads that
  sanitize to the same display name can no longer interleave writes
  into one document — the second waits and dedupes via already_exists.
- Replacing an existing final file now renames it aside and deletes it
  only after the staged copy holds the final name; a failed rename
  restores it instead of losing both copies.
- The FFmpeg command pump always delivers a result for every claimed
  command id, including on exception or cancellation; previously a
  cancel mid-command stranded the Go post-processing call forever with
  the finalizer thread blocked inside it.
2026-07-10 10:18:02 +07:00
zarzet 22151b0988 fix(native-worker): recover from dead runs and make queue replacement safe
Stale snapshot (queue wedged forever): the service is START_NOT_STICKY,
so a process kill or reboot mid-batch froze the state file at
is_running:true with nothing ever rewriting it. Adoption then set
isProcessing and polled that flag indefinitely; restored items sat at
downloading/queued until the user intervened. Adoption and the
reconcile poll now check DownloadService.isServiceRunning() (three
consecutive dead polls in the loop), reconcile whatever the worker
finished, requeue in-flight items, clear the run id, and hand the batch
back to the Dart queue.

Adoption gate race (duplicate executors): adoption was gated on
_canUseAndroidNativeWorker, which reads extension state that loads
asynchronously after startup and settings toggles the user may have
flipped mid-run. A background batch surviving task removal was then
orphaned while the Dart queue re-downloaded the same items. Adoption
now only requires Android; request contexts that cannot be built yet
(extensions still loading) are retried on each reconcile poll.

Queue replacement race (superseded worker kept running): startNativeWorker
only cancelled the coroutine, which cannot interrupt the blocking
gomobile call, and it reset the shared cancel flag the old loop checks -
so a replaced worker kept downloading its old batch, and its finally
block wrote is_running:false over the new run's snapshot and stopSelf'd
the service underneath it. Replacement now cancels the old run's Go
downloads and FFmpeg work first, and workers carry a generation token:
a superseded generation exits at the next checkpoint and skips the
final snapshot write and service teardown. An invalid replacement
payload no longer stops the service while a run is active.
2026-07-10 10:18:01 +07:00
zarzet fb3fad279c fix(native-worker): never delete a published download over bookkeeping failures
The finalizer's catch-all cleanup deleted the finished output for any
exception, including ones thrown after the file was already published
to SAF. A failed history insert (SQLITE_BUSY against the concurrently
open Flutter connection, disk full, or a schema-version mismatch that
would throw on every download), an external .lrc write error, or a
quality-metadata refresh failure destroyed a complete, valid track and
forced a full re-download.

- Track outputPublished and skip cleanupFailedFinalizationOutput once
  the file reached its final destination
- Make the history insert, external LRC write, and quality metadata
  refresh non-fatal (log + history_written:false instead of failing
  the item)
2026-07-10 10:18:01 +07:00
zarzet 7f7e408623 ci: enforce gofmt on the Go backend
The package is fully gofmt-clean after the modernization pass, so the
formatting check deferred from the initial CI setup can now be enabled.
2026-07-10 10:18:01 +07:00
zarzet 8abb99ac91 chore(backend): modernize interface{} to any, silence lint noise
gofmt -r rewrite of all 506 interface{} occurrences to the any alias
(identical semantics), drop unused spec constants into comments, rename
unused parameters to _, convert three if-else chains to tagged
switches, and use slices.ContainsFunc for the private-IP check. No
behavior change; the whole package is now gofmt-clean.
2026-07-10 10:18:00 +07:00
zarzet e85151643f fix(extensions): serialize storage/credential/salt file writes across runtimes
Each isolated download runtime carried its own storage write mutex, so
two concurrent downloads through the same extension could interleave
writes to the shared storage file, the encrypted credentials file, or -
worst - both generate different credential salts, making the loser's
credentials undecryptable. Guard all three files with a process-wide
per-path mutex and write via temp file + rename so readers never see a
torn file. Groundwork for concurrent downloads.
2026-07-10 10:18:00 +07:00
zarzet 1998a9300e feat(downloads): support up to 3 concurrent downloads
The Dart queue loop was already built around an activeDownloads map and
per-item progress tracking but was gated to one download at a time. Add
a concurrentDownloads setting (1-3, default 1) exposed in Download
Settings > Performance, and let the scheduler keep up to that many
items in flight. The experimental Android native worker is still
strictly sequential, so it is skipped in favor of the Dart queue while
concurrency is enabled.
2026-07-10 10:17:59 +07:00
zarzet 19f69a6090 refactor: dispatch on MusicServices constants instead of raw service ids
Service ids ('deezer', 'tidal', 'qobuz', 'spotify', 'amazon', 'local')
were compared as raw string literals in every dispatch chain. Introduce
MusicServices constants and use them at the comparison/switch sites so
a typo becomes a compile error instead of a silently dead branch.
2026-07-10 10:17:59 +07:00
zarzet c24a72c302 refactor(metadata): split _TrackMetadataScreenState into part files
track_metadata_screen.dart was 5,470 lines, most of it one State class
mixing five surfaces. Move four cohesive method clusters into private
extensions on _TrackMetadataScreenState in new part files: header/info
cards, lyrics + save/re-enrich flows, audio conversion + CUE split, and
file actions. Same library, so private state access is unchanged;
setState goes through a _setState forwarder as in queue_tab. The screen
file is now 1,575 lines.
2026-07-10 10:17:58 +07:00
zarzet 860649e3ac refactor(queue): split _QueueTabState method clusters into part files
queue_tab.dart was 7,567 lines with a single 7,300-line State class.
Continue the existing part-file split by moving six cohesive method
clusters into private extensions on _QueueTabState (same library, so
private state access is unchanged): selection mode, navigation/open
actions, collection item builders, filter UI, batch actions, and
queue/library item builders. setState calls in the parts go through a
_setState forwarder since @protected members cannot be called from
extensions. queue_tab.dart is now 2,462 lines.

# Conflicts:
#	lib/screens/queue_tab.dart
2026-07-10 10:15:56 +07:00
zarzet 2e7e2b1964 refactor(ui): share legacy provider-id resource helpers
legacyProviderIdFromResourceId and stripPrefixedResourceId were
copy-pasted in the album, artist, and playlist screens with slightly
different null semantics; the shared nullable variant plus explicit
?? 'spotify' fallbacks makes the difference visible at the call sites.
2026-07-10 10:12:20 +07:00
zarzet 70cba44164 refactor(ui): merge duplicated album/playlist track rows into TrackListTile
_AlbumTrackItem and _PlaylistTrackItem were line-for-line identical
except for the leading widget and whether the artist name is clickable;
both are now parameters of a shared TrackListTile.
2026-07-10 10:12:20 +07:00
zarzet 28b877f0eb refactor(providers): split download history out of download_queue_provider
download_queue_provider.dart (9,525 lines) hosted the entire download
history feature alongside the queue. Move DownloadHistoryItem/State/
Notifier and the history providers to download_history_provider.dart,
and promote the audio format/quality helpers both need into
lib/utils/audio_format_utils.dart. The queue provider re-exports the
history library, so existing imports keep working unchanged.
2026-07-10 10:12:20 +07:00
zarzet 32892bd4bd chore(i18n): localize folder-access download errors, drop em dashes
The two folder-access failure messages are stored on items as fixed
English sentinels so code can match on them; translate the known
sentinels at display time (error dialog and failed-row message) via new
downloadErrorSafPermissionLost / downloadErrorFolderAccessLost keys.
Also replace em dashes in recently added comments.
2026-07-10 10:12:19 +07:00
zarzet bb66cc470a fix(queue): keep just-completed tracks pinned while the batch is running
A track that finished downloading dropped out of the active lead zone
and re-appeared in the history zone below every still-queued sibling,
visually jumping several positions during album downloads. The existing
completion bridge masked this for only 6 seconds.

Keep completion-bridge cells pinned in the lead zone for as long as
other downloads are still active, and hide their history rows while
pinned so tracks don't render twice. Once the queue goes idle the
bridge dismisses and items settle into their normal history position.
2026-07-10 10:12:19 +07:00
zarzet 60bf871945 fix(downloads): flush queue persistence when app is backgrounded
Queue writes are debounced by 350 ms and the only guaranteed flush ran
in the provider's onDispose, which never fires when the OS kills the
backgrounded process - the most recent queue mutations were silently
lost, so items could vanish after the app sat minimised for a while.

Expose flushQueuePersistence() on the queue notifier and call it from
the app lifecycle observer on AppLifecycleState.paused, the last
reliable callback before a potential process kill.
2026-07-10 10:12:18 +07:00
zarzet d988b2c9d3 fix(downloads): defer verification challenge until app is foregrounded
When a download hit a verification challenge while the app was in the
background, the browser launch was attempted immediately (blocked or
silently dropped by the OS) and the 5-minute grant timeout kept running,
so the item failed without the user ever seeing the CAPTCHA.

Wait for AppLifecycleState.resumed before opening the challenge and
before starting the grant timeout, and post a local notification asking
the user to return to the app. If the process is killed while waiting,
the persisted queue already restores the item as queued on next launch.
2026-07-10 10:12:18 +07:00
zarzet 2f53be7c0d fix(extensions): expire stale pending auth challenges
A pending verification challenge lived in a process-global map with no
timestamp, so a challenge created while the app was backgrounded (and
never completed) was served again verbatim on the next attempt - sending
the user to an already-expired CAPTCHA page that needed a second request.

Stamp PendingAuthRequest with CreatedAt at every writer and make
ensureExtensionPendingAuthRequest discard entries older than 5 minutes,
starting a fresh signed-session verification instead.
2026-07-10 10:12:18 +07:00
zarzet a1764b1aa0 fix(metadata): close FLAC file handles after parsing
flac.ParseFile leaks the *os.File on parse errors, and read-only callers
(ReadMetadata, ExtractCoverArt, extractLyricsFromFlac) never closed the
handle on success either. Long library or duplicate-index scans leaked
one fd per FLAC file, and on Windows the lingering handles kept files
locked (the source of the flaky TempDir cleanup failure in
TestExportsJSONWrappersAndExtensionManagerSurface).

Route all parsing through parseFlacFile, which closes on error, and
defer f.Close() at every call site. File.Save already closes the
underlying handle; the deferred second Close is a harmless no-op.
2026-07-10 10:12:17 +07:00
zarzet 99eed524c2 refactor(backend): split extension_providers.go into themed files
Pure mechanical move within package gobackend — no signature or behavior
change. extension_providers.go (3,798 lines) now holds only the
extensionManager provider-listing/orchestration methods; DTOs, goja value
conversion, the provider wrapper methods, download fallback, and priority
state moved to extension_{provider_types,goja_convert,provider_wrapper,
fallback,priority}.go.
2026-07-10 10:12:17 +07:00
zarzet d909067c49 refactor(backend): split exports.go into themed files
Pure mechanical move within package gobackend — no signature or behavior
change, so the gomobile binding surface is identical. exports.go (4,141
lines) now holds only availability/dir/duplicate/filename exports; the
rest moved to exports_{musicbrainz,download,reenrich,metadata,lyrics,
deezer,extensions,store,library}.go along existing responsibility
boundaries.
2026-07-10 10:12:17 +07:00
zarzet 927fc8abac feat(downloads): detect lost SAF grant and offer folder re-selection
A persisted SAF tree grant can die silently (folder deleted or renamed,
SD card removed, app data reset, system revocation). Downloads then fail
with no recovery path beyond digging through Settings.

- Add isSafTreeAccessible bridge method: the grant must still be in the
  system's persisted permission list and the tree writable
- Show a warning banner in Files & Folders when the saved SAF folder is
  no longer accessible, with an inline re-select action
- In the download error dialog, replace Retry with a Re-select folder
  action when the failure is a lost folder grant (Android SAF or iOS
  bookmark); a successful re-pick retries the item automatically
- Share the folder-access error messages as constants so the UI can
  match on them reliably
2026-07-10 10:12:16 +07:00
zarzet 53824b7ac2 fix(ios): create security-scoped bookmark inside native folder picker session
The download/library folder picker on iOS previously took the bare path
string returned by file_picker and rebuilt a URL from it to create a
security-scoped bookmark. By then the picker's access grant is gone, so
bookmark creation either failed (folder selection silently rejected) or
produced a bookmark that could not be re-opened, after which the first
download run overwrote the user's chosen folder back to app Documents.

- Add a native pickIosDirectory method channel that presents
  UIDocumentPickerViewController itself and creates the bookmark inside
  the delegate callback, while the security-scoped grant is active
- Use it for the download directory (settings + first-run setup) and
  the local library folder pickers
- Stop overwriting the saved download directory when the bookmark fails
  at queue start; fail queued items with a clear message instead
- Skip the launch-time iOS path normalizer when a bookmark exists, so
  it no longer rewrites external folder paths and drops their bookmark

Fixes #454
2026-07-10 10:12:16 +07:00
zarzet fd7424dd81 refactor(ui): extract shared widgets, drop dead screens
Extract InLibraryBadge (pasted verbatim in five screens) and
playLocalIfAvailable (duplicated in three) into shared code, and delete
SearchScreen, LibraryPlaylistsScreen, and CollapsingHeader, which are
imported nowhere in lib/.
2026-07-10 10:12:15 +07:00
zarzet 46491c137a docs(contributing): fix branch refs and build steps, add PR template 2026-07-10 10:12:15 +07:00
zarzet 0535b266ff ci: run flutter analyze/test and go vet/test on pull requests 2026-07-10 10:12:15 +07:00
zarzet d20f569678 test(download): cover playlist_position in payload serialization test 2026-07-10 10:12:14 +07:00
zarzet 4c83aeafb5 feat(metadata): surface explicit content flag with [E] badge
Port of the explicit-content indicator from SpotiFLAC-Web (issue #456):

- Go: add explicit to ExtTrackMetadata (parsed generically from any
  extension via explicit/is_explicit/isExplicit), TrackMetadata, and
  AlbumTrackMetadata; built-in Deezer maps explicit_lyrics and
  explicit_content_lyrics == 1
- Dart: add explicit to the Track model with a tolerant
  parseExplicitFlag helper wired into all backend track parsers
- UI: new ExplicitBadge rendered through buildQualityBadges on album,
  playlist, search, and home track rows
2026-07-10 10:12:14 +07:00
zarzet 8abb9d119b fix(store): harden extension repo registry handling
- Persist registry URL inside store_cache.json and restore it on init so
  the disk cache survives app restarts instead of being wiped every launch
- Fall back to the cached registry when the fetched body fails to parse,
  matching the existing network/HTTP error fallbacks
- Detect HTML responses and surface a clear message instead of the raw
  Go JSON error (invalid character '<') seen in issue #474
- Validate the registry loads before persisting a new URL, and roll the
  backend back to the previous URL on failure, so a broken link never
  survives a restart or Android auto-backup restore
2026-07-10 10:12:13 +07:00
zarzet 6b30eba947 feat(extensions): complete signed-session grants from help dialog
Add a platform bridge to finish session grants on Android and iOS with
JSON success validation, let users paste callback URLs from the
clipboard, and auto-dismiss the verification help dialog after grant.
2026-07-10 10:12:13 +07:00
zarzet edb241faf7 fix(download): suffix batch filenames with track number when needed
Mark multi-track queue additions as batch downloads and append
{track:02} - {title} when the configured format has no uniqueness
token, instead of relying on playlist position alone.
2026-07-10 10:12:12 +07:00
rrbharath 49fa6c14e6 Update link to Extension Development Guide (#380) 2026-07-10 10:11:20 +07:00
Amix 89c9fa323b docs: align contributing guide with actual branch and Riverpod pattern (#461)
The repo has no `dev` branch (only `main` and l10n-related branches),
and recent history shows PRs merge straight into `main` — update the
fork/PR instructions to match. Also swap the Riverpod example for the
hand-written `Notifier` pattern actually used across lib/providers/,
since the codebase doesn't use riverpod_annotation code generation.
2026-07-10 10:11:16 +07:00
Amix fc2818dd97 test(extensions): add coverage for signed session HMAC auth flow (#462)
extension_signed_session.go implements the HMAC-signed session
scheme extensions use to authenticate against their own backends
(namespace scoping, on-disk persistence, rolling-key signing,
expiry/refresh, and the grant exchange flow), but had zero direct
or indirect test coverage.

Adds table-driven tests for the pure helpers (namespace
sanitization, config defaults, time parsing, URL building,
Retry-After parsing, record scope normalization) plus
runtime-backed tests using a mocked http.RoundTripper for the
stateful flows: file persistence round-trips, status/clear,
grant exchange and refresh, 401-triggered session revocation, and
unauthenticated fetches surfacing the verification URL.

The centerpiece is TestDoSignedSessionRequestSignature, which
recomputes the rolling-key HMAC signature server-side from the
request headers the client actually sent, the same way a real
backend would validate it, to catch any accidental regression in
the signing scheme itself (field order, rolling-key derivation, or
header names).
2026-07-10 10:11:12 +07:00
Amix c281763718 fix(ui): use State's context instead of stale parameter in CSV import (#460)
_importCsv guards every other post-await BuildContext use with the
State's own `mounted`/`this.context`, but one spot read the l10n
strings off the function's `context` parameter instead, requiring a
`// ignore: use_build_context_synchronously` to silence the analyzer.
Switching to `this.context` matches the surrounding guard and lets the
lint verify the usage on its own, so the suppression is no longer
needed.
2026-07-10 10:11:04 +07:00