Commit Graph
37 Commits
Author SHA1 Message Date
Constantin Leue 539a6e4171 Drop SSID display from settings, connection pill shows generic "Connected"
Real hardware testing (Pixel 10a) showed NetworkCapabilities.getTransportInfo()
consistently returns a redacted WifiInfo (SSID "<unknown ssid>", masked BSSID)
even for the app's own self-requested WifiNetworkSpecifier network, disproving
this codebase's prior assumption of a self-request exemption from
ACCESS_FINE_LOCATION for that API path (confirmed via native diagnostic
logging cross-checked against `adb shell dumpsys wifi`, which does show and
correctly attribute the true SSID at the OS level). Rather than add a location
permission with a runtime prompt purely for this cosmetic display, the
settings pill now just shows "Connected". Documented as decision 4.24.
2026-08-06 22:24:03 +02:00
Constantin LeueandClaude Sonnet 5 e5c87fba5f Drone/Netzwerk-Connect-Disconnect wirklich repariert (Riverpods Auto-Retry verschluckte den Fehlerzustand)
Der vorherige Fix (msp_telemetry_poller.dart, addError nach 3 fehl-
geschlagenen Zyklen) hat die Erkennung allein nicht repariert - aus zwei
Gruenden, beide jetzt behoben:

1. telemetryProvider brach bei WLAN-Verbindungsverlust (wifiLinkStateProvider
   != connected) den Strom bisher mit einem stillen `return;` ab, statt
   ueberhaupt subscribeTelemetry() zu abonnieren. Damit blieb der Provider
   nach einem zuvor erfolgreichen Verbindungsaufbau unbegrenzt auf dem
   letzten AsyncData(...) haengen, sobald das WLAN-Netz verloren ging -
   komplett unabhaengig vom MSP-Poller-Fix, der in diesem Fall nie erreicht
   wird. Ersetzt durch eine neue WifiLinkNotConnectedException.

2. Der eigentliche Grund, warum ich das beim ersten Fix nicht bemerkt habe:
   Riverpod 3.x wiederholt einen fehlgeschlagenen Provider standardmaessig
   automatisch (ProviderContainer.defaultRetry) und haelt ihn dabei in
   AsyncLoading(error: ..., retrying: true) statt sofort auf AsyncError zu
   wechseln - AsyncValue.when()s error:-Zweig (systemMessageAutoLogProvider)
   feuert dafuer nicht, nur der loading:-Zweig (No-Op). Das betraf sowohl
   die neue WifiLinkNotConnectedException als auch das per yield*
   durchgereichte addError aus dem MSP-Poller - beide blieben dadurch
   unbegrenzt "am Wiederholen haengen", nie als AsyncError sichtbar. Mit
   retry: (retryCount, error) => null gezielt fuer telemetryProvider
   deaktiviert - die eigentliche Wiederherstellung passiert ohnehin
   reaktiv (ref.watch(wifiLinkStateProvider) bzw. der Poller-Takt selbst),
   nicht ueber Riverpods Backoff.

Neuer Test in telemetry_provider_test.dart deckt jetzt die komplette
Kette end-to-end ab (echter MspFlightControllerLink + LoopbackTransport +
ueberschriebener wifiLinkStateProvider, keine der bisherigen Tests in
system_message_log_provider_test.dart haette diesen Fehler auffangen
koennen, da sie telemetryProvider selbst immer ueberschreiben statt seine
eigene Generatorfunktion zu durchlaufen).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 17:17:13 +02:00
Constantin LeueandClaude Sonnet 5 8d5b06d0b4 Mock-Batteriespannung an Zellenzahl des aktiven Drohnenprofils koppeln
Die simulierte Telemetrie war fest auf 6S verdrahtet (18.0-24.0V), obwohl
das Drohnenprofil laengst eine konfigurierbare Zellenzahl hat - beim
T1-Ranger-Standardprofil (4S) zeigte die Statusanzeige dadurch bis zu 18V,
obwohl 16.8V (4 * 4.2V Ladeschluss) der realistische Maximalwert ist.

MockFlightControllerLink nimmt jetzt batteryCellCount entgegen und
skaliert den simulierten Spannungsverlauf (4.2V/Zelle voll bis 3.0V/Zelle
nahezu leer) damit. telemetry_provider.dart uebergibt dafuer die
Zellenzahl des aktuell aktiven Drohnenprofils (ref.read, analog zu den
initialWaypoints - einmalig bei der Verbindungserzeugung, kein Reset des
laufenden Akkustands bei spaeteren Profilwechseln).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 14:08:11 +02:00
Constantin LeueandClaude Sonnet 5 e902197462 System messages fuer GPS-Fix und Drone connected/disconnected, Batterie-Schwellwerte auf Pro-Zelle umgestellt
- detectSystemMessages() erkennt jetzt auch den Uebergang zu einem GPS-Fix
  (flankengetriggert wie Battery/Failsafe), Meldung "GPS fix acquired (N
  satellites)".
- Die automatisch erkannten Verbindungsmeldungen heissen jetzt "Drone
  connected"/"Drone disconnected" statt "Connected"/"Connection lost" -
  praeziser an die tatsaechliche Bedeutung angelehnt (Eintreffen/Ausbleiben
  echter Telemetrie-Frames, nicht nur des rohen Socket-Zustands).
- DroneProfile: feste Pack-Alarmspannung (batteryVoltageGreenMinV/RedMinV)
  ersetzt durch batteryCellCount + Pro-Zelle-Schwellwerte
  (batteryVoltageGreenMinPerCellV/RedMinPerCellV, LiPo-Standardwerte 3.4/3.2
  V als Default). Die alten Feldnamen bleiben als berechnete Getter
  (Zellenzahl * Pro-Zelle-Wert) erhalten, damit
  telemetry_field_status.dart unveraendert bleibt. T1-Ranger-Standardprofil
  auf 4S gesetzt.
- DB-Schema v8 -> v9 (additiv, alte Pack-Spannungs-Spalten bleiben als tote
  Spalten stehen), Repository und Share-Codec-Im-/Export entsprechend
  angepasst; alte Exportdateien ohne die neuen Felder fallen auf die
  DroneProfile-Defaults zurueck statt eine unbekannte Zellenzahl zu raten.
