Skip to content

adds gemini & googlemaps#36

Merged
Josephrp merged 7 commits intodevfrom
gemini
Mar 15, 2026
Merged

adds gemini & googlemaps#36
Josephrp merged 7 commits intodevfrom
gemini

Conversation

@Josephrp
Copy link
Copy Markdown
Owner

@Josephrp Josephrp commented Mar 15, 2026

Greptile Summary

This PR adds two major capabilities: Google Maps and OpenStreetMap (Leaflet) dual-provider map support in the React frontend, and Gemini as a new LLM provider throughout the Python backend. It also introduces the /gis/emergency-events endpoint, a unified OperatorMap component with provider-switching, and supporting utilities (escapeHtml, googleMapsLoader, mapSourceConfig).

A significant number of issues raised in previous review rounds have been addressed in this PR:

  • googleMapsLoader.ts now clears the cached promise on failure (allowing retries)
  • OperatorMapLeaflet uses useEffect in ChangeView instead of useMemo for side effects
  • Leaflet marker icons are now imported via the bundler rather than the unpkg CDN
  • DOMPurify is applied to all dangerouslySetInnerHTML in Leaflet popups
  • vite-env.d.ts now declares all VITE_ env vars consumed by the map config module
  • since query param on /gis/emergency-events is typed datetime | None so FastAPI validates format before the DB is touched
  • All infoHtml template literals use the new escapeHtml utility

Key issues found in this round:

  • OperatorMap.tsx (logic): The color field on OperatorMapMarker is set by emergencyToMarker (red/green per status) but is never applied in the google.maps.Marker constructor, making emergency pins visually identical to operator pins on the Google Maps provider.
  • factory.py (logic): The Gemini model-prefix guard uses "gemini/" in model (substring) rather than model.startswith("gemini/"), which is the pattern used consistently for every other provider in the same function and which avoids false positives on model strings that happen to contain "gemini/" mid-string.
  • OperatorMap.tsx (style): clearMarkers is referenced in the init useEffect cleanup but missing from its empty dependency array; stable reference so no runtime impact, but will trigger react-hooks/exhaustive-deps lint warnings.

Confidence Score: 3/5

  • Safe to merge with minor fixes — the color field omission and substring prefix check are both small, targeted corrections before merge.
  • The PR resolves all prior critical security and correctness issues (XSS, rejected-promise caching, stale center/zoom, useMemo side effect, CDN icons, missing vite-env declarations). Two new logic issues were found: emergency marker colour is not applied in the Google Maps path (functionality regression for the emergency overlay), and the Gemini model-prefix guard uses a substring check instead of startsWith. Neither is a security issue, but the colour omission means the emergency-event layer is not usable as intended on Google Maps until fixed.
  • radioshaq/web-interface/src/components/maps/OperatorMap.tsx (emergency marker colour) and radioshaq/radioshaq/orchestrator/factory.py (Gemini model prefix guard).

Important Files Changed

