Commit Graph

117 Commits

Author SHA1 Message Date
Logan 961cc6f36e add button to clear stale 'active' calls 2026-06-21 23:45:28 -04:00
Logan d290b89736 New /profile page
Avatar (initials) + display name, email, admin badge
Account section: email, UID, role, join date, last sign-in
Discord section: link status with username/user ID/linked date, or the get-code flow if unlinked, plus unlink button
Sign out button at the bottom
2026-06-21 23:31:10 -04:00
Logan 758c6f4115 discord link banner 2026-06-21 23:23:36 -04:00
Logan 6ae4d398f8 add trips permissions 2026-06-21 20:00:48 -04:00
Logan 981f03ac06 allow overlap (note) tags 2026-06-21 15:52:15 -04:00
Logan 47430827d4 Fix discord trip itinerary 2026-06-21 15:47:07 -04:00
Logan 4dd3343026 add event editing 2026-06-21 15:35:57 -04:00
Logan fce189d8c9 assistant updates 2026-06-21 15:11:30 -04:00
Logan 3fb3bca034 add tags
Trip-level tags: admins configure available tags in the trip header (inline add/remove pills). The AI can also create new tags via the add_tag tool.
Event tags: selectable in the Add Event modal, shown as colored pills on event cards in the timeline, and on AI suggestion cards.
AI integration: sees available tags in its system prompt, applies them when proposing events, can create new ones with add_tag.
Discord: tags shown as inline code blocks under each event in /trip view.
Colors: auto-assigned from an 8-color palette by tag index, consistent everywhere.
2026-06-21 15:00:37 -04:00
Logan a0fdf2486e chat fixes
Focus: textarea gets refocused via inputRef after the AI response (or error) lands
Persistence: chat history saved to localStorage keyed by trip ID, loaded on mount — survives refreshes
2026-06-21 14:55:34 -04:00
Logan e7622c7e6d chat box fixes 2026-06-21 14:47:17 -04:00
Logan 21d15d0426 assistant markdown update 2026-06-21 14:38:53 -04:00
Logan 21268ab477 fix: migrate Places and Routes to new GCP APIs
Switch from legacy Places textsearch and Directions APIs (disabled on
this project) to Places API (New) and Routes API (New). Both places.py
and the assistant's _places_search helper updated. Also fixes uid()
recursive self-call in trips page and adds Places API response logging.
2026-06-21 14:35:12 -04:00
Logan 522748f07a debugging for trips assistant 2026-06-21 14:31:26 -04:00
Logan af4079d648 fix build 2026-06-21 14:15:09 -04:00
Logan 39c002d090 Fix assistant 2026-06-21 14:08:33 -04:00
Logan 18d96193ab Security fixes
auth.py

secrets.compare_digest replaces == for service key comparison (timing-safe)
Added require_service_key — bot-only endpoints (trip/event join/leave)
Added require_service_key_or_admin — node commands/config (bot via service key OR dashboard admin via Firebase)
Added _RateLimiter with three shared instances: trip_chat_limiter (20/5min per user), summarize_limiter (5/10min per incident), bootstrap_limiter (2/hr per system)
nodes.py

send_command and assign_system now require require_service_key_or_admin — the Discord bot can still call them via service key, but regular Firebase users are blocked
tokens.py

add_token, flush_tokens, set_preferred_system, delete_token all require require_admin_token
Token masking changed from token[:10] + "…" + token[-4:] to "•••" + token[-4:]
systems.py

All write endpoints (create, update, delete, ai-flags, ten-codes, vocabulary writes, bootstrap) now require require_admin_token
bootstrap_vocabulary also calls bootstrap_limiter.check(system_id)
incidents.py

POST /incidents/summarize (bulk) now requires require_admin_token
POST /incidents/{id}/summarize now calls summarize_limiter.check(incident_id)
trips.py

join_trip, leave_trip, join_event, leave_event require require_service_key — only the Discord bot can set Discord attendee identity
delete_trip, delete_event require require_service_key_or_admin
trip_chat rate-limited per caller UID, history stripped to user/assistant roles only, user message truncated to 2000 chars, Maps query strings capped at 200 chars
upload.py

Rejects files larger than settings.upload_max_bytes (default 100MB) with 413
storage.py

_safe_audio_filename() derives GCS object name from call_id + allowlisted extension, completely ignoring the client-supplied filename
config.py

Added upload_max_bytes: int = 100 * 1024 * 1024
Both Dockerfiles — python:3.14-slim → python:3.12-slim
2026-06-21 13:40:08 -04:00
Logan f0a0ea508a adjust assistant height 2026-06-21 13:19:45 -04:00
Logan d64259bb18 Fix auth 2026-06-21 10:14:52 -04:00
Logan 7b9aefbcc5 Add UI to trips 2026-06-21 10:12:33 -04:00
Logan 8edb717dd2 Add trips to UI
lib/types.ts — TripRecord and TripEvent types

lib/c2api.ts — getTrips, getTrip, createTrip, deleteTrip, createTripEvent, deleteTripEvent

lib/useTrips.ts — Firestore realtime hook on the trips collection, ordered by start date

app/trips/page.tsx — List page split into Upcoming / Past sections, card click navigates to detail, "+ New Trip" modal for admins with all fields including date range and maps link

app/trips/[id]/page.tsx — Detail page fetched via C2 API (gets trip + events in one call), day-by-day itinerary with time, location, maps link, notes, and Discord attendees. Add Event modal (date constrained to trip range). Admin-only delete trip + remove event.

components/Nav.tsx — Trips link added to the nav
2026-06-20 23:34:45 -04:00
Logan fb096d582d feat: add /trip slash commands + add trips & itinerary system
New /trips router with full CRUD, attendee management, and nested
events. Events validate date is within parent trip range and inherit
trip location when not explicitly set. Leaving a trip cascades
removal from all its events.

New TripCommands cog with /trip create, list, view, delete, join,
leave and /trip event add, remove, join, leave. Event autocomplete
is scoped to the selected trip. Enforces must-be-on-trip rule for
event joins with a clear error message.
2026-06-20 23:25:08 -04:00
Logan a4962d7b0e map fixes 2026-06-20 23:19:41 -04:00
Logan 4e0e0fc79f Backend (incident_correlator.py):
- Create path (line ~1274): title only uses "at {location}" when location_coords is also set
- Update path (line ~1226): same guard — best_coords must be truthy alongside best_location

Frontend (MapView.tsx):
- Desktop sidebar: cards with location_coords → <button> fly-to; cards without → <a href> that navigates to the incident page with "View details →" text
- Mobile drawer: same split — with coords fly-to+close, without coords navigate via <a>
- Removed the "no coords" italic placeholder text; the card behavior itself makes it clear
2026-06-07 03:34:15 -04:00
Logan e55412d8c7 UI Updates
app/map/page.tsx

Removed IncidentCard component and the incidents grid below the map — the on-map sidebar inside MapView is the single display
Moved kiosk exit button from top-3 left-3 (overlapping zoom controls) to bottom-[5.5rem] left-3
components/MapView.tsx

Fixed popup "View incident →" link — adds stopPropagation() + window.location.href to prevent Leaflet intercepting the click
Added "View details →" link on each sidebar incident card so you can navigate from the map panel without opening a popup
Added "News Alerts" overlay layer (placeholder, ready for RSS/feed integration)
lib/types.ts

Added preferred_token_id?: string | null to SystemRecord
lib/c2api.ts

Added setPreferredToken(tokenId, systemId) calling PUT /tokens/{tokenId}/prefer/{systemId} (backend already existed)
app/systems/page.tsx

Added PreferredTokenPanel component — loads the token pool lazily on expand, shows radio buttons to set/clear the preferred token, displayed on each system card above the AI flags panel
2026-06-03 01:08:21 -04:00
Logan 9842b18799 Fix correlation false-merge, switch STT to whisper-1 without vocab prompt
- correlator: unit_overlap on dispatch channels now applies content
  divergence check when the call has geocoded coords but the incident
  doesn't; previously this gap caused unrelated calls to merge into
  stale incidents (e.g. patrol officer at a second scene 70 min later)
- STT: switch default model from gpt-4o-transcribe to whisper-1, which
  faithfully transcribes all exchanges in multi-PTT recordings; gpt-4o
  was silently dropping utterances, starving the correlation engine
- STT: remove vocabulary from the Whisper prompt; whisper-1 echoes
  prompted terms into noise/silence, skewing extracted incident data;
  vocabulary context is now applied exclusively in the GPT extraction
  step (build_gpt_vocab_block) where it is used as reference only
2026-06-03 00:51:25 -04:00
Logan fe6bf55c0e Fix fetch failure 2026-06-03 00:19:12 -04:00
Logan f65873d690 Fix TypeScript key prop error on SourceCallPlayer map
Wrap SourceCallPlayer in Fragment to avoid the broken JSX env treating
key as a component prop on the custom component.
2026-06-01 01:56:51 -04:00
Logan 913fe0cbee Add source call audio playback to vocabulary suggestions
When the induction loop proposes a new vocabulary term, it now records
which sampled call(s) most likely produced the suggestion. Admins see
a collapsible "▶ source" player under each pending term showing the
audio clip and transcript, so they can hear what was actually said
before approving or dismissing.

- vocabulary_learner: track sampled call docs, attach source_call_ids
  to each pending term via word-overlap search with fallback
- types: VocabularyPendingTerm.source_call_ids?: string[]
- c2api: add getCall(id) using existing GET /calls/{call_id} endpoint
- VocabularyPanel: SourceCallPlayer component — lazy-loads call on
  first expand, shows audio controls + transcript snippet
2026-06-01 01:45:03 -04:00
Logan 032eef311f Fix vocabulary induction loop running too late
The loop slept 24h before its first pass, so suggestions would never
appear unless the server was up for a full day. Move the sleep to the
end so the first induction pass runs ~30s after startup.
2026-06-01 01:26:54 -04:00
Logan 3d51db80d0 Improve extraction accuracy with speaker role inference
Add a SPEAKER ROLES section to the GPT-4o-mini prompt teaching it to
distinguish dispatch voice (names a unit then gives assignment + address)
from unit voice (opens with own callsign + brief status). Applied to
location attribution (dispatch-provided address beats unit position report)
and unit extraction (dispatched units vs. acknowledging units). No extra
API calls — purely prompt-level reasoning on the existing transcript.
2026-06-01 01:17:49 -04:00
Logan 683b05beb1 Silence ERROR log for status messages from deleted nodes
_handle_status was calling doc_update unconditionally, which throws a 404
when a node has been deleted from the UI but is still running and sending
heartbeats. Catch the "No document to update" error and log at info level
instead of bubbling up to the dispatch error handler.
2026-06-01 01:06:49 -04:00
Logan cbcc85f7b1 Add consensus correlator: rules + Gemini LLM with smart tiebreaker
Refactor incident_correlator.py to a decision/commit split (preview_correlation
/ apply_correlation) so the rules engine and LLM can both produce decisions before
anything is written to Firestore.

Add llm_correlator.py: cheap Gemini Flash first-pass + Gemini Pro tiebreaker.
Wire _correlate_with_consensus in upload.py — rules-only fallback when key is
absent or call is thin; agreed/tiebreak consensus written to corr_debug.
2026-06-01 00:56:11 -04:00
Logan 6bf4333b72 Make correlation conservative: no time_fallback, pursuit-aware proximity, tiered thin path
- Remove time_fallback from _call_fits_incident: a substantive call with no
  matching signals (unit/vehicle/location) is now always orphaned on dispatch
  channels rather than attached by recency alone
- Pursuit-mode location: incidents tagged as vehicle-pursuit/pursuit/chase use
  a 20km expanded radius with speed-sanity validation (distance ÷ elapsed time
  must be ≤ 8 km/min) — location change is a positive signal for moving incidents
- Non-pursuit incidents: strict 0.5km proximity unchanged — location change = reject
- Thin path two-tier: ≤30s → attach to most-recent regardless of candidate count
  (direct conversational reply); 30s–10min → single candidate required
2026-06-01 00:08:19 -04:00
Logan b77d2cce36 Fix over-correlation: geocoding precision, thin path ambiguity, skip_reason propagation
- Geocoding: reject GEOMETRIC_CENTER/APPROXIMATE results — vague location strings
  (regions, city centroids) were resolving to node-area coords and creating false
  proximity matches that merged unrelated incidents
- Thin path: on dispatch channels with multiple active incidents, skip attachment
  rather than guessing — "10-4" with 3 active incidents is genuinely ambiguous
- Short transcripts (≤5 words) now write skip_reason="transcript_too_short" to
  the call doc, matching garbage transcript behavior
- upload.py no-scenes fallback now checks skip_reason before running correlation —
  flagged calls (garbage, too short) no longer attach via thin path