- DroneProfileEditor: "Battery voltage alarm"-Gruppe um ein Zellenzahl-Feld
  erweitert und auf V/Zelle umbenannt.
- Tests ergaenzt/angepasst: GPS-Fix-Erkennung (Unit + Provider-Integration),
  neuer v8->v9-Migrationstest analog zum bestehenden v7->v8-Test,
  Connected/Disconnected-Umbenennung in Provider- und Widget-Tests,
  Cell-Count-Roundtrip in Repository- und Share-Codec-Tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 13:06:49 +02:00
Constantin LeueandClaude Sonnet 5 b68c16d8e6 Log mission name on send, log mission/drone-profile changes, auto-send on mission switch
Include the mission name in the "Mission sent"/"Mission upload failed"
System Messages (was just the waypoint count before).

Add MissionMeta.switchSeq, bumped only by an actual mission switch
(startNew/startNewFromPlace/loadMission) - not by the id a brand-new
mission gets from its first autosave, and not by restoreLastSession() on
app start. FlyScreen compares it to detect a genuine switch and reacts
two ways: logs "Mission changed to ..." and automatically re-uploads the
new route to the flight controller, reusing the same send path as the
manual send button (same _sending guard, same success/failure snackbar
and log entry).

Mission-change and drone-profile-change logging intentionally live in
FlyScreen's ref.listen callbacks, not in mission_meta_provider.dart /
active_drone_profile_provider.dart themselves - those providers have no
notion of the app mode (Plan vs Fly, tracked separately by AppModeCubit),
and logging there would record a change regardless of mode. Since
FlyScreen only exists while Fly mode is active, scoping the listeners
there means switching missions or drone profiles from Plan mode produces
no System Messages entries, and only a mission switch (not a drone
profile switch) triggers the automatic re-upload, matching what was
asked for.

Also splits systemMessageLogProvider (the plain message list + log(), no
telemetry dependency) from the new systemMessageAutoLogProvider (the
Connected/lost/battery/failsafe/connection-type auto-detection, which
does watch telemetryProvider) - discovered while wiring the mission-change
logging that logging a plain message from Plan-mode code was forcing the
entire telemetry/transport stack to spin up as a side effect, which broke
an unrelated Plan-mode test (UdpTransport threw on a double-disconnect
during teardown). Keeping the two concerns apart means calling log() for
a one-off message never has that side effect.

Verified end to end on the Pixel_10a emulator: switching to an empty
mission while in Fly mode correctly showed "No waypoints to send" and
logged "Mission changed to ..." automatically, without touching the send
button.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 12:04:35 +02:00
Constantin LeueandClaude Sonnet 5 b09d00ab9f Rename the Fly-mode Warnings tab to System Messages, log connection/upload events
Renames DroneEvent/DroneEventSeverity/droneEventLogProvider/
DroneStatusWarningsPanel to SystemMessage/SystemMessageSeverity/
systemMessageLogProvider/DroneStatusMessagesPanel throughout, matching
what the tab now actually shows - general system messages, not just
drone-health warnings.

Adds a new SystemMessageSeverity.info level (blue) for messages that
aren't a warning/error, and three new message sources on top of the
existing battery/failsafe detection:
- "Connected", logged the first time telemetryProvider produces a frame
  after having none - the mirror image of the existing "Connection lost"
  detection (which already fires on the first stream error after having
  had data), so no new transport-specific dependency was needed.