Filename Overview
radioshaq/web-interface/src/components/maps/OperatorMap.tsx New Google Maps implementation; color field declared on OperatorMapMarker and set on emergency markers but silently ignored when constructing google.maps.Marker — emergency events are visually indistinguishable from operator markers on the Google provider. Init useEffect([], []) references clearMarkers in cleanup but omits it from the dependency array (stable ref, not a runtime bug but triggers exhaustive-deps lint).
radioshaq/radioshaq/orchestrator/factory.py Adds Gemini provider routing. The prefix guard uses "gemini/" in model (substring match) instead of model.startswith("gemini/"), so a model name like "pre-gemini/v1" would incorrectly bypass the prefix addition.
radioshaq/radioshaq/llm/client.py Refactored API-key resolution into _resolve_api_key() with provider-matched env lookup before the generic fallback chain; addresses prior cross-provider contamination at the provider-key level. Gemini is properly wired in the provider map.
radioshaq/radioshaq/database/postgres_gis.py Adds get_emergency_events_with_locations using a parameterised text() query with PostGIS geometry extraction; no SQL injection risk. Also adds id and last_seen_at to the operators-nearby response for stable marker keys.
radioshaq/radioshaq/api/routes/gis.py Adds /gis/emergency-events endpoint; since is now typed as `datetime
radioshaq/web-interface/src/maps/googleMapsLoader.ts New singleton loader for Google Maps JS API; correctly clears loadPromise on failure (addresses prior permanent-cache-on-rejection bug) and exposes isGoogleMapsConfigured() helper.
radioshaq/web-interface/src/components/maps/OperatorMapLeaflet.tsx New Leaflet/OSM implementation; addresses prior feedback — marker icons imported from bundler (no CDN), ChangeView uses useEffect (not useMemo), popups sanitized with DOMPurify.
radioshaq/web-interface/src/utils/escapeHtml.ts New XSS-escape utility covering &, <, >, ", '; used consistently across all infoHtml template literals in new map components.

Sequence Diagram

sequenceDiagram
    participant UI as React UI (MapPage)
    participant OM as OperatorMap
    participant GL as googleMapsLoader
    participant OML as OperatorMapLeaflet
    participant API as FastAPI Backend
    participant DB as PostGIS DB

    UI->>API: GET /gis/operators-nearby?lat&lng&radius
    API->>DB: get_operators_nearby()
    DB-->>API: [{ id, callsign, lat, lon, last_seen_at }]
    API-->>UI: { operators, count }

    UI->>API: GET /gis/emergency-events?since&status&limit
    API->>DB: get_emergency_events_with_locations()
    DB-->>API: [{ id, latitude, longitude, status, ... }]
    API-->>UI: { events, count }

    UI->>OM: <OperatorMap markers=[ops+emergency] />
    alt provider = 'google'
        OM->>GL: loadGoogleMaps()
        GL-->>OM: google namespace
        OM->>OM: new google.maps.Marker (color ignored ⚠️)
    else provider = 'osm'
        OM->>OML: <OperatorMapLeaflet markers />
        OML->>OML: DOMPurify.sanitize(infoHtml)
        OML-->>UI: Leaflet map with markers
    end

    note over UI,API: LLM config flow
    UI->>API: PATCH /api/v1/config/llm { provider: "gemini", gemini_api_key }
    API->>API: factory._llm_model_string → "gemini/gemini-2.5-flash"
    API->>API: _llm_api_key_from_llm_config → gemini_api_key
    API->>API: LLMClient._resolve_api_key() → GEMINI_API_KEY env
Loading

Comments Outside Diff (4)

  1. radioshaq/radioshaq/llm/client.py, line 78-86 (link)

    API key fallback chain may send wrong key to provider

    The API key resolution chain (MISTRAL_API_KEY or OPENAI_API_KEY or ANTHROPIC_API_KEY or HF_TOKEN or ... or GEMINI_API_KEY) means if a user has both MISTRAL_API_KEY and GEMINI_API_KEY set, and the model is gemini/..., the Mistral key will be used for the Gemini API call because it appears first. The factory (_llm_api_key_from_llm_config) correctly matches by provider, but the LLMClient fallback does not. This could cause confusing authentication errors. Consider making the LLMClient env-var chain aware of the model prefix, or documenting that api_key should always be passed explicitly (which the factory does).

  2. radioshaq/web-interface/src/features/map/MapPage.tsx, line 682-685 (link)

    center missing from useEffect dependency array

    fetchNearby is called with center.lat and center.lng from state, but neither is listed as a dependency. react-hooks/exhaustive-deps will warn on this, and it means the effect won't re-run if center changes independently of radiusKm. While the current control flow (center updates only come from inside fetchNearby) happens to work, the stale-closure risk makes this fragile.

  3. radioshaq/web-interface/src/maps/googleMapsLoader.ts, line 39-51 (link)

    Cached rejected promise prevents map recovery

    loadPromise is assigned once and never reset on failure. If importLibrary('maps') rejects — due to a transient network error, quota limit, or misconfiguration — the cached rejected promise is returned to every subsequent caller. The map becomes permanently broken for the session until the user reloads the page.

    The fix is to clear loadPromise inside the .catch() handler (set it back to null) before re-throwing the error, so the next loadGoogleMaps() call can make a fresh attempt rather than immediately returning the same rejected promise.

  4. radioshaq/radioshaq/llm/client.py, line 75-91 (link)

    Gemini env key shadowed by earlier fallback entries

    GEMINI_API_KEY is the last entry in the or-chain. In any environment where MISTRAL_API_KEY, OPENAI_API_KEY, or ANTHROPIC_API_KEY is also present — which is common when running multiple providers — a Gemini call will silently receive the wrong key, causing an authentication error that is hard to diagnose.

    The existing code comment already flags this: "Consider making this model-aware". A minimal fix would be to select the env var based on the configured provider (matching the existing pattern in factory.py_llm_api_key_from_llm_config) rather than trying all keys in a fixed order. The fallback chain would only be used as a last resort for unknown providers.

    The same issue exists in the chat_with_tools method further down in this file.

Last reviewed commit: cdddb2b

Greptile also left 2 inline comments on this PR.

@Josephrp
Copy link
Copy Markdown
Owner Author

@greptileai , comprehensively update your review based on the changes above :

@Josephrp
Copy link
Copy Markdown
Owner Author

@greptileai , comprehensively update your assessment , review out of diff review , and comments based on the changes above:

@Josephrp
Copy link
Copy Markdown
Owner Author

@greptileai , comprehensively update your assessment , review out of diff review , and comments based on the changes above:

@Josephrp
Copy link
Copy Markdown
Owner Author

@greptileai , comprehensively update your assessment , review out of diff review , and comments based on the changes above:

@Josephrp Josephrp merged commit 4960690 into dev Mar 15, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant