Compare commits

...

5 Commits

Author SHA1 Message Date
bvandeusen 22a4649bfc Merge pull request 'v2026.05.19.1 — hotfix: notification permission + full-player auto-minimize' (#54) from dev into main
Merge v2026.05.19.1 hotfix — notification permission + full-player auto-minimize (PR #54)
2026-05-18 23:08:33 -04:00
bvandeusen 01a1ac148e chore(flutter): bump version to 2026.5.19+11 for v2026.05.19.1
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:08:13 -04:00
bvandeusen bc34d96329 fix(player): request POST_NOTIFICATIONS; auto-minimize player on teardown
Device-surfaced on physical Android 13+ (worked on emulator):

A) The media notification never appeared because the app never
   requested POST_NOTIFICATIONS at runtime — the manifest declares it
   and the foreground service is correct, but Android 13+ denies it by
   default until asked. Add permission_handler ^12.0.1 and request
   Permission.notification once at startup (post-first-frame,
   Platform.isAndroid-guarded; no-op on <13 / once decided).

B) When the #52 idle/dismiss teardown nulled mediaItem while the full
   NowPlayingScreen was open, it stranded the user on an empty
   "Nothing playing." Scaffold. Now post-frame maybePop() so it
   auto-minimizes (the mini bar is already gone).

pubspec.lock + db.g.dart regenerated by CI/build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:03:31 -04:00
bvandeusen 8e7660c05e Merge pull request 'fix(ci): scope integration Postgres discovery to this job's network' (#53) from dev into main
Merge CI fix: scope integration Postgres discovery to this job's network (PR #53)
2026-05-18 22:50:36 -04:00
bvandeusen eaddb2478a fix(ci): scope integration Postgres discovery to this job's network
`--filter name=integration` matched EVERY concurrent integration run's
Postgres service container. A dev push and the main-merge run overlap
on the shared act_runner daemon → 2 candidates → the "expected exactly
1" guard aborts (false failure; not a code defect).

Discover instead by intersecting networks: act_runner attaches the job
container and its service container to a shared per-job network, so
select the postgres that sits on a network this job container is also
on. The dev-compose container is skipped explicitly as before.

CI-only change; the released v2026.05.19.0 code is unaffected (a clean
re-run of the failed job passes — the failure was a concurrency race).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 22:50:04 -04:00
4 changed files with 48 additions and 14 deletions
+25 -13
View File
@@ -83,19 +83,31 @@ jobs:
run: |
set -eux
# Discover THIS job's Postgres service container via the
# mounted docker socket. Scope by the job-name filter so the
# operator's dev compose `minstrel-postgres-*` (same image,
# same daemon) can never match. Require exactly one — abort
# loudly otherwise; a wrong target would truncate real data.
PG_LIST=$(docker ps --filter "name=integration" --filter "ancestor=postgres:16-alpine" --format '{{.ID}} {{.Names}}')
echo "candidates: ${PG_LIST:-<none>}"
PG_COUNT=$(printf '%s\n' "$PG_LIST" | grep -c . || true)
test "$PG_COUNT" = "1" || { echo "FATAL: expected exactly 1 postgres service container, got $PG_COUNT"; exit 1; }
PG_ID=$(printf '%s' "$PG_LIST" | awk '{print $1}')
PG_NAME=$(printf '%s' "$PG_LIST" | awk '{print $2}')
case "$PG_NAME" in
*minstrel-postgres*|*_postgres_*) echo "FATAL: matched the dev compose container ($PG_NAME), refusing"; exit 1 ;;
esac
# mounted docker socket. act_runner attaches the job
# container and its service container(s) to a shared per-job
# network, so scope discovery to a postgres that sits on a
# network THIS job container is also on. The old
# `--filter name=integration` matched EVERY concurrent
# integration run's postgres (a dev push + the main-merge run
# overlap → 2 candidates → false "expected exactly 1" abort).
# The operator's dev compose `minstrel-postgres-*` is never on
# this job's network; skip it explicitly as belt-and-suspenders
# (a wrong target would truncate real data).
SELF=$(cat /etc/hostname)
SELF_NETS=$(docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' "$SELF")
test -n "$SELF_NETS"
echo "self ($SELF) networks: $SELF_NETS"
PG_ID=""
PG_NAME=""
for cid in $(docker ps --filter "ancestor=postgres:16-alpine" -q); do
nm=$(docker inspect -f '{{.Name}}' "$cid" | sed 's#^/##')
case "$nm" in *minstrel-postgres*|*_postgres_*) continue ;; esac
for net in $(docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' "$cid"); do
case " $SELF_NETS " in *" $net "*) PG_ID="$cid"; PG_NAME="$nm"; break 2 ;; esac
done
done
test -n "$PG_ID" || { echo "FATAL: no postgres service container on this job's network (self nets: $SELF_NETS)"; exit 1; }
echo "selected postgres: $PG_ID $PG_NAME"
PG_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$PG_ID")
test -n "$PG_IP"
export MINSTREL_TEST_DATABASE_URL="postgres://minstrel:minstrel@${PG_IP}:5432/minstrel_test?sslmode=disable"
+11
View File
@@ -1,5 +1,8 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:permission_handler/permission_handler.dart';
import 'cache/cache_filler.dart';
import 'cache/metadata_prefetcher.dart';
@@ -77,6 +80,14 @@ class _MinstrelAppState extends ConsumerState<MinstrelApp> {
// probe → offlineProvider. Read here to start the poller; S4
// gates system-playlist play + Shuffle-all on it.
ref.read(offlineProvider);
// POST_NOTIFICATIONS (Android 13+) is denied-by-default until
// requested; without it the media notification is silently
// suppressed on physical devices. One-shot, post-first-frame so
// it never blocks launch; no-op on <13 / once already decided.
if (Platform.isAndroid) {
// ignore: unawaited_futures
Permission.notification.request();
}
});
}
@@ -222,6 +222,14 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
_displayedDominant = null;
_pendingPreloadId = null;
});
// Session was torn down (#52 idle/dismiss) while the full
// player was open. Don't strand the user on an empty
// "Nothing playing." screen — minimize back (the mini bar is
// already gone too). maybePop is a no-op if this is somehow
// the root route.
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) Navigator.of(context).maybePop();
});
}
return;
}
+4 -1
View File
@@ -1,7 +1,7 @@
name: minstrel
description: Minstrel mobile client
publish_to: 'none'
version: 2026.5.19+10
version: 2026.5.19+11
environment:
sdk: '>=3.5.0 <4.0.0'
@@ -25,6 +25,9 @@ dependencies:
# Lucide icon set (design system mandates Lucide, not Material).
# Icons exposed as LucideIcons.<snake_case> IconData usable in Icon().
flutter_lucide: ^1.11.0
# Runtime POST_NOTIFICATIONS request (Android 13+ denies-by-default
# until asked; the media notification is suppressed without it).
permission_handler: ^12.0.1
google_fonts: ^8.1.0
# 10.x conflicts with flutter_secure_storage 10.x on win32. Hold at 8.3.1
# until either lib bumps win32 to 6.x.