- "Connection type changed to X", from watching connectionSettingsProvider
  (skips the initial load so it doesn't fire on every app start).
- "Mission sent (N waypoints)" / "Mission upload failed: ...", logged
  from FlyScreen's send handler via a new public log() method on the
  notifier, alongside the existing snackbar.

Verified on the Pixel_10a emulator: entering Fly mode logs "Connected"
once the mock telemetry starts, and tapping the send button logs
"Mission sent (3 waypoints)" right after the snackbar.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 10:47:38 +02:00
Constantin Leue f65dceb338 connection settings cleanup and mock test added. removed bluetooth implementation 2026-08-05 20:01:25 +02:00
Constantin Leue 8e4df2d0d3 drone status menue with status and critical event log implemented 2026-08-04 23:15:56 +02:00
Constantin Leue 66e05f0ffe warning banner removed from footer in fly mode 2026-08-04 10:20:16 +02:00
Constantin Leue a66c199b37 live mission details in footer implemented 2026-08-04 09:48:01 +02:00
Constantin LeueandClaude Sonnet 5 35351140ae Persist the ground elevation profile with the mission
The Mini-Altitude-Profile terrain data only ever lived in TerrainNotifier's
in-memory state (one slot, keyed by route). Every app restart, or every
switch away from and back to a mission, forced a full re-fetch of all
AWS Terrarium elevation tiles for the route - the noticeably slow load
the user reported for some missions was this happening on every visit,
not just once.

Adds a nullable terrainProfileJson column to the missions table
(schema v6->v7) and a saveTerrainProfile() repository method, kept
separate from the regular waypoint upsert() so an ordinary autosave never
clobbers an already-cached profile. TerrainNotifier persists a profile
right after a successful fetch (fire-and-forget) and gains restore(),
called from CurrentMissionMetaNotifier whenever a mission is loaded/
started so a previously fetched profile is available immediately -
ensureFor() still validates its routeKey before use, so a stale restored
profile is never shown for a route that has since changed.

Also included in mission export/import (MissionExportData/
ParsedMissionImport) so sharing a mission carries its terrain cache along
instead of forcing the recipient to refetch it.

Sample-count/resolution stays as-is for now (still fixed 30m spacing,
10-2000 samples) - adapting the sampling density to terrain variance
(e.g. coarser sampling over flat ground) is a separate follow-up.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 08:10:41 +02:00
Constantin Leue 4671b34f5b follow mode behaviour tuned: load mission and jump to wayoinnt cancels follow mode 2026-08-03 22:39:45 +02:00
Constantin Leue 2f4353f3fa map follow on drone focus. rubberband pointing to next waypoint. drone icon bigger 40px 2026-08-03 22:07:11 +02:00
Constantin Leue 5dfd28c56b fit map bug behoben fuer plan modus 2026-08-02 22:23:49 +02:00
Constantin Leue 0649ae4507 disabled auto connect when switching to fly mode and no disconnect when leavin fly mode, race condition fix for connectionType ( now telemetry not working anymore), UI optimizations: centered speed and alt, zoom to mission uses full screen 2026-08-02 21:45:46 +02:00
Constantin Leue f42d997d4c UI improvements for telemetry debugging 2026-08-02 20:21:21 +02:00
Constantin Leue 6cfb82bd77 msp over wifi implemented 2026-08-02 09:20:05 +02:00
Constantin Leue 607dbb6584 autoconnect on fly mode (does not work yet) 2026-08-02 08:25:23 +02:00
Constantin Leue 3b3451b8a2 wifi connection established by app, process socket binding, primary os connection untouched 2026-07-31 15:25:45 +02:00
Constantin Leue 222d8743fb udp package timeout changed to 4s 2026-07-31 11:04:27 +02:00
Constantin Leue ee97a7cb9a wifi udp stream activity indicator added 2026-07-31 10:50:41 +02:00
Constantin Leue cb91436fe4 UI collapsable height profile, english settings menue 2026-07-31 09:49:28 +02:00
Constantin Leue 8cc9a51a34 WIFI connection support added 2026-07-31 08:17:12 +02:00
Constantin Leue 5994aee7ad bluetooth connectivity layer added 2026-07-30 19:46:52 +02:00
Constantin LeueandClaude Sonnet 5 310d92f187 Show mission map and footer in Fly mode, add area/drone zoom buttons
Fly mode previously showed just a black placeholder. It now renders the
same MissionMap (route + waypoint markers) and footer (warnings banner,
altitude profile, BottomStatsBar) as the Plan screen, minus the
reticle/wheels since flying observes the route rather than replanning it.
Waypoints can still be edited via the details list for spontaneous
in-flight changes.

Live drone position comes from a new telemetryProvider/
flightControllerLinkProvider pair (autoDispose), the first place
FlightControllerLink/TelemetryFrame get wired into the UI - currently
backed by MockFlightControllerLink until a real MSP transport exists
(Doku 4.19). A drone marker renders on the map once a telemetry frame
arrives.

The Fly-mode header keeps the fit-to-area button but replaces "zoom to
home" with "zoom to drone" (FlyMapControls) - the first waypoint isn't a
meaningful reference point anymore once airborne.

Extracted computeMissionStats and MissionFooterBar out of PlanScreen so
Plan/Fly share the exact same stats/footer logic instead of duplicating
it, and extracted NavIconButton out of MapSearchControls so both Plan's
and Fly's map controls use the same button widget.

Widget tests that now mount FlyScreen switch from the ProviderScope-based
_wrap() helper to a manual ProviderContainer with an explicit
FlightControllerLink.disconnect() call before test end - the mock's
Timer.periodic doesn't get cancelled by container disposal alone, and
flutter_test's pending-timer check runs before addTearDown callbacks.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 07:45:36 +02:00
Constantin LeueandClaude Sonnet 5 d745b76403 Remove Ready-to-Fly-Gate, add fly-mode header with flight-mode dropdown
Ready-to-Fly-Gate (Doku 2.2/4.5): AppModeCubit.toFly() erzwang bisher
Upload + verifiziert + disarmed und warf sonst einen StateError - da
MissionSyncService/Upload-Flow noch nicht angebunden sind, war das Gate
dauerhaft geschlossen und der Fly-Modus damit von der UI aus gar nicht
erreichbar. toFly() wechselt jetzt unbedingt in den Fly-Modus; die
readyToFly-Logik bleibt als Getter erhalten fuer den spaeteren Upload-
Flow. Der eigentliche Schutz vor einem Missions-Upload im armed-Zustand
lebt unveraendert auf Transport-Ebene (MockFlightControllerLink.
uploadMission() wirft dort weiterhin bei armed).

Fly-Modus-Kopfzeile (Doku 3.11/7.2, HTML-Demonstrator: #flightModePill/
#flightModeDropdown): top_mode_bar.dart zeigte im Fly-Modus bisher nur
eine leere Spacer-Flaeche. Zeigt jetzt die Wind-Pille (weiterhin
relevant, Doku 7.2) und eine neue Flugmodus-Auswahl:

- ui/providers/flight_mode_provider.dart: haelt den lokal gewaehlten
  FlightMode (Doku 3.1: missionRun/guidedPoint/returnHome/hold), noch
  ohne echte FlightControllerLink-Anbindung (Doku 4.19). reset() setzt
  auf missionRun (Waypoint) zurueck.
- ui/widgets/flight_mode_pill.dart: PopupMenuButton-Pille "Mode: X" mit
  den vier Optionen Waypoint/Point and Fly/Return to Home/Manual (1:1
  aus dem HTML-Demonstrator uebernommene Labels).
- top_mode_bar.dart: Fly-Button setzt beim Eintritt in den Fly-Modus
  den Flugmodus zurueck auf Waypoint (Doku 3.11: "Standard-
  Rueckstellung ... beim Eintritt in Fly-Modus").

Layout-Bug beim Implementieren gefunden und behoben: Expanded(child:
Center(child: FlightModePill())) liess die Kopfzeile ueber den
gesamten Bildschirm expandieren (derselbe "Center/Align ohne Faktor
expandiert auf verfuegbare Constraints"-Fehler wie zuvor schon bei der
Bottom-Stats-Bar in dieser Session) - behoben durch Entfernen des
ueberfluessigen Center-Wrappers, da FlightModePill sein Zentrieren
bereits selbst per Container-alignment uebernimmt. Zusaetzlich einen
RenderFlex-Overflow bei langen Modusnamen ("Return to Home") behoben,
indem der Label-Text in Flexible mit TextOverflow.ellipsis gewrappt
wurde.

Getestet: 2 neue Widget-Tests (Fly-Button wechselt jetzt direkt in den
Fly-Modus statt eine Gate-Snackbar zu zeigen; Flugmodus-Dropdown
wechselt den Modus und setzt ihn beim erneuten Eintritt zurueck),
bestehender Gate-Test ersetzt, ein bestehender Test vereinfacht (die
Gate-Erfuellung vor toFly() ist nicht mehr noetig). Alle 118 Tests
sowie flutter analyze bestehen. Manuell auf dem Pixel_10a-Emulator
verifiziert: Fly-Button wechselt direkt um, Dropdown zeigt alle vier
Optionen, Auswahl uebernimmt das Label korrekt (auch bei langen Namen
ohne Overflow), Rueckstellung auf Waypoint bei erneutem Eintritt
funktioniert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 07:15:15 +02:00
Constantin LeueandClaude Sonnet 5 c5a8c68a8e Lock map rotation, add mission warnings list with per-issue navigation
Kartendrehung (Doku 4.6): flutter_map's Zwei-Finger-Twist-Geste bewusst
deaktiviert (InteractionOptions mit InteractiveFlag.all & ~rotate in
mission_map.dart) - die Drohne fliegt nordorientiert, eine gedrehte Karte
wuerde die Peilung von Wegpunkten/Reticle nur verwirren, zumal der HTML-
Demonstrator gar keine Drehung kennt.

Warnungsliste (Doku 3.10), analog dem #warningsBtn/#warningsPanel-Flow im
HTML-Demonstrator:

- domain/mission/mission_warnings.dart: computeMissionWarnings() sammelt
  alle aktuell zutreffenden Probleme (Route-Geometrie ausserhalb der
  Drohnenfaehigkeiten, zu starker Wind, zu geringer Bodenabstand,
  Reichweite/Ausdauer ueberschritten) zu einer Liste aus {text, action}-
  Eintraegen - reine Funktion aus bereits vorhandenen Zwischenergebnissen,
  unabhaengig von Riverpod/Widgets testbar.
- domain/wind/wind_math.dart: computeLegWindBad() ergaenzt (Bodenge-
  schwindigkeit <= 1 m/s oder Windgeschwindigkeit > Drohnen-Maximum) -
  bislang gab es nur die Windkomponente/Bodengeschwindigkeit selbst, aber
  keine "zu stark"-Markierung.
- ui/widgets/warnings_panel.dart: Vollbild-Liste, jede Zeile verweist per
  Aktionslabel auf den Loesungs-Screen ("Open altitude view"/"Open speed
  view"/"Show on map").
- ui/widgets/bottom_stats_bar.dart: roter Warnungs-Button neben den
  Mission-/Drohnen-Chips, nur sichtbar solange Warnungen aktiv sind.
- ui/widgets/waypoint_list_panel.dart: `_PanelTab` zu `PanelTab` public
  gemacht plus neuer `initialTab`-Parameter, damit das Panel direkt auf
  dem Altitude- oder Speed-Tab geoeffnet werden kann statt immer auf der
  Liste zu starten.
- ui/providers/map_controller_provider.dart: fitMapToWaypoints()-Hilfs-
  funktion extrahiert und in MapSearchControls' Fit-Button (vorher eigene
  Kopie der Logik) sowie fuer die "Show on map"-Warnungsaktion
  wiederverwendet.
- ui/screens/plan/plan_screen.dart: berechnet die Warnungsliste, ersetzt
  den bisherigen Einzel-Banner (nur "Route exceeds...") durch alle
  aktuellen Warnungstexte (durch " · " getrennt, wie im HTML-Demonstrator)
  und verdrahtet Warnungs-Button/-Liste mit der passenden Navigation.

Getestet: 4 neue Unit-Tests fuer computeLegWindBad, 7 fuer
computeMissionWarnings (inkl. Grenzfaelle: veraltetes Gelaendeprofil,
kein Limit bei maxRangeM/maxEnduranceMin <= 0), 2 neue Widget-Tests
(Warnungs-Button erscheint bei Reichweiten-Ueberschreitung und oeffnet
die Liste; Antippen einer Wind-Warnung oeffnet den Speed-Tab direkt).
Alle 110 Tests sowie flutter analyze bestehen. Manuell auf dem
Pixel_10a-Emulator verifiziert: Steigraten-Ueberschreitung (Altitude auf
220m bei kurzer Distanz) macht Route/Banner/Warnungs-Button rot,
Warnungsliste zeigt den Eintrag mit "Show on map", Antippen schliesst
die Liste und zoomt die Karte korrekt auf die Bounding-Box beider
Wegpunkte.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-29 19:51:42 +02:00
Constantin LeueandClaude Sonnet 5 b51c6c16d5 Add mission/drone profile sharing and importing, fix map not fitting to loaded mission
Sharing/Import (Doku 3.5/3.6), analog shareMission()/shareDrone() und den
importMissionInput/importDroneInput-Handlern im HTML-Demonstrator:

- services/sharing/mission_share_codec.dart, drone_share_codec.dart: reine
  Funktionen zum Bauen/Parsen des Export-JSON (appVersion/exportedAt-Umschlag),
  inkl. Erkennung einer abweichenden Hauptversion beim Import - vollstaendig
  unit-getestet ohne Plugin-Abhaengigkeit.
- services/database/waypoint_json_codec.dart: Waypoint-JSON-(De-)Serialisierung
  aus mission_repository.dart herausgezogen, damit Persistenz und Sharing
  exakt dasselbe Dateiformat verwenden statt es zu duplizieren.
- services/sharing/sharing_service.dart: duenne I/O-Schicht - schreibt eine
  temporaere Datei und oeffnet das native Share-Sheet (share_plus), bzw.
  liest eine vom Nutzer per Systemdialog ausgewaehlte JSON-Datei
  (file_picker). Als Klasse mit Instanzmethoden gehalten, damit sie sich in
  Tests durch einen Fake ersetzen laesst.
- ui/widgets/missions_drones_panel.dart: Share-Icon je Zeile, Import-Button
  im Toolbar beider Tabs.

Paket-Versionen bewusst gewaehlt: file_picker 11.0.2/share_plus 11.x wurden
zunaechst wegen einer win32-Konflikt-Aufloesung genutzt, kompilierten auf
diesem Projekt (AGP 9.0.1) aber nicht - file_picker < 12.0.0-beta.1 prueft
nur AGP-Version >= 9 und ueberspringt dann das Anwenden des Kotlin-Android-
Plugins, in der Annahme, AGPs eingebauter Kotlin-Support wuerde das
uebernehmen, was hier zu einem fehlenden compileReleaseKotlin-Task und
"Symbol nicht gefunden" fuehrte. file_picker >=12.0.0-beta.1 respektiert
zusaetzlich die bereits vom Flutter-Template gesetzte Gradle-Property
android.builtInKotlin=false und wendet das Plugin dann korrekt an -
file_picker auf ">=12.0.0-beta.1 <13.0.0" (share_plus zurueck auf ^13.3.0,
beide dann konsistent auf win32 ^6.x) gesetzt, um dies zu nutzen.

Zusaetzlich beim manuellen Durchtesten auf dem Pixel_10a-Emulator einen
zweiten, davon unabhaengigen Bug gefunden und behoben: CurrentMissionMeta-
Notifier.loadMission()/restoreLastSession() aktualisierten zwar Wegpunkte
und Missionsname, bewegten aber nie die Kartenkamera - eine geladene
Mission wurde dadurch mit falscher Kartenausschnitt angezeigt (Name/
Wegpunkte einer Stadt, Karte noch an der zuletzt betrachteten Stelle).
Fix: _fitMapToWaypoints() zentriert nach dem Laden auf die Bounding-Box
der Mission, analog der bereits vorhandenen _onFitPressed()-Logik in
MapSearchControls.

Getestet: 13 neue Unit-Tests fuer die Sharing-Codecs, 4 neue Widget-Tests
mit einem Fake-SharingService (kein echter Platform-Channel-Zugriff in
Tests). Alle 97 Tests sowie flutter analyze bestehen. Manuell auf dem
Pixel_10a-Emulator verifiziert: natives Share-Sheet oeffnet sich mit der
korrekten JSON-Datei, Datei-Import ueber den Systemdialog legt eine neue
Mission bzw. ein neues Drohnenprofil an, Kartensprung beim Laden einer
Mission funktioniert jetzt korrekt fuer sowohl manuelles Laden als auch
den Autosave-Restore beim App-Start.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 19:50:19 +02:00
Constantin LeueandClaude Sonnet 5 b176ad294e Add terrain/elevation data fetching (Doku 3.9)
Rundet das Altitude-Tab um ein reales Gelaendeprofil ab, damit sichtbar
wird, ob die geplante Flughoehe ausreichend Bodenabstand haelt:

- domain/mission/terrain_math.dart: reine Sampling-/Geometrie-Funktionen
  (routeKeyForTerrain fuer Caching, distanzbasiertes Sampling alle 30m,
  lineare Sollhoehen-Interpolation, terrainDangerRanges fuer Abschnitte
  mit < 10m Bodenabstand) - unabhaengig testbar ohne Netzwerk/UI.
- services/terrain/terrain_service.dart: laedt AWS-Terrarium-PNG-Kacheln
  (elevation-tiles-prod, zoom 13) und dekodiert sie ueber dart:ui
  (instantiateImageCodec/toByteData), ohne zusaetzliches Bildpaket.
  Elevation = R*256 + G + B/256 - 32768 pro Pixel; Kacheln werden pro
  Route nur einmal geladen (nach Kachel gruppierte Sample-Punkte).
- ui/providers/terrain_provider.dart: cached das geladene Profil ueber
  routeKeyForTerrain - reine Werteaenderungen (Hoehe/Speed) loesen keinen
  erneuten Kachel-Download aus, nur eine tatsaechliche Ortsverschiebung.
- ui/widgets/full_value_chart.dart: _paintDangerBands/_paintTerrain
  zeichnen rote Gefahrenbaender bzw. die braune Gelaende-Flaeche, in der
  gleichen Reihenfolge wie im HTML-Demonstrator (dangerHtml, grid,
  terrainHtml, dann Soll-Kurve obenauf).
- ui/widgets/waypoint_list_panel.dart: triggert ensureFor() beim
  Wechsel auf den Altitude-Tab sowie bei Routenaenderungen waehrend
  dieser aktiv ist (ref.listenManual auf currentMissionProvider).

Getestet: 13 neue Unit-Tests fuer die reine Geometrie/Sampling-Logik,
3 Service-Tests mit einer zur Testzeit synthetisch erzeugten PNG
(dart:ui-Encoding, keine Testasset-Datei noetig). Alle 80 Tests sowie
flutter analyze bestehen. Manuell auf dem Pixel_10a-Emulator verifiziert:
Altitude-Tab zeigt das reale Amsterdam-Gelaendeprofil (nahe Meereshoehe)
korrekt als Flaeche unter der Sollhoehen-Linie.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 18:38:23 +02:00
Constantin LeueandClaude Sonnet 5 926d805156 Location search starts a new mission; shrink and center header bar
Ortssuche verschob bisher nur die Karte - der HTML-Demonstrator beginnt
bei einer Suche immer eine neue, nach dem gefundenen Ort benannte
Mission (setMissionFromPlace(), aufgerufen aus doSearch()). Vorherige
Aenderungen gehen dabei nicht verloren, da flushPendingAutosave()
(hier: CurrentMissionMetaNotifier._flushNow()) zuerst noch ausstehende
Autosaves schreibt.

Die Nominatim-Suche wurde dafuer aus MapSearchControls in einen
eigenstaendigen NominatimService extrahiert (analog WindService):
injectable http.Client, damit sich die Suche in Tests ohne echten
Netzwerkzugriff ueberschreiben laesst. CurrentMissionMetaNotifier bekam
dafuer startNewFromPlace() (startNew() intern darauf umgebaut, um
Duplikation zu vermeiden).

Kopfleiste von 86%/64% (Plan-/Fly-Modus) auf einheitlich 70%
Bildschirmbreite verkleinert - zentriert war sie durch Align(topCenter)
+ FractionallySizedBox bereits strukturell korrekt, wirkte bei der
vollen Breite aber unausgewogen.

Verifiziert: flutter analyze (0 issues), flutter test (64/64, davon 4
neue NominatimService-Tests und 1 neuer Widget-Test fuer den
Missions-Reset bei Ortssuche), manuell auf Pixel_10a-Emulator - Suche
nach "Rotterdam" setzt Fusszeile auf "Mission: Rotterdam" mit 0
Wegpunkten trotz zuvor bestehender Mission mit Wegpunkten.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 15:22:36 +02:00
Constantin LeueandClaude Sonnet 5 aaec7f0f72 Add mission/drone profile management with autosave and persistence
Vervollstaendigt Doku 3.5/3.6/7.1/7.3/7.5: ein Vollbild-Verwaltungsmenue
fuer Missionen und Drohnenprofile (per Tabs umschaltbar, wie im HTML-
Demonstrator #missionsPanel gemeinsam mit der Profilliste), Autosave der
aktuell bearbeiteten Mission, Drift-Persistenz statt der bisherigen
In-Memory-only currentMissionProvider, sowie Anzeige/Zugang ueber neue
Chips in der Fusszeile (HTML-Demonstrator: #missionNameBtn/#droneNameBtn).

Datenbank (services/database/): Missions-Tabelle um updatedAt ergaenzt,
neue DroneProfiles- und AppSettingsTable-Tabellen (schemaVersion 2 mit
onUpgrade-Migration). Drei Repositories kapseln Drift-Zugriff +
JSON-(De-)Serialisierung der Wegpunktliste: MissionRepository,
DroneProfileRepository, AppSettingsRepository - je mit Unit-Tests gegen
eine In-Memory-Datenbank (NativeDatabase.memory()).

Provider: CurrentMissionMetaNotifier haelt Name/id der aktuellen Mission
und autosaved sie 800ms-debounced (identisch zum HTML-Demonstrator:
scheduleAutosave()/flushPendingAutosave()) - inklusive Wiederherstellung
des zuletzt bearbeiteten Autosave-Drafts beim App-Start (Doku 7.5).
ActiveDroneProfileNotifier haelt das aktive Profil, seedet beim ersten
Start automatisch T1 Ranger und merkt sich die Auswahl ueber
AppSettingsTable neustart-fest.

UI: MissionsDronesPanel (Missions-Tab: Liste mit Umbenennen/Loeschen/
Laden; Drones-Tab: Liste mit Bearbeiten/Loeschen/Auswaehlen, faellt beim
Loeschen des aktiven Profils auf das naechste zurueck). DroneProfileEditor
als Formular (bewusste Vereinfachung gegenueber den Swipe-Gesten-Chips
des HTML-Demonstrators - bei elf Feldern ist ein normales Formular auf
einem Mobilgeraet zugaenglicher). Rename per AlertDialog+TextField statt
Browser-prompt().

Aktives Drohnenprofil ist jetzt tatsaechlich wirksam statt nur eine feste
Anzeige: Speed-/Alt-Drehrad-Grenzen, Fangradius neuer Wegpunkte und die
Flugpfad-Machbarkeitspruefung (Kurvenradius, Steig-/Sinkrate) in
PlanScreen und WaypointListPanel lesen jetzt activeDroneProfileProvider
statt der bisherigen statischen DefaultDroneProfile-Konstanten (die als
T1-Ranger-Seed-Werte weiterleben).

Beim Verifizieren zwei echte Bugs in BottomStatsBar gefunden und
gefixt: ein Stack+Align-ohne-Factor blaehte sich auf unbegrenzte Groesse
auf und verschob die Details-Pille aus ihrer sichtbaren Position (durch
zwei gleich grosse Expanded-Bereiche ersetzt); zwei ConstrainedBox-Chips
nebeneinander verursachten auf schmaleren Breiten einen RenderFlex-
Overflow (durch Flexible ersetzt).

Widget-Tests: AppShell/PlanScreen initialisieren beim Start jetzt die
echte Datenbank - alle Tests, die AppShell pumpen, ueberschreiben
appDatabaseProvider testweise mit einer In-Memory-Instanz. Ausserdem
mussten mehrere Tests den neuen 800ms-Autosave-Timer abwarten (wie
zuvor schon beim WaypointChip-Flash-Timer etabliert), damit nach
Testende kein Timer mehr aussteht.

Verifiziert: flutter analyze (0 issues), flutter test (59/59, davon 14
neue Repository-Tests), manuell auf Pixel_10a-Emulator - Wegpunkte
werden automatisch als "New mission" gespeichert und erscheinen in der
Liste, Umbenennen/Laden/Loeschen funktionieren, neues Drohnenprofil mit
abweichender Max-Speed wird angelegt+aktiviert+in der Fusszeile
angezeigt, Loeschen des aktiven Profils faellt auf das verbleibende
zurueck.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 14:59:09 +02:00
Constantin LeueandClaude Sonnet 5 1b13c45cac Add wind analysis: header pill, fetch/validate menu, speed-plot overlay
Portiert das Wind-System des HTML-Demonstrators (Doku 3.8) vollstaendig:
Open-Meteo-Winddaten, interpoliert zwischen Druckflaechen- und festen
Nabenhoehen-Stuetzpunkten auf die tatsaechliche Zielhoehe.

Domain (lib/domain/wind/wind_math.dart, reines Dart): Hoehen-
Interpolation (linear + zirkulaer fuer Windrichtung), Peilung zwischen
zwei Punkten, Kopf-/Rueckenwind-Komponente pro Flugleg - 1:1 uebernommen
aus interpolateWindAtHeight()/bearingBetween()/headTailwindComponent()
des Demonstrators, per Unit-Tests abgesichert.

Service (lib/services/wind/wind_service.dart): zwei getrennte,
fokussierte Open-Meteo-Requests statt eines kombinierten (Doku 4.10 -
ein 35-Variablen-Request verursachte im Demonstrator Modellauswahl-
Fehler). Drei Verbraucher: Kopfleisten-Pille (120 m AGL ueber
Referenzort), gebuendelter Wegpunkt-Fetch (ein Request fuer alle
Koordinaten via Open-Meteos Mehrfach-Location-Syntax) und das
Validierungspanel (rohe Messwerte je Nabenhoehe/Druckflaeche). Per
MockClient getestet (kein echter Netzwerkzugriff in Tests).

UI:
- HeaderWindPill (TopModeBar, nur ausserhalb Fly-Modus): zeigt
  Windrichtung/-geschwindigkeit, Antippen schaltet perspektivisch die
  Wind-Marker der Wegpunkte um (Zustand vorbereitet, HTML-Demonstrator:
  showPerWaypointWind).
- WaypointListPanel: "Fetch wind"/"Validate"-Buttons im Panel-Header,
  neue WIND-Spalte in der Liste.
- WindValidatePanel: neue Vollbild-Route (gleiches Muster wie
  WaypointListPanel) mit Rohdaten je Hoehenstufe inkl. Boeen (Doku 4.9:
  Boeen nur hier, nicht in der Wegpunktanzeige) und Interpolationsergebnis.
- FullValueChart (Speed-Tab): Kopf-/Rueckenwind-Diamanten pro Leg samt
  gestrichelter Nulllinie sowie gestrichelte Bodengeschwindigkeits-
  Stufenlinie.

Bug gefunden und gefixt waehrend der Emulator-Verifikation: die
Y-Achsen-Autoskalierung klemmte weiterhin auf den gueltigen Drehrad-
Wertebereich (13-25 m/s), wodurch nahe Null liegende Windkomponenten
(bei schwachem Wind der Normalfall) ausserhalb des sichtbaren Bereichs
lagen und weder Diamanten noch Nulllinie zu sehen waren. Der HTML-
Demonstrator klemmt computeSpeedChartRange() bewusst nicht auf SPD_MIN/
SPD_MAX; das Klemmen in FullValueChart._range() jetzt entsprechend
entfernt.

Waypoint-Modell um windSpeedMs/windDirFromDeg/windElevationM erweitert
(nullable, reine Planungshilfe - der Encoder liest diese Felder nie,
Doku 3.2). currentMissionProvider bekommt replaceAll() fuer den
gebuendelten Wind-Fetch, der alle Wegpunkte gleichzeitig aktualisiert.

Verifiziert: flutter analyze (0 issues), flutter test (44/44, davon 14
neue Wind-Mathematik- und 6 neue WindService-Tests), manuell auf
Pixel_10a-Emulator mit echtem Open-Meteo-Netzwerkzugriff - Kopfleisten-
Pille zeigt Live-Wind, Fetch/Validate fuellen Liste bzw. oeffnen das
Validierungspanel mit echten Messwerten, Speed-Plot zeigt nach dem Fix
Diamanten/Nulllinie/Bodengeschwindigkeit korrekt skaliert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 13:32:15 +02:00
Constantin LeueandClaude Sonnet 5 13da935f99 Merge search/home/fit controls into the Plan/Fly header pill
Suche, Home- und Fit-Button sassen bisher in einer eigenen Leiste unter
der Plan/Fly-Kopfleiste - ein reiner UI-Kompromiss, weil die Karte
(inkl. MapController) in PlanScreen lebte und von der aeusseren
Kopfleiste aus nicht erreichbar war. Der HTML-Demonstrator zeigt
Suche/Home/Fit dagegen als Mittelteil derselben Leiste wie die
Modus-Buttons (#searchWrap/#homeBtn/#fitBtn in #topBarMiddle) - das war
also keine Stilfrage, sondern eine Datenfluss-Frage.

Loesung: MapController aus einem PlanScreen-privaten Feld in einen
app-lebenslangen mapControllerProvider (Riverpod) gehoben. Damit kann
die neue MapSearchControls (ersetzt TopNavBar) direkt in TopModeBar
eingebettet werden und lebt architektonisch dort, wo sie hingehoert:
bei der Kartensteuerung, nicht als Kind von PlanScreen.

Als Nebeneffekt - tatsaechlich der wichtigere Punkt - bleibt die
Kamera-Position/Zoom jetzt auch beim Wechsel Plan -> Fly -> Plan
erhalten. Verifiziert per flutter_map-Quellcode (FlutterMap haengt
einen extern uebergebenen Controller beim Neu-Mounten nicht an und
disposed ihn nicht) sowie per neuem Widget-Test, der den Cubit direkt
durch das Ready-to-Fly-Gate schickt (der Fly-Modus ist ueber die UI
aktuell nicht erreichbar, da Upload/Verify noch nicht implementiert
ist) und die Kamera vor/nach dem Wechsel vergleicht.

Zusaetzlich: _onMapEvent unterscheidet jetzt per MapEventMove.source,
ob eine Kartenbewegung programmatisch (mapController, z.B. durch Suche/
Home/Fit) oder per Geste ausgeloest wurde, und triggert den
Snap-to-Edit-Handler entsprechend nur bei Gesten bzw. sofort bei
programmatischen Moves - das ersetzt den bisherigen Ansatz, an jeder
Aufrufstelle manuell _handleMoveEnd() zu rufen, der nicht mehr skaliert
sobald diese Aufrufstellen (wie jetzt) in einem anderen Widget liegen.

Verifiziert: flutter analyze (0 issues), flutter test (22/22, inkl.
neuem Kamera-Persistenz-Test), manuell auf Pixel_10a-Emulator (Release-
Build) - Suche/Home/Fit erscheinen fusioniert in der gruenen Pille,
Kartenverschiebung bleibt nach Wechsel in den (durch das Gate weiterhin
blockierten) Fly-Modus sichtbar unveraendert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 09:37:22 +02:00
Constantin LeueandClaude Sonnet 5 d3c5a68636 Add waypoint list panel and bottom stats bar
- BottomStatsBar: persistent "Details: N WP · dist m · duration" pill
  (HTML prototype's #bottomBar/#statsRow), tap opens the full waypoint
  list. Mission/drone name and the Send button are intentionally not part
  of this - mission library and upload aren't wired up yet.
- A warn banner ("Route exceeds drone capabilities at highlighted point")
  appears above it whenever buildRouteGeometry finds a bad vertex or leg -
  a direct, low-effort payoff from the fillet geometry work.
- WaypointListPanel: full-screen overlay listing every waypoint
  (altitude/speed/execute/catch radius), matching the prototype's List
  tab. Only one row editable at a time; editing swaps plain text for
  draggable WaypointChip values and an execute dropdown. Row tap (outside
  edit mode) centers the map on that waypoint and closes the panel; delete
  removes it and adjusts the reticle's target index the same way the halo
  menu's remove already does.
- WaypointChip: vertical-drag value chip (HTML prototype's .value-chip),
  fixed step of 10 per 14px matching chipFieldConfig - simpler than the
  original's dual vertical/horizontal axis-lock gesture, which felt like
  more fidelity than the interaction warrants here.
- Provider gained setCatchRadius and setAction (direct assignment, as
  opposed to toggleAction's toggle-to-none semantics used by the halo
  menu) to support the list's editable fields.
- Altitude/Speed full-chart tabs from the prototype (drag-to-adjust
  altitude/speed profile charts) are a separate, larger feature and not
  part of this step.

Centering on a waypoint from the list calls MapController.move(), which -
like the reticle's auto-snap - doesn't emit MoveEnd, so snap detection is
invoked manually afterward to match the prototype's resulting behavior.

Added 4 widget tests (open empty list, row-tap centers and closes, delete
removes and updates the empty state, chip drag updates altitude) - one
required a pump(duration) after the drag to flush the chip's 120ms flash
Timer, which was otherwise still pending at test teardown. All 15 tests
and flutter analyze pass. Manually verified on the Pixel_10a emulator:
opened the list with two waypoints, edited a row's altitude via chip drag
(confirmed value change, accounting for device-pixel-ratio scaling vs the
widget-test logical-pixel case), and confirmed row-tap re-centers/closes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 07:25:32 +02:00
Constantin LeueandClaude Sonnet 5 b93bbab23b Add fillet flight path geometry and Alt/Speed wheels
- RouteGeometry (lib/domain/mission/route_geometry.dart): physically
  grounded flight path - straight segments + tangential arcs at each
  course change, sized from the drone's minimum turn radius (doc 3.7/4.6),
  replacing a naive spline that would suggest unrealistic turn radii.
  Marks a vertex "bad" (turn angle >= 160°, tangent length exceeding 90%
  of either adjacent leg, or exceeding the waypoint's catch radius) and a
  leg "bad" when the required climb/descent rate exceeds the drone's
  max climb/descent rate. Pure Dart, no Flutter dependency, so it stays
  usable if the mission domain is ever split into its own package (4.22).
  Wind-based turn-radius correction from the prototype isn't ported yet -
  the wind system (doc 3.8) doesn't exist in the Flutter app.
- MissionMap now draws each RouteSegment as its own Polyline, colored red
  when bad instead of a single plain white line; waypoint markers turn red
  too when their vertex is bad.
- ValueWheel: vertical drag-to-adjust tape control (HTML prototype's
  #altWheel/#spdWheel), custom-painted tick marks, blue center indicator,
  gradient fade at top/bottom. Wired into PlanScreen on both screen edges
  with Alt/Speed readouts.
- curAlt/curSpeed now live in PlanScreen state instead of fixed constants:
  dropping a new waypoint uses whatever the wheels are currently set to;
  entering editing mode on an existing waypoint loads its values into the
  wheels; adjusting a wheel while editing writes live into that waypoint
  (provider gained setAltitude/setSpeed to match the existing
  moveWaypoint/toggleAction pattern).

Added dedicated unit tests for the geometry (empty list, straight line,
feasible 90° turn producing a line-arc-line segment sequence, an
infeasible near-180° turn, and an infeasible descent rate) rather than
relying on eyeballing it on the emulator - this is exactly the kind of
ported-math correctness that's hard to verify visually but easy to get
subtly wrong. All 11 tests (previous 6 + these 5) and flutter analyze
pass. Also manually verified the wheels on the Pixel_10a emulator: drag
changes the value and the on-screen readout in real time.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 23:13:24 +02:00
Constantin LeueandClaude Sonnet 5 6dcb765b80 Add waypoint route line, snap-to-edit, and halo action menu
Full reticle interaction state machine (idle/snapped/editing), matching
the HTML prototype's mode variable:

- idle: reticle shows "+", tap drops a new waypoint at the map center.
- snapped: after any pan ends, if the map center lands within the
  reticle's on-screen radius (converted to meters at the current
  zoom/latitude) of an existing waypoint, the view auto-recenters onto it
  and the reticle switches to a pencil icon. Tap to enter editing.
- editing: reticle shows a checkmark; panning the map now live-updates the
  target waypoint's position (drag-to-reposition) instead of moving the
  camera "away" from it, and the HaloMenu radial menu appears.

HaloMenu: three ring-wedge hit regions (custom ClipPath/CustomClipper,
angles/radii taken from the prototype's buildRingWedgePath) for
Loiter/Landing/Remove, with Material icons standing in for the
demonstrator's hand-drawn SVGs. Loiter/Landing toggle the waypoint's
action and drop back to snapped; Remove deletes it and returns to idle.

MissionMap now draws a straight white polyline connecting all waypoints
in order (the physically-grounded straight-line + fillet-arc geometry
from doc 3.7/4.6 is still open - this is a straight-line placeholder) plus
a dashed rubber-band preview from the last waypoint to the live map center
while idle.

Note on porting from Leaflet: flutter_map's MapController.move() emits
MapEventMove, not MapEventMoveEnd, so (unlike the prototype's panTo()) the
auto-recenter-on-snap doesn't re-trigger snap detection - no "autoPanning"
guard flag was needed.

Verified with flutter analyze, flutter test (three new tests: halo menu
open/close, remove, loiter toggle - the wedge tap tests had to target the
icon's exact center via tapAt(), since a wedge's bounding-box center falls
outside its actual donut-segment hit area), and a full manual pass on the
Pixel_10a emulator: idle -> add -> snap (auto-recenter confirmed) -> edit
-> drag-reposition (confirmed via re-snap after further panning showing
the moved position, not the original one) -> halo remove -> back to idle,
plus the connecting line rendering correctly between two waypoints set
far apart.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 22:56:06 +02:00
Constantin LeueandClaude Sonnet 5 db6b46140c Add center reticle control for dropping waypoints
- ReticleButton: fixed-at-screen-center "+" circle (HTML prototype:
  #centerCluster/#reticleBtn), tapping it drops a waypoint at the current
  map center.
- CurrentMissionNotifier (Riverpod): holds the waypoint list for the
  mission being edited. Reactive single-value state per doc 4.16, so
  Riverpod rather than a new Bloc/Cubit.
- MissionMap now renders each waypoint as a CircleLayer marker (white
  border, dark fill, matching the prototype's L.circleMarker styling).
- PlanScreen owns the MapController needed to read the camera center on
  tap, and wires the reticle to the provider.

Default altitude/speed/catch-radius (60m / 15 m/s / 60m) are hardcoded to
the T1 Ranger default profile values from the prototype for now - TODO
once drone profile settings (doc 3.5) exist.

Verified with flutter analyze, flutter test (incl. a new test covering the
add-waypoint path), and a real run on the Pixel_10a emulator: panned the
map and dropped two waypoints, both markers stayed correctly georeferenced.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 22:21:35 +02:00