- Update Server README to reflect current project purpose, goals, and pipeline
2026-05-31 23:51:46 -04:00
Logan f774be12b8 Fix correlation over-merge, thin-call hallucination, and geocoding accuracy
- Cap unit-continuity path at 20 min idle (unit_continuity_max_idle_minutes)
- Block time_fallback and unit-continuity matching on reassignment calls
- Expand reassignment detection to cover unit-initiated self-reassignment
- Skip GPT extraction entirely for transcripts ≤5 words (prevents hallucinated tags/units)
- Reduce geocode_max_km from 75 to 40 to reject far-out-of-area results
- Include county in geocoding query for tighter jurisdiction anchoring
2026-05-26 02:20:15 -04:00
Logan 5eed4e08ce Implement delete node function 2026-05-25 20:20:50 -04:00
Logan fa5c53891c Add PD/Town name for TG import 2026-05-25 16:42:09 -04:00
Logan c5932165d8 Bug for new nodes 2026-05-25 16:29:20 -04:00
Logan 84ab72442f Correlator bugfix 2026-05-25 15:57:59 -04:00
Logan adf10244b4 Bug hunting for correlator 2026-05-25 15:41:43 -04:00
Logan 34ca1d0baf Map fixes 2026-05-25 15:28:35 -04:00
Logan 7d6e97fd4a fix: improve geocoding specificity and increase distance threshold for repeater systems
geocode_max_km: 25 → 75 km. The node is a physical receiver, not the system boundary;
digital repeaters extend coverage well beyond 25km (North White Plains at 35.5km from
the Yorktown node is a legitimate Westchester County location).

Query now fully qualified: "High Street" → "High Street, Yorktown, New York".
Added _get_node_state() which reverse-geocodes the node position once (cached) using
Google Maps to get the state name, appended alongside the municipality.
Generic street names (High Street, Main Street) no longer resolve to wrong-country results.
2026-05-25 14:49:02 -04:00
Logan ef8e0d1bfa revert: remove leaflet.gridlayer.googlemutant — incompatible with Next.js 15 bundler
The package consistently throws 'L.GridLayer.GoogleMutant is not a constructor'
due to L-instance conflicts in the webpack bundle, despite multiple workaround
attempts. Removed package, transpilePackages entry, type stub, env var, and all
related component code. Traffic overlay dropped; geocoding (backend) unaffected.
2026-05-25 14:19:21 -04:00
Logan 8a668e6a59 fix: move map action buttons to top-left to avoid legend overlap
Fit and traffic buttons were hidden behind the legend at bottom-right.
Moved both into a column group at top-left below the zoom controls,
where there is clear unobstructed space. Replaced emoji with TRF text label.
2026-05-25 14:09:47 -04:00
Logan dbacd9a9a8 fix: add type stub for leaflet.gridlayer.googlemutant to satisfy TypeScript 2026-05-25 14:02:25 -04:00
Logan a6d841b280 fix: rewrite Google Maps traffic layer to avoid L-instance constructor error
Replace static import + createLayerComponent approach with dynamic import()
inside a useEffect, which ensures leaflet.gridlayer.googlemutant augments the
same L instance that's active at runtime. Add loading=async to Maps JS script.

Traffic is now toggled via a dedicated button (green when active) rather than
LayersControl, bypassing the react-leaflet layer lifecycle that caused the
constructor conflict.
2026-05-25 13:59:47 -04:00
Logan 96bba45ffa fix: correct npm package name to leaflet.gridlayer.googlemutant 2026-05-25 13:44:31 -04:00
Logan 6a9fe5d26f feat: replace Google tile URL hack with leaflet-google-mutant for traffic layer
Add leaflet-google-mutant@0.16.0 (exact/locked version) as a proper bridge
between the Google Maps JavaScript API and Leaflet. The old mt{s}.google.com
tile URL approach was unofficial and produced empty tiles.

Traffic layer now renders via createLayerComponent + googleMutant, loaded only
after the Maps JS API script is injected and ready (keyed off NEXT_PUBLIC_GOOGLE_MAPS_API_KEY).
Added leaflet-google-mutant to transpilePackages in next.config.mjs.
2026-05-25 13:41:10 -04:00
Logan 0279a82b10 feat: replace Nominatim geocoding with Google Maps API; add TOC map improvements
Switch geocoding from Nominatim to Google Maps Geocoding API for accurate
local place name resolution (bounds-biased, with 25km distance rejection guard).
Remove the now-unused _get_node_place reverse-geocoder and _node_place_cache.

Map page (TOC improvements):
- Weather radar tiles auto-refresh every 5 minutes via radarEpoch key cycling
- Google Maps traffic overlay added to LayersControl
- Live 24h clock overlay at bottom-left for situational awareness
- Incident sidebar cards now show age (time since dispatch) and unit count
2026-05-25 13:27:19 -04:00