From dbdd8536faae3811d01c34a3cf39205aefc81427 Mon Sep 17 00:00:00 2001
From: David Popescu <80603759+DavidPopesc@users.noreply.github.com>
Date: Sat, 22 Aug 2026 12:00:20 -0400
Subject: [PATCH 1/6] Update iOS deployment target to 15.0, configure analyzer
exclusions, and update package versions in pubspec.lock
---
analysis_options.yaml | 9 ++
ios/Runner.xcodeproj/project.pbxproj | 6 +-
ios/Runner/Info.plist | 173 +++++++++++++--------------
pubspec.lock | 16 +--
4 files changed, 106 insertions(+), 98 deletions(-)
diff --git a/analysis_options.yaml b/analysis_options.yaml
index 0d290213..bf8d4218 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -7,6 +7,15 @@
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
+analyzer:
+ exclude:
+ - build/**
+ - android/**
+ - ios/**
+ - web/**
+ - windows/**
+ - macos/**
+ - linux/**
include: package:flutter_lints/flutter.yaml
linter:
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index 3c09d15a..35c146ed 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -364,7 +364,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -494,7 +494,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -545,7 +545,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 6918f87a..8f6aba99 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -1,8 +1,8 @@
-
-
- FlutterDeepLinkingEnabled
-
- CADisableMinimumFrameDurationOnPhone
-
-
- CFBundleDevelopmentRegion
- $(DEVELOPMENT_LANGUAGE)
- CFBundleDisplayName
- DeFlock
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- deflockapp
- CFBundlePackageType
- APPL
- CFBundleShortVersionString
- $(FLUTTER_BUILD_NAME)
- CFBundleSignature
- ????
- CFBundleURLTypes
-
-
- CFBundleTypeRole
- None
- CFBundleURLSchemes
-
- deflockapp
-
-
-
- CFBundleVersion
- $(FLUTTER_BUILD_NUMBER)
- LSApplicationQueriesSchemes
-
- https
-
- LSRequiresIPhoneOS
-
- NSLocationAlwaysAndWhenInUseUsageDescription
- This app optionally uses your location to center the map on your current position and provide proximity alerts for nearby surveillance devices. These features are entirely optional.
- NSLocationWhenInUseUsageDescription
- This app optionally uses your location to show nearby cameras by centering the map on your location.
- UIApplicationSceneManifest
-
- UIApplicationSupportsMultipleScenes
+ FlutterDeepLinkingEnabled
- UISceneConfigurations
+ CADisableMinimumFrameDurationOnPhone
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ DeFlock
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ deflockapp
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleSignature
+ ????
+ CFBundleURLTypes
+
+
+ CFBundleTypeRole
+ None
+ CFBundleURLSchemes
+
+ deflockapp
+
+
+
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSApplicationQueriesSchemes
+
+ https
+
+ LSRequiresIPhoneOS
+
+ NSLocationAlwaysAndWhenInUseUsageDescription
+ This app optionally uses your location to center the map on your current position and provide proximity alerts for nearby surveillance devices. These features are entirely optional.
+ NSLocationWhenInUseUsageDescription
+ This app optionally uses your location to show nearby cameras by centering the map on your location.
+ UIApplicationSceneManifest
- UIWindowSceneSessionRoleApplication
-
-
- UISceneClassName
- UIWindowScene
- UISceneConfigurationName
- flutter
- UISceneDelegateClassName
- FlutterSceneDelegate
- UISceneStoryboardFile
- Main
-
-
+ UIApplicationSupportsMultipleScenes
+
+ UISceneConfigurations
+
+ UIWindowSceneSessionRoleApplication
+
+
+ UISceneClassName
+ UIWindowScene
+ UISceneConfigurationName
+ flutter
+ UISceneDelegateClassName
+ FlutterSceneDelegate
+ UISceneStoryboardFile
+ Main
+
+
+
+ UIApplicationSupportsIndirectInputEvents
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UIStatusBarHidden
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
- UIApplicationSupportsIndirectInputEvents
-
- UILaunchStoryboardName
- LaunchScreen
- UIMainStoryboardFile
- Main
- UIStatusBarHidden
-
- UISupportedInterfaceOrientations
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UISupportedInterfaceOrientations~ipad
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
-
diff --git a/pubspec.lock b/pubspec.lock
index e5cd22b2..c75e8e76 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -564,10 +564,10 @@ packages:
dependency: transitive
description:
name: matcher
- sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
+ sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd"
url: "https://pub.dev"
source: hosted
- version: "0.12.19"
+ version: "0.12.20"
material_color_utilities:
dependency: transitive
description:
@@ -580,10 +580,10 @@ packages:
dependency: transitive
description:
name: meta
- sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
+ sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9"
url: "https://pub.dev"
source: hosted
- version: "1.18.0"
+ version: "1.19.0"
mgrs_dart:
dependency: transitive
description:
@@ -945,10 +945,10 @@ packages:
dependency: transitive
description:
name: test_api
- sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
+ sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11"
url: "https://pub.dev"
source: hosted
- version: "0.7.11"
+ version: "0.7.12"
timezone:
dependency: transitive
description:
@@ -1081,10 +1081,10 @@ packages:
dependency: transitive
description:
name: vector_math
- sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
+ sha256: f36f9f3be64c6198714492bb455c11056e33e2f85d9a0b676a48301e44fdcf47
url: "https://pub.dev"
source: hosted
- version: "2.2.0"
+ version: "2.4.2"
vm_service:
dependency: transitive
description:
From 72b3d99a445f3f040d9b7b0a4bc91ce7bc1cfe0c Mon Sep 17 00:00:00 2001
From: David Popescu <80603759+DavidPopesc@users.noreply.github.com>
Date: Thu, 27 Aug 2026 16:13:41 -0400
Subject: [PATCH 2/6] Implement migration for Flock Raven tags and update
changelog for version 2.11.1
---
assets/changelog.json | 5 +
lib/app_state.dart | 15 +++
lib/migrations.dart | 141 +++++++++++++++++++++++++++
lib/models/node_profile.dart | 6 +-
lib/screens/about_screen.dart | 22 +++++
lib/screens/home_screen.dart | 8 +-
lib/services/changelog_service.dart | 6 +-
lib/services/overpass_service.dart | 38 ++++++++
lib/state/upload_queue_state.dart | 115 ++++++++++++++++++++++
pubspec.yaml | 2 +-
test/migrations_test.dart | 144 ++++++++++++++++++++++++++++
11 files changed, 496 insertions(+), 6 deletions(-)
create mode 100644 test/migrations_test.dart
diff --git a/assets/changelog.json b/assets/changelog.json
index 2e09f752..265871c0 100644
--- a/assets/changelog.json
+++ b/assets/changelog.json
@@ -1,4 +1,9 @@
{
+ "2.11.1": {
+ "content": [
+ "• Fix incorrect Flock Raven tags (surveillance/manufacturer) on existing saved entries"
+ ]
+ },
"2.11.0": {
"content": [
"• Further improve node loading performance",
diff --git a/lib/app_state.dart b/lib/app_state.dart
index 0ba485c7..224a29ce 100644
--- a/lib/app_state.dart
+++ b/lib/app_state.dart
@@ -616,6 +616,16 @@ class AppState extends ChangeNotifier {
}
}
+ /// Queue corrective uploads for the user's own live OSM nodes still using
+ /// the old Flock Raven tag scheme (for the one-time live-data fix prompt).
+ List addFlockRavenCorrections(List staleNodes) {
+ final corrections = _uploadQueueState.addFlockRavenCorrections(staleNodes, uploadMode: uploadMode);
+ if (corrections.isNotEmpty) {
+ _startUploader();
+ }
+ return corrections;
+ }
+
void deleteNode(OsmNode node, {String? changesetComment}) {
_uploadQueueState.addFromNodeDeletion(node, uploadMode: uploadMode, changesetComment: changesetComment);
_startUploader();
@@ -851,6 +861,11 @@ class AppState extends ChangeNotifier {
await _uploadQueueState.reloadQueue();
}
+ /// Fix queued Flock Raven entries still using the old tag scheme (for migration purposes)
+ Future migrateFlockRavenQueueTags() async {
+ return _uploadQueueState.migrateFlockRavenQueueTags();
+ }
+
// ---------- Suspected Location Methods ----------
Future setSuspectedLocationsEnabled(bool enabled) async {
await _suspectedLocationState.setEnabled(enabled);
diff --git a/lib/migrations.dart b/lib/migrations.dart
index 171e4eb4..3cec690e 100644
--- a/lib/migrations.dart
+++ b/lib/migrations.dart
@@ -4,6 +4,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'app_state.dart';
import 'services/offline_area_service.dart';
+import 'services/overpass_service.dart';
import 'services/profile_service.dart';
import 'services/suspected_location_cache.dart';
import 'widgets/nuclear_reset_dialog.dart';
@@ -215,6 +216,50 @@ class OneTimeMigrations {
}
}
+ /// Fix Flock Raven tags that were using the wrong surveillance scheme (v2.11.1)
+ /// Old (incorrect): 'surveillance': 'public', 'brand': 'Flock Safety', 'brand:wikidata': 'Q108485435'
+ /// New (correct): 'surveillance': 'outdoor', 'manufacturer': 'Flock Safety', 'manufacturer:wikidata': 'Q108485435'
+ static Future migrate_2_11_1(AppState appState) async {
+ try {
+ // Fix the persisted profile catalog (used when creating new nodes going forward)
+ final profiles = await ProfileService().load();
+ bool profilesChanged = false;
+
+ final updatedProfiles = profiles.map((profile) {
+ final tags = profile.tags;
+ if (tags['surveillance'] == 'public' &&
+ tags['brand'] == 'Flock Safety' &&
+ tags['surveillance:type'] == 'gunshot_detector') {
+ debugPrint('[Migration] 2.11.1: Fixing Flock Raven tags on profile: ${profile.id}');
+ final newTags = Map.from(tags);
+ final wikidata = newTags.remove('brand:wikidata');
+ newTags.remove('brand');
+ newTags['surveillance'] = 'outdoor';
+ newTags['manufacturer'] = 'Flock Safety';
+ newTags['manufacturer:wikidata'] = wikidata ?? 'Q108485435';
+ profilesChanged = true;
+ return profile.copyWith(tags: newTags);
+ }
+ return profile;
+ }).toList();
+
+ if (profilesChanged) {
+ await ProfileService().save(updatedProfiles);
+ await appState.reloadProfiles();
+ }
+
+ // Fix any not-yet-uploaded queue entries that snapshotted the old tags
+ final queueChanged = await appState.migrateFlockRavenQueueTags();
+
+ debugPrint('[Migration] 2.11.1 completed: fixed Flock Raven tags '
+ '(profiles changed=$profilesChanged, queue changed=$queueChanged)');
+ } catch (e, stackTrace) {
+ debugPrint('[Migration] 2.11.1 ERROR: Failed to fix Flock Raven tags: $e');
+ debugPrint('[Migration] 2.11.1 ERROR: Stack trace: $stackTrace');
+ // Don't rethrow - non-critical, worst case stale tags persist until manually fixed
+ }
+ }
+
/// Get the migration function for a specific version
static Future Function(AppState)? getMigrationForVersion(String version) {
switch (version) {
@@ -234,6 +279,8 @@ class OneTimeMigrations {
return migrate_2_10_0;
case '2.10.5':
return migrate_2_10_5;
+ case '2.11.1':
+ return migrate_2_11_1;
default:
return null;
}
@@ -262,4 +309,98 @@ class OneTimeMigrations {
}
}
}
+}
+
+/// One-time (not version-gated) scan of the logged-in user's own live OSM
+/// data for nodes still using the old Flock Raven tag scheme, offering to
+/// correct them. Runs at most once ever: skipped permanently once the user
+/// has answered the prompt, or once a scan finds nothing to fix. A failed
+/// scan (e.g. offline) is *not* marked as done, so it's retried on a later
+/// launch once conditions allow.
+class FlockRavenLiveDataFix {
+ static const _promptedKey = 'flock_raven_live_fix_prompted';
+
+ /// The old (incorrect) Flock Raven tag scheme this scan looks for.
+ static const Map _staleTags = {
+ 'man_made': 'surveillance',
+ 'surveillance': 'public',
+ 'surveillance:type': 'gunshot_detector',
+ 'brand': 'Flock Safety',
+ };
+
+ static Future checkAndPrompt(AppState appState, BuildContext? context) async {
+ try {
+ final prefs = await SharedPreferences.getInstance();
+ if (prefs.getBool(_promptedKey) == true) return;
+
+ if (!appState.isLoggedIn || appState.username.isEmpty) {
+ debugPrint('[FlockRavenLiveFix] Not logged in, will retry on a later launch');
+ return;
+ }
+
+ if (context == null || !context.mounted) {
+ debugPrint('[FlockRavenLiveFix] No context available, will retry on a later launch');
+ return;
+ }
+
+ debugPrint('[FlockRavenLiveFix] Scanning OSM for stale Flock Raven nodes owned by ${appState.username}');
+ final staleNodes = await OverpassService().fetchNodesByUserAndTags(
+ username: appState.username,
+ tagFilter: _staleTags,
+ );
+
+ if (staleNodes.isEmpty) {
+ debugPrint('[FlockRavenLiveFix] No stale nodes found, marking as done');
+ await prefs.setBool(_promptedKey, true);
+ return;
+ }
+
+ if (!context.mounted) return; // Re-check after the async scan
+
+ final count = staleNodes.length;
+ final shouldFix = await showDialog(
+ context: context,
+ barrierDismissible: false,
+ builder: (ctx) => AlertDialog(
+ title: const Text('Update Flock Raven tags?'),
+ content: Text(
+ 'Found $count Flock Raven ${count == 1 ? 'node' : 'nodes'} on your OpenStreetMap '
+ 'account using outdated tags. Update ${count == 1 ? 'it' : 'them'} to the correct tags now?',
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.of(ctx).pop(false),
+ child: const Text('No'),
+ ),
+ TextButton(
+ onPressed: () => Navigator.of(ctx).pop(true),
+ child: const Text('Yes'),
+ ),
+ ],
+ ),
+ );
+
+ if (shouldFix == true) {
+ final queued = appState.addFlockRavenCorrections(staleNodes);
+ debugPrint('[FlockRavenLiveFix] Queued ${queued.length} corrective uploads');
+ } else {
+ debugPrint('[FlockRavenLiveFix] User declined the live data fix');
+ }
+
+ // Never ask again, regardless of the answer.
+ await prefs.setBool(_promptedKey, true);
+ } catch (e, stackTrace) {
+ debugPrint('[FlockRavenLiveFix] ERROR: Scan/prompt failed: $e');
+ debugPrint('[FlockRavenLiveFix] Stack trace: $stackTrace');
+ // Don't mark as prompted - retry on a later launch
+ }
+ }
+
+ /// Dev-only: clear the "already prompted" flag so the scan/prompt fires
+ /// again on the next app launch, without needing a full app/data reset.
+ static Future resetPromptedFlagForTesting() async {
+ final prefs = await SharedPreferences.getInstance();
+ await prefs.remove(_promptedKey);
+ debugPrint('[FlockRavenLiveFix] Reset prompted flag for testing');
+ }
}
\ No newline at end of file
diff --git a/lib/models/node_profile.dart b/lib/models/node_profile.dart
index cc8612ae..90b8a860 100644
--- a/lib/models/node_profile.dart
+++ b/lib/models/node_profile.dart
@@ -195,10 +195,10 @@ class NodeProfile {
name: 'Flock Raven',
tags: const {
'man_made': 'surveillance',
- 'surveillance': 'public',
+ 'surveillance': 'outdoor',
'surveillance:type': 'gunshot_detector',
- 'brand': 'Flock Safety',
- 'brand:wikidata': 'Q108485435',
+ 'manufacturer': 'Flock Safety',
+ 'manufacturer:wikidata': 'Q108485435',
},
builtin: true,
requiresDirection: false,
diff --git a/lib/screens/about_screen.dart b/lib/screens/about_screen.dart
index 485cc493..534ce982 100644
--- a/lib/screens/about_screen.dart
+++ b/lib/screens/about_screen.dart
@@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import '../dev_config.dart';
+import '../migrations.dart';
import '../services/localization_service.dart';
import '../services/nuclear_reset_service.dart';
import '../widgets/welcome_dialog.dart';
@@ -261,12 +262,33 @@ class AboutScreen extends StatelessWidget {
),
),
),
+ const SizedBox(height: 8),
+ SizedBox(
+ width: double.infinity,
+ child: OutlinedButton.icon(
+ onPressed: () => _resetFlockRavenLiveFixPrompt(context),
+ icon: const Icon(Icons.replay),
+ label: const Text('Reset Flock Raven Live-Fix Prompt'),
+ ),
+ ),
],
),
),
);
}
+ /// Dev-only: clears the one-time "already prompted" flag for the live OSM
+ /// Flock Raven tag scan, so it can be re-triggered on the next app launch
+ /// without a full data reset (which would also log the user out).
+ Future _resetFlockRavenLiveFixPrompt(BuildContext context) async {
+ await FlockRavenLiveDataFix.resetPromptedFlagForTesting();
+ if (context.mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('Flag reset. Relaunch the app to retrigger the scan/prompt.')),
+ );
+ }
+ }
+
/// Show confirmation dialog for nuclear reset
Future _showNuclearResetConfirmation(BuildContext context) async {
final confirmed = await showDialog(
diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart
index 60d9ca86..bc64f42f 100644
--- a/lib/screens/home_screen.dart
+++ b/lib/screens/home_screen.dart
@@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
import '../app_state.dart';
import '../dev_config.dart';
+import '../migrations.dart';
import '../widgets/map_view.dart';
import '../services/localization_service.dart';
import '../services/map_data_provider.dart';
@@ -233,7 +234,12 @@ class _HomeScreenState extends State with TickerProviderStateMixin {
// Complete the version change workflow (updates last seen version)
await ChangelogService().completeVersionChange();
-
+
+ // One-time (not version-gated) scan/prompt for live OSM data still
+ // using the old Flock Raven tag scheme.
+ if (!mounted) return;
+ await FlockRavenLiveDataFix.checkAndPrompt(appState, context);
+
} catch (e) {
// Silently handle errors to avoid breaking the app launch
debugPrint('[HomeScreen] Error checking for popup: $e');
diff --git a/lib/services/changelog_service.dart b/lib/services/changelog_service.dart
index 0797daf0..f00db56d 100644
--- a/lib/services/changelog_service.dart
+++ b/lib/services/changelog_service.dart
@@ -244,7 +244,11 @@ class ChangelogService {
if (needsMigration(lastSeenVersion, currentVersion, '2.10.5')) {
versionsNeedingMigration.add('2.10.5');
}
-
+
+ if (needsMigration(lastSeenVersion, currentVersion, '2.11.1')) {
+ versionsNeedingMigration.add('2.11.1');
+ }
+
// Future versions can be added here
// if (needsMigration(lastSeenVersion, currentVersion, '2.0.0')) {
diff --git a/lib/services/overpass_service.dart b/lib/services/overpass_service.dart
index 90299d7e..92a4a6b5 100644
--- a/lib/services/overpass_service.dart
+++ b/lib/services/overpass_service.dart
@@ -192,6 +192,44 @@ out;
}
+ /// Fetch nodes last edited by [username] that match a raw tag filter, with
+ /// no bounding box (global query). Used for one-time account-wide data-fix
+ /// scans (e.g. finding a user's own nodes still using a stale profile's
+ /// old tags after that profile's tags are corrected in a later release).
+ Future> fetchNodesByUserAndTags({
+ required String username,
+ required Map tagFilter,
+ ResiliencePolicy? policy,
+ }) async {
+ final query = _buildUserTagQuery(username, tagFilter);
+ final endpoint = _primaryEndpoint;
+ final canFallback = _endpointOverride == null;
+ final effectivePolicy = policy ?? _policy;
+
+ return executeWithFallback>(
+ primaryUrl: endpoint,
+ fallbackUrl: canFallback ? fallbackEndpoint : null,
+ execute: (url) => _attemptFetch(url, query, effectivePolicy),
+ classifyError: _classifyError,
+ policy: effectivePolicy,
+ );
+ }
+
+ /// Build an Overpass QL query for all nodes last touched by [username]
+ /// matching every entry in [tagFilter].
+ String _buildUserTagQuery(String username, Map tagFilter) {
+ final escapedUser = username.replaceAll('\\', '\\\\').replaceAll('"', '\\"');
+ final filters = tagFilter.entries
+ .map((entry) => '["${entry.key}"="${entry.value}"]')
+ .join();
+
+ return '''
+[out:json][timeout:60];
+node(user:"$escapedUser")$filters;
+out body;
+''';
+ }
+
/// Deduplicate profiles for Overpass queries by removing profiles that are
/// subsumed by others. A profile A subsumes profile B if all of A's
/// non-empty tags exist in B with identical values — meaning every node
diff --git a/lib/state/upload_queue_state.dart b/lib/state/upload_queue_state.dart
index a7250b7c..332ec1e3 100644
--- a/lib/state/upload_queue_state.dart
+++ b/lib/state/upload_queue_state.dart
@@ -287,6 +287,61 @@ class UploadQueueState extends ChangeNotifier {
return upload;
}
+ /// Queue corrective "modify" uploads for live OSM nodes (found via an
+ /// account-wide scan, not necessarily in the local cache) that still carry
+ /// the old Flock Raven tag scheme. Unlike a normal edit session these nodes
+ /// may not be cached locally, so no pending-edit visual markers are added —
+ /// they'll simply refresh once each upload completes.
+ List addFlockRavenCorrections(List staleNodes, {required UploadMode uploadMode}) {
+ final correctedProfile = NodeProfile(
+ id: 'builtin-flock-raven',
+ name: 'Flock Raven',
+ tags: const {
+ 'man_made': 'surveillance',
+ 'surveillance': 'outdoor',
+ 'surveillance:type': 'gunshot_detector',
+ 'manufacturer': 'Flock Safety',
+ 'manufacturer:wikidata': 'Q108485435',
+ },
+ builtin: true,
+ requiresDirection: false,
+ submittable: true,
+ editable: true,
+ );
+
+ final corrections = [];
+
+ for (final node in staleNodes) {
+ final existingTags = Map.from(node.tags);
+ for (final key in correctedProfile.tags.keys) {
+ existingTags.remove(key);
+ }
+ existingTags.remove('brand');
+ existingTags.remove('brand:wikidata');
+
+ final upload = PendingUpload(
+ coord: node.coord,
+ direction: 0,
+ profile: correctedProfile,
+ additionalExistingTags: existingTags,
+ changesetComment: 'Correct Flock Raven surveillance/manufacturer tags',
+ uploadMode: uploadMode,
+ operation: UploadOperation.modify,
+ originalNodeId: node.id,
+ );
+
+ _queue.add(upload);
+ corrections.add(upload);
+ }
+
+ if (corrections.isNotEmpty) {
+ _saveQueue();
+ notifyListeners();
+ }
+
+ return corrections;
+ }
+
void clearQueue() {
// Clean up all pending nodes from cache before clearing queue
for (final upload in _queue) {
@@ -822,6 +877,66 @@ class UploadQueueState extends ChangeNotifier {
notifyListeners();
}
+ /// Fix queued (not-yet-uploaded) Flock Raven entries that still carry the old
+ /// tag scheme ('surveillance': 'public', 'brand': 'Flock Safety') by
+ /// replacing them with the corrected scheme ('surveillance': 'outdoor',
+ /// 'manufacturer': 'Flock Safety'). Returns true if any entries were changed.
+ Future migrateFlockRavenQueueTags() async {
+ bool changed = false;
+
+ for (var i = 0; i < _queue.length; i++) {
+ final upload = _queue[i];
+ final profile = upload.profile;
+ if (profile == null) continue;
+
+ final tags = profile.tags;
+ if (tags['surveillance'] == 'public' &&
+ tags['brand'] == 'Flock Safety' &&
+ tags['surveillance:type'] == 'gunshot_detector') {
+ final newTags = Map.from(tags);
+ final wikidata = newTags.remove('brand:wikidata');
+ newTags.remove('brand');
+ newTags['surveillance'] = 'outdoor';
+ newTags['manufacturer'] = 'Flock Safety';
+ newTags['manufacturer:wikidata'] = wikidata ?? 'Q108485435';
+
+ _queue[i] = PendingUpload(
+ coord: upload.coord,
+ direction: upload.direction,
+ profile: profile.copyWith(tags: newTags),
+ operatorProfile: upload.operatorProfile,
+ refinedTags: upload.refinedTags,
+ additionalExistingTags: upload.additionalExistingTags,
+ changesetComment: upload.changesetComment,
+ uploadMode: upload.uploadMode,
+ operation: upload.operation,
+ originalNodeId: upload.originalNodeId,
+ submittedNodeId: upload.submittedNodeId,
+ tempNodeId: upload.tempNodeId,
+ attempts: upload.attempts,
+ error: upload.error,
+ errorMessage: upload.errorMessage,
+ completing: upload.completing,
+ uploadState: upload.uploadState,
+ changesetId: upload.changesetId,
+ nodeOperationCompletedAt: upload.nodeOperationCompletedAt,
+ changesetCloseAttempts: upload.changesetCloseAttempts,
+ lastChangesetCloseAttemptAt: upload.lastChangesetCloseAttemptAt,
+ nodeSubmissionAttempts: upload.nodeSubmissionAttempts,
+ lastNodeSubmissionAttemptAt: upload.lastNodeSubmissionAttemptAt,
+ );
+ changed = true;
+ }
+ }
+
+ if (changed) {
+ await _saveQueue();
+ notifyListeners();
+ }
+
+ return changed;
+ }
+
// Public method to manually trigger cache repopulation (useful for debugging or after cache clears)
void repopulateCacheFromQueue() {
_repopulateCacheFromQueue();
diff --git a/pubspec.yaml b/pubspec.yaml
index d25567ff..cb32209e 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,7 +1,7 @@
name: deflockapp
description: Map public surveillance infrastructure with OpenStreetMap
publish_to: "none"
-version: 2.11.0+61 # The thing after the + is the version code, incremented with each release
+version: 2.11.1+62 # The thing after the + is the version code, incremented with each release
environment:
sdk: ">=3.10.3 <4.0.0" # Resolved dependency floor (Dart 3.10.3 = Flutter 3.38+)
diff --git a/test/migrations_test.dart b/test/migrations_test.dart
new file mode 100644
index 00000000..ce4646d7
--- /dev/null
+++ b/test/migrations_test.dart
@@ -0,0 +1,144 @@
+import 'dart:convert';
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:latlong2/latlong.dart';
+import 'package:mocktail/mocktail.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+import 'package:deflockapp/app_state.dart';
+import 'package:deflockapp/migrations.dart';
+import 'package:deflockapp/models/node_profile.dart';
+import 'package:deflockapp/models/pending_upload.dart';
+import 'package:deflockapp/services/profile_service.dart';
+import 'package:deflockapp/state/settings_state.dart';
+import 'package:deflockapp/state/upload_queue_state.dart';
+
+class MockAppState extends Mock implements AppState {}
+
+NodeProfile staleFlockRavenProfile() => NodeProfile(
+ id: 'builtin-flock-raven',
+ name: 'Flock Raven',
+ tags: const {
+ 'man_made': 'surveillance',
+ 'surveillance': 'public',
+ 'surveillance:type': 'gunshot_detector',
+ 'brand': 'Flock Safety',
+ 'brand:wikidata': 'Q108485435',
+ },
+ builtin: true,
+ requiresDirection: false,
+ );
+
+void main() {
+ late MockAppState mockAppState;
+
+ setUp(() {
+ SharedPreferences.setMockInitialValues({});
+ mockAppState = MockAppState();
+ AppState.instance = mockAppState;
+ when(() => mockAppState.offlineMode).thenReturn(false);
+ when(() => mockAppState.offlineFeaturesEnabled).thenReturn(false);
+ when(() => mockAppState.reloadProfiles()).thenAnswer((_) async {});
+ when(() => mockAppState.migrateFlockRavenQueueTags()).thenAnswer((_) async => false);
+ });
+
+ group('OneTimeMigrations.migrate_2_11_1 (profile catalog)', () {
+ test('fixes stale Flock Raven profile tags in storage', () async {
+ await ProfileService().save([staleFlockRavenProfile()]);
+
+ await OneTimeMigrations.migrate_2_11_1(mockAppState);
+
+ final saved = await ProfileService().load();
+ final fixed = saved.firstWhere((p) => p.id == 'builtin-flock-raven');
+
+ expect(fixed.tags['surveillance'], 'outdoor');
+ expect(fixed.tags['manufacturer'], 'Flock Safety');
+ expect(fixed.tags['manufacturer:wikidata'], 'Q108485435');
+ expect(fixed.tags.containsKey('brand'), isFalse);
+ expect(fixed.tags.containsKey('brand:wikidata'), isFalse);
+
+ verify(() => mockAppState.reloadProfiles()).called(1);
+ verify(() => mockAppState.migrateFlockRavenQueueTags()).called(1);
+ });
+
+ test('leaves an already-correct profile untouched', () async {
+ final correctProfile = staleFlockRavenProfile().copyWith(tags: const {
+ 'man_made': 'surveillance',
+ 'surveillance': 'outdoor',
+ 'surveillance:type': 'gunshot_detector',
+ 'manufacturer': 'Flock Safety',
+ 'manufacturer:wikidata': 'Q108485435',
+ });
+ await ProfileService().save([correctProfile]);
+
+ await OneTimeMigrations.migrate_2_11_1(mockAppState);
+
+ verifyNever(() => mockAppState.reloadProfiles());
+ });
+
+ test('does not touch the unrelated ALPR "Flock" profile', () async {
+ final alprFlock = NodeProfile(
+ id: 'builtin-flock',
+ name: 'Flock',
+ tags: const {
+ 'man_made': 'surveillance',
+ 'surveillance': 'public',
+ 'surveillance:type': 'ALPR',
+ 'manufacturer': 'Flock Safety',
+ 'manufacturer:wikidata': 'Q108485435',
+ },
+ builtin: true,
+ );
+ await ProfileService().save([alprFlock]);
+
+ await OneTimeMigrations.migrate_2_11_1(mockAppState);
+
+ final saved = await ProfileService().load();
+ expect(saved.single.tags, alprFlock.tags);
+ verifyNever(() => mockAppState.reloadProfiles());
+ });
+ });
+
+ group('UploadQueueState.migrateFlockRavenQueueTags', () {
+ test('fixes a queued (not-yet-uploaded) Flock Raven entry', () async {
+ final pending = PendingUpload(
+ coord: const LatLng(1, 2),
+ direction: 0,
+ profile: staleFlockRavenProfile(),
+ changesetComment: 'Add Flock Raven surveillance node',
+ uploadMode: UploadMode.simulate,
+ operation: UploadOperation.create,
+ );
+
+ SharedPreferences.setMockInitialValues({
+ 'queue': jsonEncode([pending.toJson()]),
+ });
+
+ final state = UploadQueueState();
+ await state.init();
+
+ final changed = await state.migrateFlockRavenQueueTags();
+ expect(changed, isTrue);
+
+ final fixedTags = state.pendingUploads.single.profile!.tags;
+ expect(fixedTags['surveillance'], 'outdoor');
+ expect(fixedTags['manufacturer'], 'Flock Safety');
+ expect(fixedTags.containsKey('brand'), isFalse);
+
+ // Persisted change should survive a reload.
+ final reloaded = UploadQueueState();
+ await reloaded.init();
+ expect(reloaded.pendingUploads.single.profile!.tags['surveillance'], 'outdoor');
+ });
+
+ test('leaves a queue with no Flock Raven entries unchanged', () async {
+ SharedPreferences.setMockInitialValues({'queue': jsonEncode([])});
+
+ final state = UploadQueueState();
+ await state.init();
+
+ final changed = await state.migrateFlockRavenQueueTags();
+ expect(changed, isFalse);
+ });
+ });
+}
From c27296d826214f835d4f879b1ec31bc40ef43546 Mon Sep 17 00:00:00 2001
From: David Popescu <80603759+DavidPopesc@users.noreply.github.com>
Date: Thu, 27 Aug 2026 17:09:37 -0400
Subject: [PATCH 3/6] Refactor: Remove Flock Raven live data fix logic and
related methods.
---
lib/app_state.dart | 10 ----
lib/migrations.dart | 95 ------------------------------
lib/screens/about_screen.dart | 22 -------
lib/screens/home_screen.dart | 16 +++--
lib/services/overpass_service.dart | 38 ------------
lib/state/upload_queue_state.dart | 55 -----------------
test/migrations_test.dart | 1 -
7 files changed, 10 insertions(+), 227 deletions(-)
diff --git a/lib/app_state.dart b/lib/app_state.dart
index 224a29ce..2507269a 100644
--- a/lib/app_state.dart
+++ b/lib/app_state.dart
@@ -616,16 +616,6 @@ class AppState extends ChangeNotifier {
}
}
- /// Queue corrective uploads for the user's own live OSM nodes still using
- /// the old Flock Raven tag scheme (for the one-time live-data fix prompt).
- List addFlockRavenCorrections(List staleNodes) {
- final corrections = _uploadQueueState.addFlockRavenCorrections(staleNodes, uploadMode: uploadMode);
- if (corrections.isNotEmpty) {
- _startUploader();
- }
- return corrections;
- }
-
void deleteNode(OsmNode node, {String? changesetComment}) {
_uploadQueueState.addFromNodeDeletion(node, uploadMode: uploadMode, changesetComment: changesetComment);
_startUploader();
diff --git a/lib/migrations.dart b/lib/migrations.dart
index 3cec690e..c8ef1264 100644
--- a/lib/migrations.dart
+++ b/lib/migrations.dart
@@ -4,7 +4,6 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'app_state.dart';
import 'services/offline_area_service.dart';
-import 'services/overpass_service.dart';
import 'services/profile_service.dart';
import 'services/suspected_location_cache.dart';
import 'widgets/nuclear_reset_dialog.dart';
@@ -309,98 +308,4 @@ class OneTimeMigrations {
}
}
}
-}
-
-/// One-time (not version-gated) scan of the logged-in user's own live OSM
-/// data for nodes still using the old Flock Raven tag scheme, offering to
-/// correct them. Runs at most once ever: skipped permanently once the user
-/// has answered the prompt, or once a scan finds nothing to fix. A failed
-/// scan (e.g. offline) is *not* marked as done, so it's retried on a later
-/// launch once conditions allow.
-class FlockRavenLiveDataFix {
- static const _promptedKey = 'flock_raven_live_fix_prompted';
-
- /// The old (incorrect) Flock Raven tag scheme this scan looks for.
- static const Map _staleTags = {
- 'man_made': 'surveillance',
- 'surveillance': 'public',
- 'surveillance:type': 'gunshot_detector',
- 'brand': 'Flock Safety',
- };
-
- static Future checkAndPrompt(AppState appState, BuildContext? context) async {
- try {
- final prefs = await SharedPreferences.getInstance();
- if (prefs.getBool(_promptedKey) == true) return;
-
- if (!appState.isLoggedIn || appState.username.isEmpty) {
- debugPrint('[FlockRavenLiveFix] Not logged in, will retry on a later launch');
- return;
- }
-
- if (context == null || !context.mounted) {
- debugPrint('[FlockRavenLiveFix] No context available, will retry on a later launch');
- return;
- }
-
- debugPrint('[FlockRavenLiveFix] Scanning OSM for stale Flock Raven nodes owned by ${appState.username}');
- final staleNodes = await OverpassService().fetchNodesByUserAndTags(
- username: appState.username,
- tagFilter: _staleTags,
- );
-
- if (staleNodes.isEmpty) {
- debugPrint('[FlockRavenLiveFix] No stale nodes found, marking as done');
- await prefs.setBool(_promptedKey, true);
- return;
- }
-
- if (!context.mounted) return; // Re-check after the async scan
-
- final count = staleNodes.length;
- final shouldFix = await showDialog(
- context: context,
- barrierDismissible: false,
- builder: (ctx) => AlertDialog(
- title: const Text('Update Flock Raven tags?'),
- content: Text(
- 'Found $count Flock Raven ${count == 1 ? 'node' : 'nodes'} on your OpenStreetMap '
- 'account using outdated tags. Update ${count == 1 ? 'it' : 'them'} to the correct tags now?',
- ),
- actions: [
- TextButton(
- onPressed: () => Navigator.of(ctx).pop(false),
- child: const Text('No'),
- ),
- TextButton(
- onPressed: () => Navigator.of(ctx).pop(true),
- child: const Text('Yes'),
- ),
- ],
- ),
- );
-
- if (shouldFix == true) {
- final queued = appState.addFlockRavenCorrections(staleNodes);
- debugPrint('[FlockRavenLiveFix] Queued ${queued.length} corrective uploads');
- } else {
- debugPrint('[FlockRavenLiveFix] User declined the live data fix');
- }
-
- // Never ask again, regardless of the answer.
- await prefs.setBool(_promptedKey, true);
- } catch (e, stackTrace) {
- debugPrint('[FlockRavenLiveFix] ERROR: Scan/prompt failed: $e');
- debugPrint('[FlockRavenLiveFix] Stack trace: $stackTrace');
- // Don't mark as prompted - retry on a later launch
- }
- }
-
- /// Dev-only: clear the "already prompted" flag so the scan/prompt fires
- /// again on the next app launch, without needing a full app/data reset.
- static Future resetPromptedFlagForTesting() async {
- final prefs = await SharedPreferences.getInstance();
- await prefs.remove(_promptedKey);
- debugPrint('[FlockRavenLiveFix] Reset prompted flag for testing');
- }
}
\ No newline at end of file
diff --git a/lib/screens/about_screen.dart b/lib/screens/about_screen.dart
index 534ce982..485cc493 100644
--- a/lib/screens/about_screen.dart
+++ b/lib/screens/about_screen.dart
@@ -2,7 +2,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import '../dev_config.dart';
-import '../migrations.dart';
import '../services/localization_service.dart';
import '../services/nuclear_reset_service.dart';
import '../widgets/welcome_dialog.dart';
@@ -262,33 +261,12 @@ class AboutScreen extends StatelessWidget {
),
),
),
- const SizedBox(height: 8),
- SizedBox(
- width: double.infinity,
- child: OutlinedButton.icon(
- onPressed: () => _resetFlockRavenLiveFixPrompt(context),
- icon: const Icon(Icons.replay),
- label: const Text('Reset Flock Raven Live-Fix Prompt'),
- ),
- ),
],
),
),
);
}
- /// Dev-only: clears the one-time "already prompted" flag for the live OSM
- /// Flock Raven tag scan, so it can be re-triggered on the next app launch
- /// without a full data reset (which would also log the user out).
- Future _resetFlockRavenLiveFixPrompt(BuildContext context) async {
- await FlockRavenLiveDataFix.resetPromptedFlagForTesting();
- if (context.mounted) {
- ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(content: Text('Flag reset. Relaunch the app to retrigger the scan/prompt.')),
- );
- }
- }
-
/// Show confirmation dialog for nuclear reset
Future _showNuclearResetConfirmation(BuildContext context) async {
final confirmed = await showDialog(
diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart
index bc64f42f..fff1784e 100644
--- a/lib/screens/home_screen.dart
+++ b/lib/screens/home_screen.dart
@@ -6,7 +6,6 @@ import 'package:provider/provider.dart';
import '../app_state.dart';
import '../dev_config.dart';
-import '../migrations.dart';
import '../widgets/map_view.dart';
import '../services/localization_service.dart';
import '../services/map_data_provider.dart';
@@ -235,11 +234,6 @@ class _HomeScreenState extends State with TickerProviderStateMixin {
// Complete the version change workflow (updates last seen version)
await ChangelogService().completeVersionChange();
- // One-time (not version-gated) scan/prompt for live OSM data still
- // using the old Flock Raven tag scheme.
- if (!mounted) return;
- await FlockRavenLiveDataFix.checkAndPrompt(appState, context);
-
} catch (e) {
// Silently handle errors to avoid breaking the app launch
debugPrint('[HomeScreen] Error checking for popup: $e');
@@ -341,6 +335,14 @@ class _HomeScreenState extends State with TickerProviderStateMixin {
onSelectedNodeChanged: (id) => setState(() => _selectedNodeId = id),
);
+ // Deleting the node rebuilds this sheet mid-close (into a "deleted"
+ // placeholder of a different height) before the close animation settles.
+ // That late MeasuredSheet measurement can arrive after `closed` has
+ // already reset the height below, leaving it stuck non-zero forever
+ // (and the location button permanently greyed out). Ignore any height
+ // updates once the sheet has actually closed.
+ bool sheetClosed = false;
+
final controller = _scaffoldKey.currentState!.showBottomSheet(
(ctx) => Padding(
padding: EdgeInsets.only(
@@ -348,6 +350,7 @@ class _HomeScreenState extends State with TickerProviderStateMixin {
),
child: MeasuredSheet(
onHeightChanged: (height) {
+ if (sheetClosed) return;
_sheetCoordinator.updateTagSheetHeight(
height + MediaQuery.of(context).padding.bottom,
() => setState(() {}),
@@ -382,6 +385,7 @@ class _HomeScreenState extends State with TickerProviderStateMixin {
// Reset height and selection when sheet is dismissed (unless transitioning to edit)
controller.closed.then((_) {
+ sheetClosed = true;
if (!_sheetCoordinator.transitioningToEdit) {
_sheetCoordinator.resetTagSheetHeight(() => setState(() {}));
setState(() => _selectedNodeId = null);
diff --git a/lib/services/overpass_service.dart b/lib/services/overpass_service.dart
index 92a4a6b5..90299d7e 100644
--- a/lib/services/overpass_service.dart
+++ b/lib/services/overpass_service.dart
@@ -192,44 +192,6 @@ out;
}
- /// Fetch nodes last edited by [username] that match a raw tag filter, with
- /// no bounding box (global query). Used for one-time account-wide data-fix
- /// scans (e.g. finding a user's own nodes still using a stale profile's
- /// old tags after that profile's tags are corrected in a later release).
- Future> fetchNodesByUserAndTags({
- required String username,
- required Map tagFilter,
- ResiliencePolicy? policy,
- }) async {
- final query = _buildUserTagQuery(username, tagFilter);
- final endpoint = _primaryEndpoint;
- final canFallback = _endpointOverride == null;
- final effectivePolicy = policy ?? _policy;
-
- return executeWithFallback>(
- primaryUrl: endpoint,
- fallbackUrl: canFallback ? fallbackEndpoint : null,
- execute: (url) => _attemptFetch(url, query, effectivePolicy),
- classifyError: _classifyError,
- policy: effectivePolicy,
- );
- }
-
- /// Build an Overpass QL query for all nodes last touched by [username]
- /// matching every entry in [tagFilter].
- String _buildUserTagQuery(String username, Map tagFilter) {
- final escapedUser = username.replaceAll('\\', '\\\\').replaceAll('"', '\\"');
- final filters = tagFilter.entries
- .map((entry) => '["${entry.key}"="${entry.value}"]')
- .join();
-
- return '''
-[out:json][timeout:60];
-node(user:"$escapedUser")$filters;
-out body;
-''';
- }
-
/// Deduplicate profiles for Overpass queries by removing profiles that are
/// subsumed by others. A profile A subsumes profile B if all of A's
/// non-empty tags exist in B with identical values — meaning every node
diff --git a/lib/state/upload_queue_state.dart b/lib/state/upload_queue_state.dart
index 332ec1e3..565147e9 100644
--- a/lib/state/upload_queue_state.dart
+++ b/lib/state/upload_queue_state.dart
@@ -287,61 +287,6 @@ class UploadQueueState extends ChangeNotifier {
return upload;
}
- /// Queue corrective "modify" uploads for live OSM nodes (found via an
- /// account-wide scan, not necessarily in the local cache) that still carry
- /// the old Flock Raven tag scheme. Unlike a normal edit session these nodes
- /// may not be cached locally, so no pending-edit visual markers are added —
- /// they'll simply refresh once each upload completes.
- List addFlockRavenCorrections(List staleNodes, {required UploadMode uploadMode}) {
- final correctedProfile = NodeProfile(
- id: 'builtin-flock-raven',
- name: 'Flock Raven',
- tags: const {
- 'man_made': 'surveillance',
- 'surveillance': 'outdoor',
- 'surveillance:type': 'gunshot_detector',
- 'manufacturer': 'Flock Safety',
- 'manufacturer:wikidata': 'Q108485435',
- },
- builtin: true,
- requiresDirection: false,
- submittable: true,
- editable: true,
- );
-
- final corrections = [];
-
- for (final node in staleNodes) {
- final existingTags = Map.from(node.tags);
- for (final key in correctedProfile.tags.keys) {
- existingTags.remove(key);
- }
- existingTags.remove('brand');
- existingTags.remove('brand:wikidata');
-
- final upload = PendingUpload(
- coord: node.coord,
- direction: 0,
- profile: correctedProfile,
- additionalExistingTags: existingTags,
- changesetComment: 'Correct Flock Raven surveillance/manufacturer tags',
- uploadMode: uploadMode,
- operation: UploadOperation.modify,
- originalNodeId: node.id,
- );
-
- _queue.add(upload);
- corrections.add(upload);
- }
-
- if (corrections.isNotEmpty) {
- _saveQueue();
- notifyListeners();
- }
-
- return corrections;
- }
-
void clearQueue() {
// Clean up all pending nodes from cache before clearing queue
for (final upload in _queue) {
diff --git a/test/migrations_test.dart b/test/migrations_test.dart
index ce4646d7..0127b2d7 100644
--- a/test/migrations_test.dart
+++ b/test/migrations_test.dart
@@ -10,7 +10,6 @@ import 'package:deflockapp/migrations.dart';
import 'package:deflockapp/models/node_profile.dart';
import 'package:deflockapp/models/pending_upload.dart';
import 'package:deflockapp/services/profile_service.dart';
-import 'package:deflockapp/state/settings_state.dart';
import 'package:deflockapp/state/upload_queue_state.dart';
class MockAppState extends Mock implements AppState {}
From 1685a25a66a7e8cbaa337f92349a743be11abd94 Mon Sep 17 00:00:00 2001
From: David Popescu <80603759+DavidPopesc@users.noreply.github.com>
Date: Thu, 27 Aug 2026 17:17:19 -0400
Subject: [PATCH 4/6] Remove migrations_test.dart file as it is no longer
needed
---
test/migrations_test.dart | 143 --------------------------------------
1 file changed, 143 deletions(-)
delete mode 100644 test/migrations_test.dart
diff --git a/test/migrations_test.dart b/test/migrations_test.dart
deleted file mode 100644
index 0127b2d7..00000000
--- a/test/migrations_test.dart
+++ /dev/null
@@ -1,143 +0,0 @@
-import 'dart:convert';
-
-import 'package:flutter_test/flutter_test.dart';
-import 'package:latlong2/latlong.dart';
-import 'package:mocktail/mocktail.dart';
-import 'package:shared_preferences/shared_preferences.dart';
-
-import 'package:deflockapp/app_state.dart';
-import 'package:deflockapp/migrations.dart';
-import 'package:deflockapp/models/node_profile.dart';
-import 'package:deflockapp/models/pending_upload.dart';
-import 'package:deflockapp/services/profile_service.dart';
-import 'package:deflockapp/state/upload_queue_state.dart';
-
-class MockAppState extends Mock implements AppState {}
-
-NodeProfile staleFlockRavenProfile() => NodeProfile(
- id: 'builtin-flock-raven',
- name: 'Flock Raven',
- tags: const {
- 'man_made': 'surveillance',
- 'surveillance': 'public',
- 'surveillance:type': 'gunshot_detector',
- 'brand': 'Flock Safety',
- 'brand:wikidata': 'Q108485435',
- },
- builtin: true,
- requiresDirection: false,
- );
-
-void main() {
- late MockAppState mockAppState;
-
- setUp(() {
- SharedPreferences.setMockInitialValues({});
- mockAppState = MockAppState();
- AppState.instance = mockAppState;
- when(() => mockAppState.offlineMode).thenReturn(false);
- when(() => mockAppState.offlineFeaturesEnabled).thenReturn(false);
- when(() => mockAppState.reloadProfiles()).thenAnswer((_) async {});
- when(() => mockAppState.migrateFlockRavenQueueTags()).thenAnswer((_) async => false);
- });
-
- group('OneTimeMigrations.migrate_2_11_1 (profile catalog)', () {
- test('fixes stale Flock Raven profile tags in storage', () async {
- await ProfileService().save([staleFlockRavenProfile()]);
-
- await OneTimeMigrations.migrate_2_11_1(mockAppState);
-
- final saved = await ProfileService().load();
- final fixed = saved.firstWhere((p) => p.id == 'builtin-flock-raven');
-
- expect(fixed.tags['surveillance'], 'outdoor');
- expect(fixed.tags['manufacturer'], 'Flock Safety');
- expect(fixed.tags['manufacturer:wikidata'], 'Q108485435');
- expect(fixed.tags.containsKey('brand'), isFalse);
- expect(fixed.tags.containsKey('brand:wikidata'), isFalse);
-
- verify(() => mockAppState.reloadProfiles()).called(1);
- verify(() => mockAppState.migrateFlockRavenQueueTags()).called(1);
- });
-
- test('leaves an already-correct profile untouched', () async {
- final correctProfile = staleFlockRavenProfile().copyWith(tags: const {
- 'man_made': 'surveillance',
- 'surveillance': 'outdoor',
- 'surveillance:type': 'gunshot_detector',
- 'manufacturer': 'Flock Safety',
- 'manufacturer:wikidata': 'Q108485435',
- });
- await ProfileService().save([correctProfile]);
-
- await OneTimeMigrations.migrate_2_11_1(mockAppState);
-
- verifyNever(() => mockAppState.reloadProfiles());
- });
-
- test('does not touch the unrelated ALPR "Flock" profile', () async {
- final alprFlock = NodeProfile(
- id: 'builtin-flock',
- name: 'Flock',
- tags: const {
- 'man_made': 'surveillance',
- 'surveillance': 'public',
- 'surveillance:type': 'ALPR',
- 'manufacturer': 'Flock Safety',
- 'manufacturer:wikidata': 'Q108485435',
- },
- builtin: true,
- );
- await ProfileService().save([alprFlock]);
-
- await OneTimeMigrations.migrate_2_11_1(mockAppState);
-
- final saved = await ProfileService().load();
- expect(saved.single.tags, alprFlock.tags);
- verifyNever(() => mockAppState.reloadProfiles());
- });
- });
-
- group('UploadQueueState.migrateFlockRavenQueueTags', () {
- test('fixes a queued (not-yet-uploaded) Flock Raven entry', () async {
- final pending = PendingUpload(
- coord: const LatLng(1, 2),
- direction: 0,
- profile: staleFlockRavenProfile(),
- changesetComment: 'Add Flock Raven surveillance node',
- uploadMode: UploadMode.simulate,
- operation: UploadOperation.create,
- );
-
- SharedPreferences.setMockInitialValues({
- 'queue': jsonEncode([pending.toJson()]),
- });
-
- final state = UploadQueueState();
- await state.init();
-
- final changed = await state.migrateFlockRavenQueueTags();
- expect(changed, isTrue);
-
- final fixedTags = state.pendingUploads.single.profile!.tags;
- expect(fixedTags['surveillance'], 'outdoor');
- expect(fixedTags['manufacturer'], 'Flock Safety');
- expect(fixedTags.containsKey('brand'), isFalse);
-
- // Persisted change should survive a reload.
- final reloaded = UploadQueueState();
- await reloaded.init();
- expect(reloaded.pendingUploads.single.profile!.tags['surveillance'], 'outdoor');
- });
-
- test('leaves a queue with no Flock Raven entries unchanged', () async {
- SharedPreferences.setMockInitialValues({'queue': jsonEncode([])});
-
- final state = UploadQueueState();
- await state.init();
-
- final changed = await state.migrateFlockRavenQueueTags();
- expect(changed, isFalse);
- });
- });
-}
From 8284c77e59d065290c7f8fc1df439ddeec7bdf89 Mon Sep 17 00:00:00 2001
From: David Popescu <80603759+DavidPopesc@users.noreply.github.com>
Date: Thu, 27 Aug 2026 17:22:41 -0400
Subject: [PATCH 5/6] Revert "Remove migrations_test.dart file as it is no
longer needed"
This reverts commit 1685a25a66a7e8cbaa337f92349a743be11abd94.
---
test/migrations_test.dart | 143 ++++++++++++++++++++++++++++++++++++++
1 file changed, 143 insertions(+)
create mode 100644 test/migrations_test.dart
diff --git a/test/migrations_test.dart b/test/migrations_test.dart
new file mode 100644
index 00000000..0127b2d7
--- /dev/null
+++ b/test/migrations_test.dart
@@ -0,0 +1,143 @@
+import 'dart:convert';
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:latlong2/latlong.dart';
+import 'package:mocktail/mocktail.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+import 'package:deflockapp/app_state.dart';
+import 'package:deflockapp/migrations.dart';
+import 'package:deflockapp/models/node_profile.dart';
+import 'package:deflockapp/models/pending_upload.dart';
+import 'package:deflockapp/services/profile_service.dart';
+import 'package:deflockapp/state/upload_queue_state.dart';
+
+class MockAppState extends Mock implements AppState {}
+
+NodeProfile staleFlockRavenProfile() => NodeProfile(
+ id: 'builtin-flock-raven',
+ name: 'Flock Raven',
+ tags: const {
+ 'man_made': 'surveillance',
+ 'surveillance': 'public',
+ 'surveillance:type': 'gunshot_detector',
+ 'brand': 'Flock Safety',
+ 'brand:wikidata': 'Q108485435',
+ },
+ builtin: true,
+ requiresDirection: false,
+ );
+
+void main() {
+ late MockAppState mockAppState;
+
+ setUp(() {
+ SharedPreferences.setMockInitialValues({});
+ mockAppState = MockAppState();
+ AppState.instance = mockAppState;
+ when(() => mockAppState.offlineMode).thenReturn(false);
+ when(() => mockAppState.offlineFeaturesEnabled).thenReturn(false);
+ when(() => mockAppState.reloadProfiles()).thenAnswer((_) async {});
+ when(() => mockAppState.migrateFlockRavenQueueTags()).thenAnswer((_) async => false);
+ });
+
+ group('OneTimeMigrations.migrate_2_11_1 (profile catalog)', () {
+ test('fixes stale Flock Raven profile tags in storage', () async {
+ await ProfileService().save([staleFlockRavenProfile()]);
+
+ await OneTimeMigrations.migrate_2_11_1(mockAppState);
+
+ final saved = await ProfileService().load();
+ final fixed = saved.firstWhere((p) => p.id == 'builtin-flock-raven');
+
+ expect(fixed.tags['surveillance'], 'outdoor');
+ expect(fixed.tags['manufacturer'], 'Flock Safety');
+ expect(fixed.tags['manufacturer:wikidata'], 'Q108485435');
+ expect(fixed.tags.containsKey('brand'), isFalse);
+ expect(fixed.tags.containsKey('brand:wikidata'), isFalse);
+
+ verify(() => mockAppState.reloadProfiles()).called(1);
+ verify(() => mockAppState.migrateFlockRavenQueueTags()).called(1);
+ });
+
+ test('leaves an already-correct profile untouched', () async {
+ final correctProfile = staleFlockRavenProfile().copyWith(tags: const {
+ 'man_made': 'surveillance',
+ 'surveillance': 'outdoor',
+ 'surveillance:type': 'gunshot_detector',
+ 'manufacturer': 'Flock Safety',
+ 'manufacturer:wikidata': 'Q108485435',
+ });
+ await ProfileService().save([correctProfile]);
+
+ await OneTimeMigrations.migrate_2_11_1(mockAppState);
+
+ verifyNever(() => mockAppState.reloadProfiles());
+ });
+
+ test('does not touch the unrelated ALPR "Flock" profile', () async {
+ final alprFlock = NodeProfile(
+ id: 'builtin-flock',
+ name: 'Flock',
+ tags: const {
+ 'man_made': 'surveillance',
+ 'surveillance': 'public',
+ 'surveillance:type': 'ALPR',
+ 'manufacturer': 'Flock Safety',
+ 'manufacturer:wikidata': 'Q108485435',
+ },
+ builtin: true,
+ );
+ await ProfileService().save([alprFlock]);
+
+ await OneTimeMigrations.migrate_2_11_1(mockAppState);
+
+ final saved = await ProfileService().load();
+ expect(saved.single.tags, alprFlock.tags);
+ verifyNever(() => mockAppState.reloadProfiles());
+ });
+ });
+
+ group('UploadQueueState.migrateFlockRavenQueueTags', () {
+ test('fixes a queued (not-yet-uploaded) Flock Raven entry', () async {
+ final pending = PendingUpload(
+ coord: const LatLng(1, 2),
+ direction: 0,
+ profile: staleFlockRavenProfile(),
+ changesetComment: 'Add Flock Raven surveillance node',
+ uploadMode: UploadMode.simulate,
+ operation: UploadOperation.create,
+ );
+
+ SharedPreferences.setMockInitialValues({
+ 'queue': jsonEncode([pending.toJson()]),
+ });
+
+ final state = UploadQueueState();
+ await state.init();
+
+ final changed = await state.migrateFlockRavenQueueTags();
+ expect(changed, isTrue);
+
+ final fixedTags = state.pendingUploads.single.profile!.tags;
+ expect(fixedTags['surveillance'], 'outdoor');
+ expect(fixedTags['manufacturer'], 'Flock Safety');
+ expect(fixedTags.containsKey('brand'), isFalse);
+
+ // Persisted change should survive a reload.
+ final reloaded = UploadQueueState();
+ await reloaded.init();
+ expect(reloaded.pendingUploads.single.profile!.tags['surveillance'], 'outdoor');
+ });
+
+ test('leaves a queue with no Flock Raven entries unchanged', () async {
+ SharedPreferences.setMockInitialValues({'queue': jsonEncode([])});
+
+ final state = UploadQueueState();
+ await state.init();
+
+ final changed = await state.migrateFlockRavenQueueTags();
+ expect(changed, isFalse);
+ });
+ });
+}
From 2afc68152a0900ec84ff4d55c895379b209c5881 Mon Sep 17 00:00:00 2001
From: David Popescu <80603759+DavidPopesc@users.noreply.github.com>
Date: Thu, 27 Aug 2026 17:33:37 -0400
Subject: [PATCH 6/6] Update iOS deployment target to be back on 13.0 in
project configuration
---
ios/Runner.xcodeproj/project.pbxproj | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index 35c146ed..3c09d15a 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -364,7 +364,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -494,7 +494,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -545,7 +545,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;