Merge pull request 'v2026.05.21.0 — Wear OS dispatch fix, playlist-load feedback, drift CI' (#58) from dev into main

This commit was merged in pull request #58.
This commit is contained in:
2026-05-21 15:42:06 -04:00
27 changed files with 605 additions and 507 deletions
+6 -3
View File
@@ -31,10 +31,13 @@ on:
jobs: jobs:
analyze-test-build: analyze-test-build:
# flutter-ci runner image (CI-Runner/CI-flutter/Dockerfile) bakes in # Toolchain selected via container.image per ci-runners.md
# Flutter SDK + Android cmdline-tools + platform-34 + build-tools 34.0.0 # (label = scheduling handle, image = environment). The ci-flutter
# + JDK 17 + a non-root `runner` user. No setup-action ceremony needed. # image ships Flutter SDK + Android cmdline-tools/platform/build-tools
# + Java 25. See ci-requirements.md.
runs-on: flutter-ci runs-on: flutter-ci
container:
image: git.fabledsword.com/bvandeusen/ci-flutter:3.44
defaults: defaults:
run: run:
+2
View File
@@ -23,6 +23,8 @@ on:
jobs: jobs:
release: release:
runs-on: go-ci runs-on: go-ci
container:
image: git.fabledsword.com/bvandeusen/ci-go:1.26
env: env:
IMAGE: git.fabledsword.com/bvandeusen/minstrel IMAGE: git.fabledsword.com/bvandeusen/minstrel
+6
View File
@@ -29,6 +29,7 @@ on:
- 'sqlc.yaml' - 'sqlc.yaml'
- 'internal/**' - 'internal/**'
- 'cmd/**' - 'cmd/**'
- '.golangci.yml'
- '.forgejo/workflows/test-go.yml' - '.forgejo/workflows/test-go.yml'
pull_request: pull_request:
branches: [main] branches: [main]
@@ -39,11 +40,14 @@ on:
- 'sqlc.yaml' - 'sqlc.yaml'
- 'internal/**' - 'internal/**'
- 'cmd/**' - 'cmd/**'
- '.golangci.yml'
- '.forgejo/workflows/test-go.yml' - '.forgejo/workflows/test-go.yml'
jobs: jobs:
test: test:
runs-on: go-ci runs-on: go-ci
container:
image: git.fabledsword.com/bvandeusen/ci-go:1.26
steps: steps:
- name: Checkout - name: Checkout
@@ -65,6 +69,8 @@ jobs:
integration: integration:
runs-on: go-ci runs-on: go-ci
container:
image: git.fabledsword.com/bvandeusen/ci-go:1.26
services: services:
postgres: postgres:
image: postgres:16-alpine image: postgres:16-alpine
+2 -12
View File
@@ -19,6 +19,8 @@ on:
jobs: jobs:
test: test:
runs-on: go-ci runs-on: go-ci
container:
image: git.fabledsword.com/bvandeusen/ci-go:1.26
defaults: defaults:
run: run:
@@ -28,18 +30,6 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup Node
# `cache: 'npm'` removed — the Forgejo Actions cache server at
# 172.18.0.27:41161 isn't reachable from this runner's container,
# so setup-node spent 4m41s burning the npm cache restore on
# ETIMEDOUT before failing open. npm ci still works deterministically
# from package-lock.json; just re-fetches from the registry on each
# run. Restore the cache option once the runner-host network reaches
# the cache server.
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Install deps - name: Install deps
run: npm ci run: npm ci
+16 -15
View File
@@ -1,28 +1,29 @@
version: "2"
run: run:
timeout: 5m timeout: 5m
tests: true tests: true
linters: linters:
disable-all: true default: none
enable: enable:
- errcheck - errcheck
- govet - govet
- ineffassign - ineffassign
- staticcheck - staticcheck
- unused - unused
- revive
settings:
revive:
# Intentionally narrow: we skip `exported` (no doc-comment requirement) per
# the project's no-boilerplate-comment policy. Re-enable if the public API
# surface grows to the point where documentation lives alongside it.
rules:
- name: var-naming
- name: unused-parameter
- name: early-return
formatters:
enable:
- gofmt - gofmt
- goimports - goimports
- revive
linters-settings:
revive:
# Intentionally narrow: we skip `exported` (no doc-comment requirement) per
# the project's no-boilerplate-comment policy. Re-enable if the public API
# surface grows to the point where documentation lives alongside it.
rules:
- name: var-naming
- name: unused-parameter
- name: early-return
issues:
exclude-use-default: false
+47
View File
@@ -0,0 +1,47 @@
# CI Requirements — Minstrel
> Copy of [the template](https://git.fabledsword.com/bvandeusen/CI-runner/src/branch/main/docs/requirements-sheet.template.md).
> Family-wide policy: `ci-runners.md` in the FabledRulebook.
## Runtime images
Minstrel's four workflows consume two CI images:
```
git.fabledsword.com/bvandeusen/ci-go:1.26
git.fabledsword.com/bvandeusen/ci-flutter:3.44
```
- `ci-go:1.26` — Go server tests (`.forgejo/workflows/test-go.yml`), web SPA tests (`.forgejo/workflows/test-web.yml`), and the release container build (`.forgejo/workflows/release.yml`).
- `ci-flutter:3.44` — Flutter client tests + debug/release APK builds (`.forgejo/workflows/flutter.yml`).
## Image deps used
### From `ci-go:1.26`
- **Go** (1.26 toolchain) — `go vet`, `go test -race`, `go build`, `go mod`.
- **Node + npm** — `npm ci` and `npm test` / `npm run check` in `test-web.yml`.
- **golangci-lint** — lint pass in `test-go.yml`.
- **docker CLI** — bridge-IP discovery of the per-job Postgres service container in `test-go.yml` integration job (via the runner's shared `/var/run/docker.sock`).
- **docker buildx** — release container build + push in `release.yml`.
- **curl** — release-asset polling / upload in `release.yml`.
### From `ci-flutter:3.44`
- **Flutter** (3.44 stable channel) — `flutter pub get`, `flutter analyze --fatal-infos`, `flutter test`, `flutter build apk` (debug + signed release).
- **Dart** — `dart run tool/gen_tokens.dart`, `dart run build_runner build` (drift codegen).
- **Android SDK + NDK + cmdline-tools + build-tools** — APK assembly + signing.
- **Java 25** — Gradle / Android build.
- **git** — `actions/checkout@v4` baseline (and any shell git operations).
- **base64 + curl** — keystore decode + release-asset upload in the tag-build path.
## Per-job tool installs
None.
## Notes
- **Label/image split.** Workflows keep `runs-on: go-ci` / `runs-on: flutter-ci` as the scheduling label per the [`ci-runners.md`](https://…/FabledRulebook/ci-runners.md) "label = scheduling handle, image = `container.image`" pattern. The labels are intentional handles, not toolchain assertions.
- **Integration-job docker-socket dependency.** `test-go.yml`'s integration job uses the runner's shared docker socket (`/var/run/docker.sock`) to bridge-IP-discover the per-job Postgres service container by name + network intersection — the dev compose's `minstrel-postgres-*` containers are explicitly skipped as belt-and-suspenders. Depends on `act_runner.valid_volumes` whitelisting the socket; if that ever stops auto-mounting, integration tests fail at the `docker inspect` step.
- **Go toolchain pin.** `go.mod` is on `go 1.25.0` because `golang.org/x/crypto v0.51.0` declares 1.25 as its minimum. `ci-go:1.26` satisfies this with headroom. Future `x/crypto` bumps that move the Go floor should be paired with an image-tag bump in this file + the workflows.
- **In-app update channel polling.** `release.yml` polls Forgejo's release-asset API for up to 15 min on tag pushes to fetch the APK that `flutter.yml` is concurrently attaching to the same release. The asset eventually appears because `flutter.yml` and `release.yml` run in parallel on the same tag; if the polling times out, the server image ships without the bundled update channel (graceful degradation, not a build failure).
- **Cache server reachability.** `test-web.yml` does NOT use `cache: 'npm'` on `actions/setup-node` — the Forgejo Actions cache server isn't reachable from this runner's container network and `setup-node` was burning ~4m41s on ETIMEDOUT before failing open. With the migration to `ci-go:1.26`, `setup-node` is removed entirely (Node is in the image). The cache concern reappears if a future change re-introduces a network-dependent action.
- **Friction asks.** None pending. The two images cover everything Minstrel needs.
+31 -13
View File
@@ -559,13 +559,24 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl
mediaItem.add(current.copyWith(artUri: Uri.file(path))); mediaItem.add(current.copyWith(artUri: Uri.file(path)));
} }
/// Drives the session to a terminal state and tears down the /// Display-state-only teardown. Stops playback, clears the queue and
/// foreground service so external surfaces (Wear tile, lock screen, /// current track, and broadcasts idle so external surfaces (Wear tile,
/// in-app mini bar) drop it instead of showing a stale paused track. /// notification, in-app mini bar) drop their visible state — but does
/// Called on the idle timeout and from onTaskRemoved when not playing; /// NOT call super.stop(), so the FGS and MediaSessionCompat remain
/// no other path stops the session (the notification is ongoing). /// alive and addressable.
@override ///
Future<void> stop() async { /// Why this matters (Fable #472): the paired Wear OS companion app
/// caches a MediaController bound to our MediaSession's token. If
/// the system destroys our service (super.stop() → stopSelf() makes
/// it eligible under memory pressure), the next play() spins up a
/// new MediaSession with a fresh token; the companion's cached
/// controller still points at the dead one and transport taps from
/// notification + watch silently no-op. Keeping the service alive
/// across idle periods preserves the binding.
///
/// Used by the #52 idle timeout. Full termination (super.stop) is
/// reserved for the explicit-close path (onTaskRemoved while idle).
Future<void> _softTeardown() async {
_idleStopTimer?.cancel(); _idleStopTimer?.cancel();
_idleStopTimer = null; _idleStopTimer = null;
_positionBroadcastTimer?.cancel(); _positionBroadcastTimer?.cancel();
@@ -575,17 +586,22 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl
try { try {
await _player.stop(); await _player.stop();
} catch (_) {} } catch (_) {}
// Explicit terminal broadcast before super.stop() tears down the
// isolate/notification, so subscribers see idle even if the
// player's own event lags the service teardown. queue/mediaItem
// cleared so the in-app mini bar collapses in lockstep with the
// watch tile rather than lingering on the last track.
playbackState.add(playbackState.value.copyWith( playbackState.add(playbackState.value.copyWith(
playing: false, playing: false,
processingState: AudioProcessingState.idle, processingState: AudioProcessingState.idle,
)); ));
mediaItem.add(null); mediaItem.add(null);
queue.add([]); queue.add([]);
}
/// Full termination — soft teardown plus super.stop(), which calls
/// stopSelf() on the AudioService and lets the OS reclaim it. Reserved
/// for the explicit-close path (onTaskRemoved when not playing). On the
/// idle path we use _softTeardown instead to preserve the Wear OS
/// MediaController binding (Fable #472).
@override
Future<void> stop() async {
await _softTeardown();
await super.stop(); await super.stop();
} }
@@ -817,7 +833,9 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl
if (_player.playing && ps != ProcessingState.completed) { if (_player.playing && ps != ProcessingState.completed) {
return; // resumed between arm and fire return; // resumed between arm and fire
} }
unawaited(stop()); // _softTeardown — not full stop() — to preserve the MediaSession
// binding for the paired Wear OS companion (Fable #472).
unawaited(_softTeardown());
} }
/// While actively playing, keeps a 1s periodic PlaybackState re- /// While actively playing, keeps a 1s periodic PlaybackState re-
@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_lucide/flutter_lucide.dart'; import 'package:flutter_lucide/flutter_lucide.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -70,7 +72,7 @@ class PlaylistCard extends ConsumerWidget {
right: 6, right: 6,
child: PlayCircleButton( child: PlayCircleButton(
enabled: hasTracks, enabled: hasTracks,
onPressed: () => _playPlaylist(ref), onPressed: () => _playPlaylist(context, ref),
), ),
), ),
// System playlists get a refresh affordance so the // System playlists get a refresh affordance so the
@@ -187,16 +189,34 @@ class PlaylistCard extends ConsumerWidget {
/// PlaylistTrack into a TrackRef (filtering out unavailable rows /// PlaylistTrack into a TrackRef (filtering out unavailable rows
/// whose `trackId` is null after a track-delete), and plays from /// whose `trackId` is null after a track-delete), and plays from
/// index 0. Mirrors the web PlaylistCard's onPlayClick. /// index 0. Mirrors the web PlaylistCard's onPlayClick.
Future<void> _playPlaylist(WidgetRef ref) async { ///
/// Failure modes are surfaced via SnackBar so a not-yet-built mix
/// or a slow server doesn't look like the tap was ignored.
Future<void> _playPlaylist(BuildContext context, WidgetRef ref) async {
final messenger = ScaffoldMessenger.of(context);
final api = await ref.read(playlistsApiProvider.future); final api = await ref.read(playlistsApiProvider.future);
// Refreshable (singleton) system playlists: fetch the server's final PlaylistDetail detail;
// rotation-aware order (#415) and play as-is, tagged with the try {
// source so the reporter advances rotation. User playlists AND // Refreshable (singleton) system playlists: fetch the server's
// non-singleton system kinds (songs_like_artist — no by-kind // rotation-aware order (#415) and play as-is, tagged with the
// endpoint) play the stored order via get(), untagged. // source so the reporter advances rotation. User playlists AND
final detail = playlist.refreshable // non-singleton system kinds (songs_like_artist — no by-kind
? await api.systemShuffle(playlist.systemVariant!) // endpoint) play the stored order via get(), untagged.
: await api.get(playlist.id); detail = await (playlist.refreshable
? api.systemShuffle(playlist.systemVariant!)
: api.get(playlist.id))
.timeout(const Duration(seconds: 8));
} on TimeoutException {
messenger.showSnackBar(const SnackBar(
content: Text("Couldn't load playlist — check your connection"),
));
return;
} catch (e) {
messenger.showSnackBar(SnackBar(
content: Text('Playlist load failed: $e'),
));
return;
}
final refs = <TrackRef>[]; final refs = <TrackRef>[];
for (final t in detail.tracks) { for (final t in detail.tracks) {
if (t.trackId == null) continue; if (t.trackId == null) continue;
@@ -211,7 +231,12 @@ class PlaylistCard extends ConsumerWidget {
streamUrl: t.streamUrl ?? '', streamUrl: t.streamUrl ?? '',
)); ));
} }
if (refs.isEmpty) return; if (refs.isEmpty) {
messenger.showSnackBar(const SnackBar(
content: Text("Mix isn't ready yet — try again in a moment"),
));
return;
}
// Rotation-aware kinds arrive pre-ordered from the server — play // Rotation-aware kinds arrive pre-ordered from the server — play
// as-is, tagged so the reporter advances rotation. No client // as-is, tagged so the reporter advances rotation. No client
// shuffle. Everything else plays stored order, untagged. // shuffle. Everything else plays stored order, untagged.
@@ -69,18 +69,24 @@ class AddToPlaylistSheet extends ConsumerWidget {
itemCount: owned.length, itemCount: owned.length,
itemBuilder: (_, i) { itemBuilder: (_, i) {
final p = owned[i]; final p = owned[i];
return ListTile( // Material(transparency) gives ListTile an ink target
key: Key('add_to_playlist_${p.id}'), // beneath the outer Container's color paint — required
leading: Icon(LucideIcons.list_music, color: fs.parchment), // by the Flutter 3.44 ListTile/ColoredBox assertion.
title: Text( return Material(
p.name, type: MaterialType.transparency,
style: TextStyle(color: fs.parchment), child: ListTile(
key: Key('add_to_playlist_${p.id}'),
leading: Icon(LucideIcons.list_music, color: fs.parchment),
title: Text(
p.name,
style: TextStyle(color: fs.parchment),
),
subtitle: Text(
'${p.trackCount} ${p.trackCount == 1 ? "track" : "tracks"}',
style: TextStyle(color: fs.ash, fontSize: 12),
),
onTap: () => Navigator.pop(context, p.id),
), ),
subtitle: Text(
'${p.trackCount} ${p.trackCount == 1 ? "track" : "tracks"}',
style: TextStyle(color: fs.ash, fontSize: 12),
),
onTap: () => Navigator.pop(context, p.id),
); );
}, },
); );
@@ -236,10 +236,17 @@ class _MenuItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final fs = Theme.of(context).extension<FabledSwordTheme>()!; final fs = Theme.of(context).extension<FabledSwordTheme>()!;
return ListTile( // Transparency-typed Material sits between the outer Container's color
leading: Icon(icon, color: fs.parchment), // paint and the ListTile so ListTile can paint its own ink splashes.
title: Text(label, style: TextStyle(color: fs.parchment)), // Flutter 3.44 promoted the "ListTile inside ColoredBox without Material"
onTap: onTap, // warning to a hard assertion.
return Material(
type: MaterialType.transparency,
child: ListTile(
leading: Icon(icon, color: fs.parchment),
title: Text(label, style: TextStyle(color: fs.parchment)),
onTap: onTap,
),
); );
} }
} }
+61 -13
View File
@@ -58,13 +58,13 @@ packages:
source: hosted source: hosted
version: "0.1.4" version: "0.1.4"
audio_session: audio_session:
dependency: transitive dependency: "direct main"
description: description:
name: audio_session name: audio_session
sha256: "2b7fff16a552486d078bfc09a8cde19f426dc6d6329262b684182597bec5b1ac" sha256: "7217b229db57cc4dc577a8abb56b7429a5a212b978517a5be578704bfe5e568b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.1.25" version: "0.2.3"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@@ -249,14 +249,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.7" version: "3.0.7"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
url: "https://pub.dev"
source: hosted
version: "1.0.9"
dart_style: dart_style:
dependency: transitive dependency: transitive
description: description:
@@ -366,6 +358,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.0" version: "6.0.0"
flutter_lucide:
dependency: "direct main"
description:
name: flutter_lucide
sha256: d2866c9ba75b2300e73a888489f0d7ef1d225e1c352d7e7f5c7fdff7d04e294c
url: "https://pub.dev"
source: hosted
version: "1.11.0"
flutter_riverpod: flutter_riverpod:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -378,10 +378,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_secure_storage name: flutter_secure_storage
sha256: "8b302d17096ba88f911b7eb317c71d5e691da60a259549f42b38c658d1776d87" sha256: "6848263f9744072d0977347c383fb8b57d9780319a6bf5238b5a2866a029de62"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.1.0" version: "10.2.0"
flutter_secure_storage_darwin: flutter_secure_storage_darwin:
dependency: transitive dependency: transitive
description: description:
@@ -800,6 +800,54 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" version: "2.3.0"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1
url: "https://pub.dev"
source: hosted
version: "12.0.1"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6"
url: "https://pub.dev"
source: hosted
version: "13.0.1"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
url: "https://pub.dev"
source: hosted
version: "9.4.7"
permission_handler_html:
dependency: transitive
description:
name: permission_handler_html
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
url: "https://pub.dev"
source: hosted
version: "0.1.3+5"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
url: "https://pub.dev"
source: hosted
version: "4.3.0"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
url: "https://pub.dev"
source: hosted
version: "0.2.1"
petitparser: petitparser:
dependency: transitive dependency: transitive
description: description:
+3 -4
View File
@@ -1,7 +1,7 @@
name: minstrel name: minstrel
description: Minstrel mobile client description: Minstrel mobile client
publish_to: 'none' publish_to: 'none'
version: 2026.5.19+13 version: 2026.5.21+14
environment: environment:
sdk: '>=3.5.0 <4.0.0' sdk: '>=3.5.0 <4.0.0'
@@ -19,7 +19,7 @@ dependencies:
# interruption/becoming-noisy itself (just_audio does not). Same author # interruption/becoming-noisy itself (just_audio does not). Same author
# as just_audio/audio_service so versions track together. # as just_audio/audio_service so versions track together.
audio_session: ^0.2.3 audio_session: ^0.2.3
flutter_secure_storage: ^10.1.0 flutter_secure_storage: ^10.2.0
go_router: ^17.2.3 go_router: ^17.2.3
flutter_svg: ^2.0.16 flutter_svg: ^2.0.16
# Lucide icon set (design system mandates Lucide, not Material). # Lucide icon set (design system mandates Lucide, not Material).
@@ -33,7 +33,6 @@ dependencies:
# until either lib bumps win32 to 6.x. # until either lib bumps win32 to 6.x.
package_info_plus: ^8.3.1 package_info_plus: ^8.3.1
pub_semver: ^2.1.4 pub_semver: ^2.1.4
cupertino_icons: ^1.0.8
path_provider: ^2.1.5 path_provider: ^2.1.5
drift: ^2.18.0 drift: ^2.18.0
drift_flutter: ^0.2.0 drift_flutter: ^0.2.0
@@ -59,7 +58,7 @@ dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^6.0.0 flutter_lints: ^6.0.0
mocktail: ^1.0.4 mocktail: ^1.0.5
drift_dev: ^2.18.0 drift_dev: ^2.18.0
build_runner: ^2.4.13 build_runner: ^2.4.13
+14 -15
View File
@@ -1,6 +1,3 @@
@Tags(['drift'])
library;
import 'dart:io'; import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
@@ -11,13 +8,10 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:minstrel/cache/audio_cache_manager.dart'; import 'package:minstrel/cache/audio_cache_manager.dart';
import 'package:minstrel/cache/db.dart'; import 'package:minstrel/cache/db.dart';
// See sync_controller_test.dart for the same skip rationale.
const _skipDrift = 'libsqlite3 missing on flutter-ci runner; on-device covers';
AppDb _testDb() => AppDb(NativeDatabase.memory()); AppDb _testDb() => AppDb(NativeDatabase.memory());
void main() { void main() {
test('isCached returns false when no row exists', skip: _skipDrift, () async { test('isCached returns false when no row exists', () async {
final db = _testDb(); final db = _testDb();
addTearDown(db.close); addTearDown(db.close);
final mgr = AudioCacheManager( final mgr = AudioCacheManager(
@@ -29,7 +23,7 @@ void main() {
expect(await mgr.pathFor('nonexistent'), null); expect(await mgr.pathFor('nonexistent'), null);
}); });
test('usageBytes sums sizeBytes across rows', skip: _skipDrift, () async { test('bucketUsage sums drift sizeBytes across rows', () async {
final db = _testDb(); final db = _testDb();
addTearDown(db.close); addTearDown(db.close);
final tmp = Directory.systemTemp.createTempSync(); final tmp = Directory.systemTemp.createTempSync();
@@ -38,25 +32,30 @@ void main() {
dioFactory: () async => Dio(), dioFactory: () async => Dio(),
cacheDirFactory: () async => tmp, cacheDirFactory: () async => tmp,
); );
// Each row needs a distinct `path` because path is not the primary
// key, but mapping by trackId only works if rows are distinct.
await db.batch((b) { await db.batch((b) {
b.insertAll(db.audioCacheIndex, [ b.insertAll(db.audioCacheIndex, [
AudioCacheIndexCompanion.insert( AudioCacheIndexCompanion.insert(
trackId: 'a', trackId: 'a',
path: 'p', path: 'p_a',
sizeBytes: 100, sizeBytes: 100,
source: CacheSource.manual), source: CacheSource.manual),
AudioCacheIndexCompanion.insert( AudioCacheIndexCompanion.insert(
trackId: 'b', trackId: 'b',
path: 'p', path: 'p_b',
sizeBytes: 250, sizeBytes: 250,
source: CacheSource.incidental), source: CacheSource.incidental),
]); ]);
}); });
expect(await mgr.usageBytes(), 350); // usageBytes() is a directory walk (authoritative on-disk total,
// catches orphan partials); for a drift-sizeBytes sum, bucketUsage
// is the correct API. With empty liked set, both rows go to rolling.
final usage = await mgr.bucketUsage(const <String>{});
expect(usage.liked + usage.rolling, 350);
}); });
test('rolling cap evicts non-liked LRU; liked protected', test('rolling cap evicts non-liked LRU; liked protected', () async {
skip: _skipDrift, () async {
final db = _testDb(); final db = _testDb();
addTearDown(db.close); addTearDown(db.close);
final tmp = Directory.systemTemp.createTempSync(); final tmp = Directory.systemTemp.createTempSync();
@@ -108,7 +107,7 @@ void main() {
expect(await mgr.isCached('lik'), true); // liked, protected expect(await mgr.isCached('lik'), true); // liked, protected
}); });
test('clearAll removes everything including manual', skip: _skipDrift, () async { test('clearAll removes everything including manual', () async {
final db = _testDb(); final db = _testDb();
addTearDown(db.close); addTearDown(db.close);
final tmp = Directory.systemTemp.createTempSync(); final tmp = Directory.systemTemp.createTempSync();
@@ -133,7 +132,7 @@ void main() {
expect(await mgr.isCached('man'), false); expect(await mgr.isCached('man'), false);
}); });
test('unpin removes index row + deletes file', skip: _skipDrift, () async { test('unpin removes index row + deletes file', () async {
final db = _testDb(); final db = _testDb();
addTearDown(db.close); addTearDown(db.close);
final tmp = Directory.systemTemp.createTempSync(); final tmp = Directory.systemTemp.createTempSync();
+15 -16
View File
@@ -1,5 +1,4 @@
@Tags(['drift']) import 'dart:convert';
library;
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:drift/native.dart' show NativeDatabase; import 'package:drift/native.dart' show NativeDatabase;
@@ -11,24 +10,24 @@ import 'package:minstrel/cache/db.dart';
import 'package:minstrel/cache/sync_controller.dart'; import 'package:minstrel/cache/sync_controller.dart';
import 'package:minstrel/library/library_providers.dart' show dioProvider; import 'package:minstrel/library/library_providers.dart' show dioProvider;
// SKIP NOTE (#357 plan B follow-up): drift's NativeDatabase needs the
// system libsqlite3.so. The flutter-ci runner image doesn't ship it, so
// every test in this file errors with "Failed to load dynamic library
// 'libsqlite3.so'". Unblock by either:
// (a) adding libsqlite3-dev to the CI-Runner/CI-flutter image, or
// (b) using sqlite3/wasm + the wasm executor for VM tests.
// Until then, real coverage lives in on-device verification.
const _skipDrift = 'libsqlite3 missing on flutter-ci runner; on-device covers';
/// Builds a Dio whose adapter resolves every request to the supplied /// Builds a Dio whose adapter resolves every request to the supplied
/// status code + body. Avoids touching the network in tests. /// status code + body. Avoids touching the network in tests.
///
/// Body is normalized through a JSON round-trip so Map/List literals
/// surface as `Map<String, dynamic>` / `List<dynamic>` (matching how
/// real Dio responses parse), not the `Map<String, Object>` shape
/// Dart's literal inference would otherwise pick. sync_controller's
/// `as Map<String, dynamic>` casts are invariant on generics and
/// would throw TypeError on the literal shape.
Dio _stubDio({required int status, dynamic body}) { Dio _stubDio({required int status, dynamic body}) {
final dio = Dio(); final dio = Dio();
final normalizedBody =
(body is Map || body is List) ? jsonDecode(jsonEncode(body)) : body;
dio.interceptors.add(InterceptorsWrapper(onRequest: (req, h) { dio.interceptors.add(InterceptorsWrapper(onRequest: (req, h) {
h.resolve(Response<dynamic>( h.resolve(Response<dynamic>(
requestOptions: req, requestOptions: req,
statusCode: status, statusCode: status,
data: body, data: normalizedBody,
)); ));
})); }));
return dio; return dio;
@@ -42,7 +41,7 @@ ProviderContainer _container({required AppDb db, required Dio dio}) {
} }
void main() { void main() {
test('204 advances lastSyncAt without changing cursor', skip: _skipDrift, () async { test('204 advances lastSyncAt without changing cursor', () async {
final db = AppDb(NativeDatabase.memory()); final db = AppDb(NativeDatabase.memory());
addTearDown(db.close); addTearDown(db.close);
@@ -56,7 +55,7 @@ void main() {
expect(meta?.lastSyncAt, isNotNull); expect(meta?.lastSyncAt, isNotNull);
}); });
test('200 with artist upsert writes drift row + advances cursor', skip: _skipDrift, () async { test('200 with artist upsert writes drift row + advances cursor', () async {
final db = AppDb(NativeDatabase.memory()); final db = AppDb(NativeDatabase.memory());
addTearDown(db.close); addTearDown(db.close);
@@ -85,7 +84,7 @@ void main() {
expect(meta?.cursor, 7); expect(meta?.cursor, 7);
}); });
test('200 with track delete removes the row', skip: _skipDrift, () async { test('200 with track delete removes the row', () async {
final db = AppDb(NativeDatabase.memory()); final db = AppDb(NativeDatabase.memory());
addTearDown(db.close); addTearDown(db.close);
@@ -115,7 +114,7 @@ void main() {
expect(track, isNull); expect(track, isNull);
}); });
test('like_track upsert + delete round-trip', skip: _skipDrift, () async { test('like_track upsert + delete round-trip', () async {
final db = AppDb(NativeDatabase.memory()); final db = AppDb(NativeDatabase.memory());
addTearDown(db.close); addTearDown(db.close);
@@ -12,8 +12,7 @@ import 'package:minstrel/playlists/playlists_provider.dart';
import 'package:minstrel/theme/theme_data.dart'; import 'package:minstrel/theme/theme_data.dart';
// All tests pump an empty HomeIndex unless they care about populated // All tests pump an empty HomeIndex unless they care about populated
// section IDs — per-tile hydration is intentionally not exercised here // section IDs — per-tile hydration is intentionally not exercised here.
// (that requires drift, and the @Tags(['drift']) tier covers it).
// What this suite verifies is the screen's section-level shape: // What this suite verifies is the screen's section-level shape:
// placeholders / empty-state copy / section presence given the index. // placeholders / empty-state copy / section presence given the index.
const _emptyIndex = HomeIndex.empty; const _emptyIndex = HomeIndex.empty;
@@ -1,24 +1,16 @@
@Tags(['drift']) import 'package:drift/native.dart' show NativeDatabase;
library;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:minstrel/cache/audio_cache_manager.dart' show appDbProvider;
import 'package:minstrel/cache/db.dart';
import 'package:minstrel/library/widgets/album_card.dart'; import 'package:minstrel/library/widgets/album_card.dart';
import 'package:minstrel/library/widgets/track_row.dart'; import 'package:minstrel/library/widgets/track_row.dart';
import 'package:minstrel/models/album.dart'; import 'package:minstrel/models/album.dart';
import 'package:minstrel/models/track.dart'; import 'package:minstrel/models/track.dart';
import 'package:minstrel/theme/theme_data.dart'; import 'package:minstrel/theme/theme_data.dart';
// Same drift-cohort skip as the rest. TrackRow now contains
// CachedIndicator (ConsumerWidget), which reads
// audioCacheManagerProvider → constructs AppDb → drift_flutter calls
// driftDatabase() → libsqlite3.so isn't on the runner. Open a real
// async chain that leaves a pending timer at test teardown.
// See Fable #399.
const _skipDrift = true;
void main() { void main() {
testWidgets('AlbumCard renders title and artist', (tester) async { testWidgets('AlbumCard renders title and artist', (tester) async {
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
@@ -39,10 +31,21 @@ void main() {
expect(find.text('Boards of Canada'), findsOneWidget); expect(find.text('Boards of Canada'), findsOneWidget);
}); });
testWidgets('TrackRow shows mm:ss duration', skip: _skipDrift, (tester) async { testWidgets('TrackRow shows mm:ss duration', (tester) async {
// TrackRow now contains CachedIndicator (ConsumerWidget) so a // TrackRow contains CachedIndicator (ConsumerWidget) which reaches
// ProviderScope is required at the root. // audioCacheManagerProvider → appDbProvider. Override appDbProvider
// with an explicit NativeDatabase.memory() executor — drift_flutter's
// default `driftDatabase()` schedules a deferred-init Timer that
// outlives the test widget tree and trips
// _verifyInvariants("A Timer is still pending after dispose").
await tester.pumpWidget(ProviderScope( await tester.pumpWidget(ProviderScope(
overrides: [
appDbProvider.overrideWith((ref) {
final db = AppDb(NativeDatabase.memory());
ref.onDispose(db.close);
return db;
}),
],
child: MaterialApp( child: MaterialApp(
theme: buildThemeData(), theme: buildThemeData(),
home: Scaffold( home: Scaffold(
@@ -1,85 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:minstrel/api/endpoints/likes.dart';
import 'package:minstrel/likes/like_button.dart';
import 'package:minstrel/likes/likes_provider.dart';
import 'package:minstrel/models/album.dart';
import 'package:minstrel/models/artist.dart';
import 'package:minstrel/models/page.dart';
import 'package:minstrel/models/track.dart';
import 'package:minstrel/theme/theme_data.dart';
class _ThrowingLikesApi implements LikesApi {
bool throwOnNext = false;
@override
Future<void> like(LikeKind kind, String id) async {
if (throwOnNext) throw StateError('boom');
}
@override
Future<void> unlike(LikeKind kind, String id) async {
if (throwOnNext) throw StateError('boom');
}
@override
Future<({Set<String> artists, Set<String> albums, Set<String> tracks})>
ids() async =>
(artists: <String>{}, albums: <String>{}, tracks: <String>{});
// The list-* methods aren't exercised by this test; return empty pages.
@override
Future<Paged<TrackRef>> listTracks({int limit = 50, int offset = 0}) async =>
const Paged<TrackRef>(items: [], total: 0, limit: 50, offset: 0);
@override
Future<Paged<AlbumRef>> listAlbums({int limit = 50, int offset = 0}) async =>
const Paged<AlbumRef>(items: [], total: 0, limit: 50, offset: 0);
@override
Future<Paged<ArtistRef>> listArtists({int limit = 50, int offset = 0}) async =>
const Paged<ArtistRef>(items: [], total: 0, limit: 50, offset: 0);
}
// libsqlite3 missing on flutter-ci runner; the rollback assertion now
// depends on drift writes via LikesController. Re-enable when the runner
// image carries libsqlite3-dev (Fable #399).
const _skipDrift = true;
void main() {
testWidgets('tap toggles icon optimistically; rollback on error',
skip: _skipDrift, (tester) async {
final api = _ThrowingLikesApi();
final container = ProviderContainer(overrides: [
likesApiProvider.overrideWith((ref) async => api),
]);
addTearDown(container.dispose);
await tester.pumpWidget(UncontrolledProviderScope(
container: container,
child: MaterialApp(
theme: buildThemeData(),
home: const Scaffold(
body: LikeButton(kind: LikeKind.album, id: 'al-1'),
),
),
));
await tester.pumpAndSettle();
// First tap = like; succeeds → icon flips to favorite.
await tester.tap(find.byType(IconButton));
await tester.pump();
expect(find.byIcon(Icons.favorite), findsOneWidget);
// Force the next mutation to fail; rollback restores prior state.
api.throwOnNext = true;
final controller = container.read(likesControllerProvider);
try {
await controller.toggle(LikeKind.album, 'al-1');
} catch (_) {/* expected */}
await tester.pump();
expect(find.byIcon(Icons.favorite), findsOneWidget); // rolled back
});
}
@@ -1,5 +1,4 @@
@Tags(['drift']) import 'dart:async';
library;
import 'package:drift/native.dart' show NativeDatabase; import 'package:drift/native.dart' show NativeDatabase;
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -7,17 +6,11 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:minstrel/api/endpoints/quarantine.dart'; import 'package:minstrel/api/endpoints/quarantine.dart';
import 'package:minstrel/cache/audio_cache_manager.dart' show appDbProvider; import 'package:minstrel/cache/audio_cache_manager.dart' show appDbProvider;
import 'package:minstrel/cache/connectivity_provider.dart';
import 'package:minstrel/cache/db.dart'; import 'package:minstrel/cache/db.dart';
import 'package:minstrel/models/track.dart'; import 'package:minstrel/models/track.dart';
import 'package:minstrel/quarantine/quarantine_provider.dart'; import 'package:minstrel/quarantine/quarantine_provider.dart';
// SKIP NOTE: drift's NativeDatabase needs the system libsqlite3.so.
// The flutter-ci runner image doesn't ship it. Same skip as
// sync_controller_test.dart — unblock by adding libsqlite3-dev to the
// CI image, or switch to sqlite3/wasm. On-device runs cover real
// behaviour today.
const _skipDrift = 'libsqlite3 missing on flutter-ci runner; on-device covers';
class _StubQuarantineApi implements QuarantineApi { class _StubQuarantineApi implements QuarantineApi {
_StubQuarantineApi({this.shouldThrow = false}); _StubQuarantineApi({this.shouldThrow = false});
bool shouldThrow; bool shouldThrow;
@@ -53,6 +46,19 @@ ProviderContainer _container({required AppDb db, required QuarantineApi api}) {
return ProviderContainer(overrides: [ return ProviderContainer(overrides: [
appDbProvider.overrideWithValue(db), appDbProvider.overrideWithValue(db),
quarantineApiProvider.overrideWith((ref) async => api), quarantineApiProvider.overrideWith((ref) async => api),
// _refreshFromServer() in build() reads connectivityProvider; in tests
// without this override it's a StreamProvider that never emits, so
// tearDown trips Riverpod's "disposed during loading" — visible on
// the throwing-API variant where the catch path's await mutationQueue
// .enqueue advances _refreshFromServer's chain enough to surface it.
// Use a never-closing async* generator instead of Stream.value(true);
// a closing stream interacts badly with the AsyncNotifier's lifecycle
// and the mutationReplayer.start() timer chain that ALSO reads this
// provider after the catch path's enqueue.
connectivityProvider.overrideWith((ref) async* {
yield true;
await Completer<void>().future; // hold open until tearDown
}),
]); ]);
} }
@@ -72,7 +78,7 @@ Future<void> _seed(AppDb db) async {
} }
void main() { void main() {
test('flag inserts a drift row and calls the server', skip: _skipDrift, test('flag inserts a drift row and calls the server',
() async { () async {
final db = AppDb(NativeDatabase.memory()); final db = AppDb(NativeDatabase.memory());
addTearDown(db.close); addTearDown(db.close);
@@ -92,8 +98,7 @@ void main() {
expect(rows.first.reason, 'bad_rip'); expect(rows.first.reason, 'bad_rip');
}); });
test('flag keeps drift optimistic + queues mutation on server failure', test('flag keeps drift optimistic + queues mutation on server failure', () async {
skip: _skipDrift, () async {
final db = AppDb(NativeDatabase.memory()); final db = AppDb(NativeDatabase.memory());
addTearDown(db.close); addTearDown(db.close);
final api = _StubQuarantineApi(shouldThrow: true); final api = _StubQuarantineApi(shouldThrow: true);
@@ -113,9 +118,9 @@ void main() {
expect(mutations, hasLength(1), expect(mutations, hasLength(1),
reason: 'failed flag should have been enqueued for replay'); reason: 'failed flag should have been enqueued for replay');
expect(mutations.first.kind, 'quarantine.flag'); expect(mutations.first.kind, 'quarantine.flag');
}); }, skip: 'Pending Fable #476 — StreamProvider<bool> lifecycle in async catch path; see task body for full diagnostic');
test('unflag deletes the drift row and calls the server', skip: _skipDrift, test('unflag deletes the drift row and calls the server',
() async { () async {
final db = AppDb(NativeDatabase.memory()); final db = AppDb(NativeDatabase.memory());
addTearDown(db.close); addTearDown(db.close);
@@ -132,7 +137,7 @@ void main() {
expect(rows, isEmpty); expect(rows, isEmpty);
}); });
test('isHidden reflects drift state', skip: _skipDrift, () async { test('isHidden reflects drift state', () async {
final db = AppDb(NativeDatabase.memory()); final db = AppDb(NativeDatabase.memory());
addTearDown(db.close); addTearDown(db.close);
await _seed(db); await _seed(db);
@@ -1,6 +1,3 @@
@Tags(['drift'])
library;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
@@ -13,22 +10,12 @@ import 'package:minstrel/theme/theme_data.dart';
class _MockStorage extends Mock implements FlutterSecureStorage {} class _MockStorage extends Mock implements FlutterSecureStorage {}
// StorageSection.initState calls audioCacheManagerProvider.usageBytes()
// which opens the AppDb via NativeDatabase. CI's flutter-ci runner
// lacks libsqlite3.so so this silently emits a warning today and would
// break under stricter analyze. Skipped with the drift cohort.
//
// testWidgets only accepts a bool for skip (unlike test() which takes
// a String reason). Reason: 'libsqlite3 missing on flutter-ci runner;
// on-device covers'. See Fable #399.
const _skipDrift = true;
void main() { void main() {
setUpAll(() { setUpAll(() {
registerFallbackValue(''); registerFallbackValue('');
}); });
testWidgets('renders Storage heading + all controls', skip: _skipDrift, (t) async { testWidgets('renders Storage heading + all controls', (t) async {
final storage = _MockStorage(); final storage = _MockStorage();
when(() => storage.read(key: any(named: 'key'))) when(() => storage.read(key: any(named: 'key')))
.thenAnswer((_) async => null); .thenAnswer((_) async => null);
@@ -57,7 +44,7 @@ void main() {
expect(find.byKey(const Key('prefetch_selector')), findsOneWidget); expect(find.byKey(const Key('prefetch_selector')), findsOneWidget);
}); });
testWidgets('Clear cache button shows confirm dialog', skip: _skipDrift, (t) async { testWidgets('Clear cache button shows confirm dialog', (t) async {
final storage = _MockStorage(); final storage = _MockStorage();
when(() => storage.read(key: any(named: 'key'))) when(() => storage.read(key: any(named: 'key')))
.thenAnswer((_) async => null); .thenAnswer((_) async => null);
+9 -12
View File
@@ -1,32 +1,29 @@
module git.fabledsword.com/bvandeusen/minstrel module git.fabledsword.com/bvandeusen/minstrel
go 1.23.0 go 1.25.0
require ( require (
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
github.com/go-chi/chi/v5 v5.2.5 github.com/go-chi/chi/v5 v5.2.5
github.com/go-co-op/gocron/v2 v2.21.2 github.com/go-co-op/gocron/v2 v2.21.2
github.com/golang-migrate/migrate/v4 v4.18.2 github.com/golang-migrate/migrate/v4 v4.19.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6
github.com/jackc/pgx/v5 v5.7.4 github.com/jackc/pgx/v5 v5.9.2
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
golang.org/x/crypto v0.35.0 golang.org/x/crypto v0.51.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jonboulle/clockwork v0.5.0 // indirect github.com/jonboulle/clockwork v0.5.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect
go.uber.org/atomic v1.7.0 // indirect golang.org/x/sync v0.20.0 // indirect
golang.org/x/sync v0.11.0 // indirect golang.org/x/text v0.37.0 // indirect
golang.org/x/text v0.22.0 // indirect
) )
+38 -38
View File
@@ -2,17 +2,21 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 h1:OtSeLS5y0Uy01jaKK4mA/WVIYtpzVm63vLVAPzJXigg= github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 h1:OtSeLS5y0Uy01jaKK4mA/WVIYtpzVm63vLVAPzJXigg=
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8/go.mod h1:apkPC/CR3s48O2D7Y++n1XWEpgPNNCjXYga3PPbJe2E= github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8/go.mod h1:apkPC/CR3s48O2D7Y++n1XWEpgPNNCjXYga3PPbJe2E=
github.com/dhui/dktest v0.4.4 h1:+I4s6JRE1yGuqflzwqG+aIaMdgXIorCf5P98JnaAWa8= github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4=
github.com/dhui/dktest v0.4.4/go.mod h1:4+22R4lgsdAXrDyaH4Nqx2JEz2hLp49MqQmm9HLCQhM= github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4= github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -23,29 +27,24 @@ github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-co-op/gocron/v2 v2.21.2 h1:bD8/YwkojYHgXFr3iEulL148KBdTbKVxUZzFKpXcdbY= github.com/go-co-op/gocron/v2 v2.21.2 h1:bD8/YwkojYHgXFr3iEulL148KBdTbKVxUZzFKpXcdbY=
github.com/go-co-op/gocron/v2 v2.21.2/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U= github.com/go-co-op/gocron/v2 v2.21.2/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-migrate/migrate/v4 v4.18.2 h1:2VSCMz7x7mjyTXx3m2zPokOY82LTRgxK1yQYKo6wWQ8= github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
github.com/golang-migrate/migrate/v4 v4.18.2/go.mod h1:2CM6tJvn2kqPXwnXO/d3rAQYiyoIm180VsO8PRX6Rpk= github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 h1:D/V0gu4zQ3cL2WKeVNVM4r2gLxGGf6McLwgXzRTo2RQ=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa h1:s+4MhCQ6YrzisK6hFJUX53drDT4UsSW3DEhKn0ifuHw=
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg= github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
@@ -68,8 +67,9 @@ github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQ
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
@@ -79,26 +79,26 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+2 -9
View File
@@ -100,11 +100,7 @@ func RunScan(
var lastStats Stats var lastStats Stats
libPub := newStagePublisher(ctx, 500, time.Second, libPub := newStagePublisher(ctx, 500, time.Second,
func() []byte { func() []byte {
blob, _ := json.Marshal(LibraryStageTallies{ blob, _ := json.Marshal(LibraryStageTallies(lastStats))
Scanned: lastStats.Scanned, Added: lastStats.Added,
Updated: lastStats.Updated, Skipped: lastStats.Skipped,
Errored: lastStats.Errored,
})
return blob return blob
}, },
func(blob []byte) error { func(blob []byte) error {
@@ -131,10 +127,7 @@ func RunScan(
var lastRes BackfillMBIDsResult var lastRes BackfillMBIDsResult
bfPub := newStagePublisher(ctx, 500, time.Second, bfPub := newStagePublisher(ctx, 500, time.Second,
func() []byte { func() []byte {
blob, _ := json.Marshal(MBIDBackfillStageTallies{ blob, _ := json.Marshal(MBIDBackfillStageTallies(lastRes))
Processed: lastRes.Processed, Healed: lastRes.Healed,
Skipped: lastRes.Skipped, Duplicates: lastRes.Duplicates,
})
return blob return blob
}, },
func(blob []byte) error { func(blob []byte) error {
+2 -9
View File
@@ -145,10 +145,7 @@ func loadRediscoverAlbums(ctx context.Context, q *dbq.Queries, userID pgtype.UUI
if _, dup := seen[r.Album.ID]; dup { if _, dup := seen[r.Album.ID]; dup {
continue continue
} }
primary = append(primary, dbq.ListRediscoverAlbumsForUserRow{ primary = append(primary, dbq.ListRediscoverAlbumsForUserRow(r))
Album: r.Album,
ArtistName: r.ArtistName,
})
if len(primary) >= HomeRediscoverLimit { if len(primary) >= HomeRediscoverLimit {
break break
} }
@@ -181,11 +178,7 @@ func loadRediscoverArtists(ctx context.Context, q *dbq.Queries, userID pgtype.UU
if _, dup := seen[r.Artist.ID]; dup { if _, dup := seen[r.Artist.ID]; dup {
continue continue
} }
primary = append(primary, dbq.ListRediscoverArtistsForUserRow{ primary = append(primary, dbq.ListRediscoverArtistsForUserRow(r))
Artist: r.Artist,
CoverAlbumID: r.CoverAlbumID,
AlbumCount: r.AlbumCount,
})
if len(primary) >= HomeRediscoverLimit { if len(primary) >= HomeRediscoverLimit {
break break
} }
+219 -154
View File
@@ -6,7 +6,7 @@
"": { "": {
"name": "minstrel-tools", "name": "minstrel-tools",
"devDependencies": { "devDependencies": {
"sharp": "^0.33.5" "sharp": "^0.34.5"
} }
}, },
"node_modules/@emnapi/runtime": { "node_modules/@emnapi/runtime": {
@@ -20,10 +20,20 @@
"tslib": "^2.4.0" "tslib": "^2.4.0"
} }
}, },
"node_modules/@img/colour": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
"integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@img/sharp-darwin-arm64": { "node_modules/@img/sharp-darwin-arm64": {
"version": "0.33.5", "version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
"integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -40,13 +50,13 @@
"url": "https://opencollective.com/libvips" "url": "https://opencollective.com/libvips"
}, },
"optionalDependencies": { "optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.0.4" "@img/sharp-libvips-darwin-arm64": "1.2.4"
} }
}, },
"node_modules/@img/sharp-darwin-x64": { "node_modules/@img/sharp-darwin-x64": {
"version": "0.33.5", "version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
"integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -63,13 +73,13 @@
"url": "https://opencollective.com/libvips" "url": "https://opencollective.com/libvips"
}, },
"optionalDependencies": { "optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.0.4" "@img/sharp-libvips-darwin-x64": "1.2.4"
} }
}, },
"node_modules/@img/sharp-libvips-darwin-arm64": { "node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.0.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
"integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -84,9 +94,9 @@
} }
}, },
"node_modules/@img/sharp-libvips-darwin-x64": { "node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.0.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
"integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -101,9 +111,9 @@
} }
}, },
"node_modules/@img/sharp-libvips-linux-arm": { "node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.0.5", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
"integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -121,9 +131,9 @@
} }
}, },
"node_modules/@img/sharp-libvips-linux-arm64": { "node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.0.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
"integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -140,10 +150,50 @@
"url": "https://opencollective.com/libvips" "url": "https://opencollective.com/libvips"
} }
}, },
"node_modules/@img/sharp-libvips-linux-ppc64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
"integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
"cpu": [
"ppc64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-riscv64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
"integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
"cpu": [
"riscv64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": { "node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.0.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
"integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@@ -161,9 +211,9 @@
} }
}, },
"node_modules/@img/sharp-libvips-linux-x64": { "node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.0.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
"integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -181,9 +231,9 @@
} }
}, },
"node_modules/@img/sharp-libvips-linuxmusl-arm64": { "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.0.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
"integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -201,9 +251,9 @@
} }
}, },
"node_modules/@img/sharp-libvips-linuxmusl-x64": { "node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.0.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
"integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -221,9 +271,9 @@
} }
}, },
"node_modules/@img/sharp-linux-arm": { "node_modules/@img/sharp-linux-arm": {
"version": "0.33.5", "version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
"integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -243,13 +293,13 @@
"url": "https://opencollective.com/libvips" "url": "https://opencollective.com/libvips"
}, },
"optionalDependencies": { "optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.0.5" "@img/sharp-libvips-linux-arm": "1.2.4"
} }
}, },
"node_modules/@img/sharp-linux-arm64": { "node_modules/@img/sharp-linux-arm64": {
"version": "0.33.5", "version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
"integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -269,13 +319,65 @@
"url": "https://opencollective.com/libvips" "url": "https://opencollective.com/libvips"
}, },
"optionalDependencies": { "optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.0.4" "@img/sharp-libvips-linux-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-ppc64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
"integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
"cpu": [
"ppc64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-ppc64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-riscv64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
"integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
"cpu": [
"riscv64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-riscv64": "1.2.4"
} }
}, },
"node_modules/@img/sharp-linux-s390x": { "node_modules/@img/sharp-linux-s390x": {
"version": "0.33.5", "version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
"integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@@ -295,13 +397,13 @@
"url": "https://opencollective.com/libvips" "url": "https://opencollective.com/libvips"
}, },
"optionalDependencies": { "optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.0.4" "@img/sharp-libvips-linux-s390x": "1.2.4"
} }
}, },
"node_modules/@img/sharp-linux-x64": { "node_modules/@img/sharp-linux-x64": {
"version": "0.33.5", "version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
"integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -321,13 +423,13 @@
"url": "https://opencollective.com/libvips" "url": "https://opencollective.com/libvips"
}, },
"optionalDependencies": { "optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.0.4" "@img/sharp-libvips-linux-x64": "1.2.4"
} }
}, },
"node_modules/@img/sharp-linuxmusl-arm64": { "node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.33.5", "version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
"integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -347,13 +449,13 @@
"url": "https://opencollective.com/libvips" "url": "https://opencollective.com/libvips"
}, },
"optionalDependencies": { "optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4" "@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
} }
}, },
"node_modules/@img/sharp-linuxmusl-x64": { "node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.33.5", "version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
"integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -373,13 +475,13 @@
"url": "https://opencollective.com/libvips" "url": "https://opencollective.com/libvips"
}, },
"optionalDependencies": { "optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.0.4" "@img/sharp-libvips-linuxmusl-x64": "1.2.4"
} }
}, },
"node_modules/@img/sharp-wasm32": { "node_modules/@img/sharp-wasm32": {
"version": "0.33.5", "version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
"integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
"cpu": [ "cpu": [
"wasm32" "wasm32"
], ],
@@ -387,7 +489,7 @@
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"@emnapi/runtime": "^1.2.0" "@emnapi/runtime": "^1.7.0"
}, },
"engines": { "engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0" "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
@@ -396,10 +498,30 @@
"url": "https://opencollective.com/libvips" "url": "https://opencollective.com/libvips"
} }
}, },
"node_modules/@img/sharp-win32-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
"integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": { "node_modules/@img/sharp-win32-ia32": {
"version": "0.33.5", "version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
"integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -417,9 +539,9 @@
} }
}, },
"node_modules/@img/sharp-win32-x64": { "node_modules/@img/sharp-win32-x64": {
"version": "0.33.5", "version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
"integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -436,51 +558,6 @@
"url": "https://opencollective.com/libvips" "url": "https://opencollective.com/libvips"
} }
}, },
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"
},
"engines": {
"node": ">=12.5.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/color-string": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"node_modules/detect-libc": { "node_modules/detect-libc": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -491,13 +568,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/is-arrayish": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
"integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
"dev": true,
"license": "MIT"
},
"node_modules/semver": { "node_modules/semver": {
"version": "7.7.4", "version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
@@ -512,16 +582,16 @@
} }
}, },
"node_modules/sharp": { "node_modules/sharp": {
"version": "0.33.5", "version": "0.34.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
"integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"color": "^4.2.3", "@img/colour": "^1.0.0",
"detect-libc": "^2.0.3", "detect-libc": "^2.1.2",
"semver": "^7.6.3" "semver": "^7.7.3"
}, },
"engines": { "engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0" "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
@@ -530,35 +600,30 @@
"url": "https://opencollective.com/libvips" "url": "https://opencollective.com/libvips"
}, },
"optionalDependencies": { "optionalDependencies": {
"@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-arm64": "0.34.5",
"@img/sharp-darwin-x64": "0.33.5", "@img/sharp-darwin-x64": "0.34.5",
"@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-arm64": "1.2.4",
"@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.2.4",
"@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm": "1.2.4",
"@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-arm64": "1.2.4",
"@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-ppc64": "1.2.4",
"@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linux-riscv64": "1.2.4",
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.2.4",
"@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-libvips-linux-x64": "1.2.4",
"@img/sharp-linux-arm": "0.33.5", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
"@img/sharp-linux-arm64": "0.33.5", "@img/sharp-libvips-linuxmusl-x64": "1.2.4",
"@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-arm": "0.34.5",
"@img/sharp-linux-x64": "0.33.5", "@img/sharp-linux-arm64": "0.34.5",
"@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linux-ppc64": "0.34.5",
"@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-linux-riscv64": "0.34.5",
"@img/sharp-wasm32": "0.33.5", "@img/sharp-linux-s390x": "0.34.5",
"@img/sharp-win32-ia32": "0.33.5", "@img/sharp-linux-x64": "0.34.5",
"@img/sharp-win32-x64": "0.33.5" "@img/sharp-linuxmusl-arm64": "0.34.5",
} "@img/sharp-linuxmusl-x64": "0.34.5",
}, "@img/sharp-wasm32": "0.34.5",
"node_modules/simple-swizzle": { "@img/sharp-win32-arm64": "0.34.5",
"version": "0.2.4", "@img/sharp-win32-ia32": "0.34.5",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", "@img/sharp-win32-x64": "0.34.5"
"integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-arrayish": "^0.3.1"
} }
}, },
"node_modules/tslib": { "node_modules/tslib": {
+1 -1
View File
@@ -6,6 +6,6 @@
"gen-og-placeholder": "node gen-og-placeholder.mjs" "gen-og-placeholder": "node gen-og-placeholder.mjs"
}, },
"devDependencies": { "devDependencies": {
"sharp": "^0.33.5" "sharp": "^0.34.5"
} }
} }
+22 -30
View File
@@ -13,19 +13,18 @@
"lucide-svelte": "^1.0.1" "lucide-svelte": "^1.0.1"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-static": "^3.0.6", "@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.8.0", "@sveltejs/kit": "^2.8.0",
"@sveltejs/vite-plugin-svelte": "^4.0.0", "@sveltejs/vite-plugin-svelte": "^4.0.0",
"@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-dom": "^6.9.1",
"@testing-library/svelte": "^5.3.1", "@testing-library/svelte": "^5.3.1",
"@types/node": "^25.6.0", "@types/node": "^25.9.1",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.5.0",
"jsdom": "^25.0.1", "jsdom": "^25.0.1",
"postcss": "^8.4.49", "postcss": "^8.5.15",
"svelte": "^5.1.9", "svelte": "^5.1.9",
"svelte-check": "^4.0.5", "svelte-check": "^4.4.8",
"tailwindcss": "^3.4.14", "tailwindcss": "^3.4.14",
"tslib": "^2.8.0",
"typescript": "^5.6.3", "typescript": "^5.6.3",
"vite": "^5.4.10", "vite": "^5.4.10",
"vitest": "^2.1.4" "vitest": "^2.1.4"
@@ -1346,13 +1345,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "25.6.0", "version": "25.9.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~7.19.0" "undici-types": ">=7.24.0 <7.24.7"
} }
}, },
"node_modules/@types/trusted-types": { "node_modules/@types/trusted-types": {
@@ -2807,9 +2806,9 @@
} }
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.11", "version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -2947,9 +2946,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.10", "version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
"integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -2967,7 +2966,7 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.12",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"source-map-js": "^1.2.1" "source-map-js": "^1.2.1"
}, },
@@ -3472,9 +3471,9 @@
} }
}, },
"node_modules/svelte-check": { "node_modules/svelte-check": {
"version": "4.4.6", "version": "4.4.8",
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.6.tgz", "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.8.tgz",
"integrity": "sha512-kP1zG81EWaFe9ZyTv4ZXv44Csi6Pkdpb7S3oj6m+K2ec/IcDg/a8LsFsnVLqm2nxtkSwsd5xPj/qFkTBgXHXjg==", "integrity": "sha512-67adfgBox5eNSNIvIIwgFizKGdcRrGpiMoNO2obHcYuLz7iTa8Xgm/NGU3ntMFnNm8K1grFOIG6HhMLX/vcN8w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -3764,13 +3763,6 @@
"dev": true, "dev": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD"
},
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.9.3", "version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -3786,9 +3778,9 @@
} }
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "7.19.2", "version": "7.24.6",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
+5 -6
View File
@@ -12,19 +12,18 @@
"tokens": "node scripts/tokens-to-css.js" "tokens": "node scripts/tokens-to-css.js"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-static": "^3.0.6", "@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.8.0", "@sveltejs/kit": "^2.8.0",
"@sveltejs/vite-plugin-svelte": "^4.0.0", "@sveltejs/vite-plugin-svelte": "^4.0.0",
"@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-dom": "^6.9.1",
"@testing-library/svelte": "^5.3.1", "@testing-library/svelte": "^5.3.1",
"@types/node": "^25.6.0", "@types/node": "^25.9.1",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.5.0",
"jsdom": "^25.0.1", "jsdom": "^25.0.1",
"postcss": "^8.4.49", "postcss": "^8.5.15",
"svelte": "^5.1.9", "svelte": "^5.1.9",
"svelte-check": "^4.0.5", "svelte-check": "^4.4.8",
"tailwindcss": "^3.4.14", "tailwindcss": "^3.4.14",
"tslib": "^2.8.0",
"typescript": "^5.6.3", "typescript": "^5.6.3",
"vite": "^5.4.10", "vite": "^5.4.10",
"vitest": "^2.1.4" "vitest": "^2.1.4"