| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
Maps: keyless static previews + native-map handoff; fix Unknown location labels (#12841) * feat(backend): authed static-map proxy route with Redis-cached renders GET /v1/static-map renders dark-styled Google Static Maps images server-side (the only Maps key stays server-restricted) and caches the bytes in Redis keyed by the quantized pin set + size, so repeat renders of the same place across users/sessions cost one upstream call per distinct pin set per week. utils/static_map.py is the single provider seam for app map previews; swapping providers touches that module only. - pins parsed/bounded/de-duplicated/sorted, capped at 50; ~11m quantization makes users at the same place share one cached image - one pin centers at street zoom; several pins use provider auto-fit - failures return 502 (never cached) - the app renders its offline pin-dot canvas; auth prevents an open proxy on the project key - rate policy static_map:get (240/h per uid) stops hot loops; cached hits are one Redis read Tests: tests/routers/test_static_map.py (auth 401, 400 malformed pins, 502 upstream failure, 200 + private cache headers, cache hit/miss/order- insensitivity/no-cache-on-failure/missing key tolerances). Verified: .venv/bin/python -m pytest tests/routers/test_static_map.py -q -> 14 passed; scan_async_blockers clean. * fix(backend): enrich sync-path geolocation at the pipeline coordinator Failure-Class: FC-sync-geolocation-missing-address | new | none Offline-synced conversations were created with raw coordinates and no address: REST create, developer API, integration ingest, and live finalization all run resolve_geolocation, but the sync path shipped raw coords straight through. Recaps built from synced conversations showed 'Unknown' timeline rows. _run_full_pipeline_background_async - the one coordinator both the inline and Cloud Tasks dispatch branches call - now enriches the job's geolocation once, before any segment is processed and before the concurrency gate (no slot held during the geocode call). The resolver keeps the caller's exact coordinates and returns its input unchanged on any geocode miss/error, so a failure never drops the location. Tests: tests/unit/test_sync_geolocation_enrichment.py drives the real coordinator with a fake geocoder - one geocode per job (not per segment), enriched value reaches every segment, raw geolocation survives a geocode failure, None passthrough. Fixture stub lists in test_sync_v2 / test_sync_transcription_prefs gained the utils.conversations.location stub with an identity passthrough (the real resolver's miss behavior). Verified: pytest tests/unit/test_sync_geolocation_enrichment.py test_sync_v2.py test_sync_transcription_prefs.py -> all passed. * fix(backend): fill empty daily-summary pin addresses at read time Failure-Class: FC-sync-geolocation-missing-address | new | none generate_comprehensive_daily_summary copied c.geolocation.address verbatim, so pins from conversations created before write-time enrichment (the sync path) rendered as 'Unknown' recap timeline rows. The pins loop now fills an empty address through the shared ~100m- rounded geocode cache - the same entries write-time enrichment writes, so an already-enriched day costs no extra upstream call. All three callers are sync contexts, so the sync geocoder is used. A geocode miss or error leaves the address empty and keeps the pin (the app labels it 'Unknown'); regenerating a summary retroactively fixes history with no migration. Tests: tests/unit/test_daily_summary_location_address_fill.py (filled from geocoder with exact coords preserved, present address skips the geocoder, miss and exception both keep the pin address-less). Verified: pytest tests/unit/test_daily_summary_location_address_fill.py test_daily_summary_zero_coordinate_locations.py -> all passed. * feat(app): OmiMapPreview - one static-map widget with an offline pin-dot canvas Every map preview now funnels through a single widget backed by the authed backend static-map proxy (GET /v1/static-map): URL built by buildOmiStaticMapUrl (pins quantized to 4 decimals to match the server's cache quantization, deduped, capped at 50), image fetched with the session's Authorization header, and a deterministic dark canvas with one white dot per pin while loading, offline, or on any failure - never an error state. Product direction: previews in-app, tapping opens the native map app via MapsUtil.launchMap. The conversation detail geolocation card migrates from the client-keyed getMapImageUrl URL to this widget; getMapImageUrl is removed (maps_util keeps launchMap + place URL), taking the last direct client-side Google Static Maps call with it. Verified: flutter test test/widgets/omi_map_preview_test.dart test/widgets/daily_summary_card_test.dart (widget+URL builder tests); flutter analyze clean on changed files; analyze_ratchet.sh passed. * feat(app): replace CARTO tile maps with static previews; drop flutter_map CARTO began enforcing API keys on basemaps.cartocdn.com (keyless requests now return watermarked tiles), and Omi has no CARTO key. All three flutter_map surfaces now render OmiMapPreview instead - zero remaining tile traffic, no keyed tile provider in the app: - conversation map page: static preview of every ~100m cluster anchor (tap opens the native map app) above a grouped 'conversations at this place' list; single-conversation places open the conversation directly, multi-conversation places keep the cluster bottom sheet; grouping logic and cluster-row keys unchanged - daily summary card: 96px preview strip of the day's pins in the recap carousel (the highest-frequency map surface) - now one cached server-side render per distinct pin set instead of dozens of tiles per card - daily summary detail 'Your Day's Journey': 200px preview; image tap still opens the day's first stop (Apple Maps cannot take waypoints via map_launcher), per-stop timeline rows unchanged flutter_map is removed from pubspec (latlong2 stays - journey grouping uses it). The client-embedded Env.googleMapsApiKey goes with it: all static maps come from the server-restricted key behind the proxy now. envied outputs regenerated; test EnvFields stubs updated to match. Verified: bash test.sh / flutter test -> 1758 passed, 5 skipped, 0 failed; scripts/analyze_ratchet.sh passed (9 lint counts improved). * test(app): update map-surface tests for static previews - daily summary card: assert the preview strip, proxy URL pins/quantize/ dedupe, offline canvas fallback, and no-map-for-invalid-coordinates (was: tile provider request counting) - daily summary detail page: drop the TileProvider seam (widget no longer takes one) - conversation map groups: group-card key + cluster sheet rows replace marker keys; journey/env stubs gain the EnvFields change Verified: flutter test on all four files -> passed. * test(backend): stub utils.conversations.location in the cloud-tasks loader pipeline.py now imports async_resolve_geolocation at module scope; the cloud-tasks stub loader replaces its parent packages with MagicMocks, so the submodule import fails in file isolation (the earlier green run leaned on a module cached by another test file). Adds the explicit stub with an identity passthrough - the real resolver's miss behavior. Verified: BACKEND_UNIT_TEST_FILE_LIST=<this file> bash test.sh -> 91 passed. * chore(app): drop GOOGLE_MAPS_API_KEY from the env template The client no longer embeds a Maps key - all static-map renders go through the server-restricted key behind GET /v1/static-map. * test(backend): provide redis r on the usage-tracking stub set external_integrations now imports utils.conversations.location (daily- summary address fill), which imports the redis client symbol; the usage-tracking stub environment replaces database.redis_db with an empty module, so the import fails in file isolation. Give the stub an r. Verified: BACKEND_UNIT_TEST_FILE_LIST=<this file> bash test.sh -> 21 passed. * docs(backend): note the static-map provider URL budget with current limits * chore(backend): declare the static-map route policy in the manifest * fix(backend): drop the redundant isinstance on the typed pins parameter * fix(app): hold the preview canvas until the auth header resolves The first build fired the authed proxy request before getAuthHeader() resolved; CachedNetworkImage keys its cache by URL, so the later header-arriving setState never re-fetched - a permanent 401 fallback canvas. The widget now renders the pin-dot canvas (which already doubles as the error/offline path) until the header resolves, and only then mounts the network image with the Authorization header attached. The header resolver is injectable (authHeaderProvider) so tests control when it completes; an explicit imageUrl (test seam) skips the gate. Tests: new regression case asserts no CachedNetworkImage while auth is unresolved and the authed image after the completer fires; the card tests repoint URL assertions at the preview widget's pins (URL building has its own unit tests). Verified: flutter analyze clean; flutter test (full) -> 1759 passed. * fix(app): restore the conversation_map_marker automation key on group rows The per-place tappable key predates the static preview; keep it stable for automation (the PR brief promises preserved keys). Verified: flutter test test/unit/conversation_map_groups_test.dart passed. * fix(backend): normalize static-map dimensions, dedup render stampedes, count canvas fallbacks Review findings on the static-map proxy: - Dimension handling: one proportional scale factor (min(1, 640/w, 640/h)) computed once in fetch_static_map replaces the independent per-axis clamps - aspect is preserved and every request that differs only by scale normalizes onto the SAME cache entry (cache key and provider URL both use the effective size). - Stampede dedup: after a miss, a per-key render lock (r.set nx, 30s TTL) elects one renderer; concurrent misses poll the cache (0.25s interval, 15s budget) for the holder's result. Lock-held-timeout and lock-unavailable (Redis broken) both fail OPEN to an unlocked render - a lost lock never becomes a 502, and a broken Redis never pays the 15s wait budget. - record_fallback (component=static_map, provider_static_map -> client_pin_canvas, outcome=degraded) fires before the 502 so the degrade is counted in the shared telemetry. Tests: proportional normalization + shared cache entry, oversized fetch normalizes (size=640x150), concurrent misses render once, waiter polls a foreign lock holder to its hit, wait-timeout renders unlocked, 422s for out-of-bounds width/height via TestClient (Query contract), fallback telemetry kwargs. Verified: pytest tests/routers/test_static_map.py -> 21 passed; pyright (scripts/typecheck.sh) 0 errors, no findings in changed files. * fix(backend): cap daily-summary geocode attempts; boost-exempt static_map:get - The read-time address fill now bounds geocode ATTEMPTS at 10 per summary generation: cache hits are cheap but attempts are the deterministic wall-clock bound (10 x the geocoder's 10s worst case stays inside the job budget). Pins past the cap keep an empty address and the app's 'Unknown' fallback. Test: 11 empty-address pins -> 10 filled, 11th untouched, 10 geocoder calls. - static_map:get joins the boost-exempt rate policies: under prod's RATE_LIMIT_BOOST the 240/h hot-loop cap would otherwise resolve to 24k/h and stop protecting the billable provider calls. Verified: pytest test_daily_summary_location_address_fill.py -> 5 passed; utils.rate_limit_config import shows static_map:get in BOOST_EXEMPT_POLICIES. * test(backend): self-contained enrichment fakes; model the resolver short-circuit - test_sync_geolocation_enrichment no longer imports the transcription-prefs fixture module: sharing it loaded real heavyweight modules (GCS/Firestore protos, anthropic/jiter) inside the stub window, and stub_modules' teardown evicted them so later files broke on re-import (duplicate proto registration, jiter NameError) when run in one process. The fakes are now minimal and self-contained - every heavy leaf is stubbed, light enum modules (stt.outcomes, sync.lanes, sync.telemetry) stay real. - The None-geolocation test now models the real resolver's short-circuit (falsy input returns immediately, attempts recorded only for truthy input) and asserts: resolver called exactly once with None AND zero geocode attempts. Docstring states what is actually proven. - One-line caveat at the cloud-tasks loader injection site: new submodule imports must be added to heavy_deps explicitly (MagicMock parents do not resolve submodules). Migrating that hand-rolled loader to stub_modules would cascade across its 37 call sites - deliberately left as-is. Verified: pytest on the combined focused group (static_map, summary fill, sync enrichment, cloud tasks, sync v2, transcription prefs, geocode resolve) -> 372 passed in one process. | 13 天前 | |
Maps: keyless static previews + native-map handoff; fix Unknown location labels (#12841) * feat(backend): authed static-map proxy route with Redis-cached renders GET /v1/static-map renders dark-styled Google Static Maps images server-side (the only Maps key stays server-restricted) and caches the bytes in Redis keyed by the quantized pin set + size, so repeat renders of the same place across users/sessions cost one upstream call per distinct pin set per week. utils/static_map.py is the single provider seam for app map previews; swapping providers touches that module only. - pins parsed/bounded/de-duplicated/sorted, capped at 50; ~11m quantization makes users at the same place share one cached image - one pin centers at street zoom; several pins use provider auto-fit - failures return 502 (never cached) - the app renders its offline pin-dot canvas; auth prevents an open proxy on the project key - rate policy static_map:get (240/h per uid) stops hot loops; cached hits are one Redis read Tests: tests/routers/test_static_map.py (auth 401, 400 malformed pins, 502 upstream failure, 200 + private cache headers, cache hit/miss/order- insensitivity/no-cache-on-failure/missing key tolerances). Verified: .venv/bin/python -m pytest tests/routers/test_static_map.py -q -> 14 passed; scan_async_blockers clean. * fix(backend): enrich sync-path geolocation at the pipeline coordinator Failure-Class: FC-sync-geolocation-missing-address | new | none Offline-synced conversations were created with raw coordinates and no address: REST create, developer API, integration ingest, and live finalization all run resolve_geolocation, but the sync path shipped raw coords straight through. Recaps built from synced conversations showed 'Unknown' timeline rows. _run_full_pipeline_background_async - the one coordinator both the inline and Cloud Tasks dispatch branches call - now enriches the job's geolocation once, before any segment is processed and before the concurrency gate (no slot held during the geocode call). The resolver keeps the caller's exact coordinates and returns its input unchanged on any geocode miss/error, so a failure never drops the location. Tests: tests/unit/test_sync_geolocation_enrichment.py drives the real coordinator with a fake geocoder - one geocode per job (not per segment), enriched value reaches every segment, raw geolocation survives a geocode failure, None passthrough. Fixture stub lists in test_sync_v2 / test_sync_transcription_prefs gained the utils.conversations.location stub with an identity passthrough (the real resolver's miss behavior). Verified: pytest tests/unit/test_sync_geolocation_enrichment.py test_sync_v2.py test_sync_transcription_prefs.py -> all passed. * fix(backend): fill empty daily-summary pin addresses at read time Failure-Class: FC-sync-geolocation-missing-address | new | none generate_comprehensive_daily_summary copied c.geolocation.address verbatim, so pins from conversations created before write-time enrichment (the sync path) rendered as 'Unknown' recap timeline rows. The pins loop now fills an empty address through the shared ~100m- rounded geocode cache - the same entries write-time enrichment writes, so an already-enriched day costs no extra upstream call. All three callers are sync contexts, so the sync geocoder is used. A geocode miss or error leaves the address empty and keeps the pin (the app labels it 'Unknown'); regenerating a summary retroactively fixes history with no migration. Tests: tests/unit/test_daily_summary_location_address_fill.py (filled from geocoder with exact coords preserved, present address skips the geocoder, miss and exception both keep the pin address-less). Verified: pytest tests/unit/test_daily_summary_location_address_fill.py test_daily_summary_zero_coordinate_locations.py -> all passed. * feat(app): OmiMapPreview - one static-map widget with an offline pin-dot canvas Every map preview now funnels through a single widget backed by the authed backend static-map proxy (GET /v1/static-map): URL built by buildOmiStaticMapUrl (pins quantized to 4 decimals to match the server's cache quantization, deduped, capped at 50), image fetched with the session's Authorization header, and a deterministic dark canvas with one white dot per pin while loading, offline, or on any failure - never an error state. Product direction: previews in-app, tapping opens the native map app via MapsUtil.launchMap. The conversation detail geolocation card migrates from the client-keyed getMapImageUrl URL to this widget; getMapImageUrl is removed (maps_util keeps launchMap + place URL), taking the last direct client-side Google Static Maps call with it. Verified: flutter test test/widgets/omi_map_preview_test.dart test/widgets/daily_summary_card_test.dart (widget+URL builder tests); flutter analyze clean on changed files; analyze_ratchet.sh passed. * feat(app): replace CARTO tile maps with static previews; drop flutter_map CARTO began enforcing API keys on basemaps.cartocdn.com (keyless requests now return watermarked tiles), and Omi has no CARTO key. All three flutter_map surfaces now render OmiMapPreview instead - zero remaining tile traffic, no keyed tile provider in the app: - conversation map page: static preview of every ~100m cluster anchor (tap opens the native map app) above a grouped 'conversations at this place' list; single-conversation places open the conversation directly, multi-conversation places keep the cluster bottom sheet; grouping logic and cluster-row keys unchanged - daily summary card: 96px preview strip of the day's pins in the recap carousel (the highest-frequency map surface) - now one cached server-side render per distinct pin set instead of dozens of tiles per card - daily summary detail 'Your Day's Journey': 200px preview; image tap still opens the day's first stop (Apple Maps cannot take waypoints via map_launcher), per-stop timeline rows unchanged flutter_map is removed from pubspec (latlong2 stays - journey grouping uses it). The client-embedded Env.googleMapsApiKey goes with it: all static maps come from the server-restricted key behind the proxy now. envied outputs regenerated; test EnvFields stubs updated to match. Verified: bash test.sh / flutter test -> 1758 passed, 5 skipped, 0 failed; scripts/analyze_ratchet.sh passed (9 lint counts improved). * test(app): update map-surface tests for static previews - daily summary card: assert the preview strip, proxy URL pins/quantize/ dedupe, offline canvas fallback, and no-map-for-invalid-coordinates (was: tile provider request counting) - daily summary detail page: drop the TileProvider seam (widget no longer takes one) - conversation map groups: group-card key + cluster sheet rows replace marker keys; journey/env stubs gain the EnvFields change Verified: flutter test on all four files -> passed. * test(backend): stub utils.conversations.location in the cloud-tasks loader pipeline.py now imports async_resolve_geolocation at module scope; the cloud-tasks stub loader replaces its parent packages with MagicMocks, so the submodule import fails in file isolation (the earlier green run leaned on a module cached by another test file). Adds the explicit stub with an identity passthrough - the real resolver's miss behavior. Verified: BACKEND_UNIT_TEST_FILE_LIST=<this file> bash test.sh -> 91 passed. * chore(app): drop GOOGLE_MAPS_API_KEY from the env template The client no longer embeds a Maps key - all static-map renders go through the server-restricted key behind GET /v1/static-map. * test(backend): provide redis r on the usage-tracking stub set external_integrations now imports utils.conversations.location (daily- summary address fill), which imports the redis client symbol; the usage-tracking stub environment replaces database.redis_db with an empty module, so the import fails in file isolation. Give the stub an r. Verified: BACKEND_UNIT_TEST_FILE_LIST=<this file> bash test.sh -> 21 passed. * docs(backend): note the static-map provider URL budget with current limits * chore(backend): declare the static-map route policy in the manifest * fix(backend): drop the redundant isinstance on the typed pins parameter * fix(app): hold the preview canvas until the auth header resolves The first build fired the authed proxy request before getAuthHeader() resolved; CachedNetworkImage keys its cache by URL, so the later header-arriving setState never re-fetched - a permanent 401 fallback canvas. The widget now renders the pin-dot canvas (which already doubles as the error/offline path) until the header resolves, and only then mounts the network image with the Authorization header attached. The header resolver is injectable (authHeaderProvider) so tests control when it completes; an explicit imageUrl (test seam) skips the gate. Tests: new regression case asserts no CachedNetworkImage while auth is unresolved and the authed image after the completer fires; the card tests repoint URL assertions at the preview widget's pins (URL building has its own unit tests). Verified: flutter analyze clean; flutter test (full) -> 1759 passed. * fix(app): restore the conversation_map_marker automation key on group rows The per-place tappable key predates the static preview; keep it stable for automation (the PR brief promises preserved keys). Verified: flutter test test/unit/conversation_map_groups_test.dart passed. * fix(backend): normalize static-map dimensions, dedup render stampedes, count canvas fallbacks Review findings on the static-map proxy: - Dimension handling: one proportional scale factor (min(1, 640/w, 640/h)) computed once in fetch_static_map replaces the independent per-axis clamps - aspect is preserved and every request that differs only by scale normalizes onto the SAME cache entry (cache key and provider URL both use the effective size). - Stampede dedup: after a miss, a per-key render lock (r.set nx, 30s TTL) elects one renderer; concurrent misses poll the cache (0.25s interval, 15s budget) for the holder's result. Lock-held-timeout and lock-unavailable (Redis broken) both fail OPEN to an unlocked render - a lost lock never becomes a 502, and a broken Redis never pays the 15s wait budget. - record_fallback (component=static_map, provider_static_map -> client_pin_canvas, outcome=degraded) fires before the 502 so the degrade is counted in the shared telemetry. Tests: proportional normalization + shared cache entry, oversized fetch normalizes (size=640x150), concurrent misses render once, waiter polls a foreign lock holder to its hit, wait-timeout renders unlocked, 422s for out-of-bounds width/height via TestClient (Query contract), fallback telemetry kwargs. Verified: pytest tests/routers/test_static_map.py -> 21 passed; pyright (scripts/typecheck.sh) 0 errors, no findings in changed files. * fix(backend): cap daily-summary geocode attempts; boost-exempt static_map:get - The read-time address fill now bounds geocode ATTEMPTS at 10 per summary generation: cache hits are cheap but attempts are the deterministic wall-clock bound (10 x the geocoder's 10s worst case stays inside the job budget). Pins past the cap keep an empty address and the app's 'Unknown' fallback. Test: 11 empty-address pins -> 10 filled, 11th untouched, 10 geocoder calls. - static_map:get joins the boost-exempt rate policies: under prod's RATE_LIMIT_BOOST the 240/h hot-loop cap would otherwise resolve to 24k/h and stop protecting the billable provider calls. Verified: pytest test_daily_summary_location_address_fill.py -> 5 passed; utils.rate_limit_config import shows static_map:get in BOOST_EXEMPT_POLICIES. * test(backend): self-contained enrichment fakes; model the resolver short-circuit - test_sync_geolocation_enrichment no longer imports the transcription-prefs fixture module: sharing it loaded real heavyweight modules (GCS/Firestore protos, anthropic/jiter) inside the stub window, and stub_modules' teardown evicted them so later files broke on re-import (duplicate proto registration, jiter NameError) when run in one process. The fakes are now minimal and self-contained - every heavy leaf is stubbed, light enum modules (stt.outcomes, sync.lanes, sync.telemetry) stay real. - The None-geolocation test now models the real resolver's short-circuit (falsy input returns immediately, attempts recorded only for truthy input) and asserts: resolver called exactly once with None AND zero geocode attempts. Docstring states what is actually proven. - One-line caveat at the cloud-tasks loader injection site: new submodule imports must be added to heavy_deps explicitly (MagicMock parents do not resolve submodules). Migrating that hand-rolled loader to stub_modules would cascade across its 37 call sites - deliberately left as-is. Verified: pytest on the combined focused group (static_map, summary fill, sync enrichment, cloud tasks, sync v2, transcription prefs, geocode resolve) -> 372 passed in one process. | 13 天前 | |
feat(app): add debug-only local_prod profile for a local backend with production identity Pairs production Firebase identity (based-hardware) with a developer-chosen backend endpoint, selected only by an explicit OMI_APP_PROFILE=local_prod dart-define. Release builds reject the profile at startup, so no shipped artifact can select it. local_dev keeps its stricter loopback/private-network rule. Amends INV-DATA-1 to enumerate the exception. Verified: flutter test test/unit/env_test.dart (19 passed); ran the prod-flavor app on-device with OMI_APP_PROFILE=local_prod against a locally served backend. | 1 个月前 | |
Maps: keyless static previews + native-map handoff; fix Unknown location labels (#12841) * feat(backend): authed static-map proxy route with Redis-cached renders GET /v1/static-map renders dark-styled Google Static Maps images server-side (the only Maps key stays server-restricted) and caches the bytes in Redis keyed by the quantized pin set + size, so repeat renders of the same place across users/sessions cost one upstream call per distinct pin set per week. utils/static_map.py is the single provider seam for app map previews; swapping providers touches that module only. - pins parsed/bounded/de-duplicated/sorted, capped at 50; ~11m quantization makes users at the same place share one cached image - one pin centers at street zoom; several pins use provider auto-fit - failures return 502 (never cached) - the app renders its offline pin-dot canvas; auth prevents an open proxy on the project key - rate policy static_map:get (240/h per uid) stops hot loops; cached hits are one Redis read Tests: tests/routers/test_static_map.py (auth 401, 400 malformed pins, 502 upstream failure, 200 + private cache headers, cache hit/miss/order- insensitivity/no-cache-on-failure/missing key tolerances). Verified: .venv/bin/python -m pytest tests/routers/test_static_map.py -q -> 14 passed; scan_async_blockers clean. * fix(backend): enrich sync-path geolocation at the pipeline coordinator Failure-Class: FC-sync-geolocation-missing-address | new | none Offline-synced conversations were created with raw coordinates and no address: REST create, developer API, integration ingest, and live finalization all run resolve_geolocation, but the sync path shipped raw coords straight through. Recaps built from synced conversations showed 'Unknown' timeline rows. _run_full_pipeline_background_async - the one coordinator both the inline and Cloud Tasks dispatch branches call - now enriches the job's geolocation once, before any segment is processed and before the concurrency gate (no slot held during the geocode call). The resolver keeps the caller's exact coordinates and returns its input unchanged on any geocode miss/error, so a failure never drops the location. Tests: tests/unit/test_sync_geolocation_enrichment.py drives the real coordinator with a fake geocoder - one geocode per job (not per segment), enriched value reaches every segment, raw geolocation survives a geocode failure, None passthrough. Fixture stub lists in test_sync_v2 / test_sync_transcription_prefs gained the utils.conversations.location stub with an identity passthrough (the real resolver's miss behavior). Verified: pytest tests/unit/test_sync_geolocation_enrichment.py test_sync_v2.py test_sync_transcription_prefs.py -> all passed. * fix(backend): fill empty daily-summary pin addresses at read time Failure-Class: FC-sync-geolocation-missing-address | new | none generate_comprehensive_daily_summary copied c.geolocation.address verbatim, so pins from conversations created before write-time enrichment (the sync path) rendered as 'Unknown' recap timeline rows. The pins loop now fills an empty address through the shared ~100m- rounded geocode cache - the same entries write-time enrichment writes, so an already-enriched day costs no extra upstream call. All three callers are sync contexts, so the sync geocoder is used. A geocode miss or error leaves the address empty and keeps the pin (the app labels it 'Unknown'); regenerating a summary retroactively fixes history with no migration. Tests: tests/unit/test_daily_summary_location_address_fill.py (filled from geocoder with exact coords preserved, present address skips the geocoder, miss and exception both keep the pin address-less). Verified: pytest tests/unit/test_daily_summary_location_address_fill.py test_daily_summary_zero_coordinate_locations.py -> all passed. * feat(app): OmiMapPreview - one static-map widget with an offline pin-dot canvas Every map preview now funnels through a single widget backed by the authed backend static-map proxy (GET /v1/static-map): URL built by buildOmiStaticMapUrl (pins quantized to 4 decimals to match the server's cache quantization, deduped, capped at 50), image fetched with the session's Authorization header, and a deterministic dark canvas with one white dot per pin while loading, offline, or on any failure - never an error state. Product direction: previews in-app, tapping opens the native map app via MapsUtil.launchMap. The conversation detail geolocation card migrates from the client-keyed getMapImageUrl URL to this widget; getMapImageUrl is removed (maps_util keeps launchMap + place URL), taking the last direct client-side Google Static Maps call with it. Verified: flutter test test/widgets/omi_map_preview_test.dart test/widgets/daily_summary_card_test.dart (widget+URL builder tests); flutter analyze clean on changed files; analyze_ratchet.sh passed. * feat(app): replace CARTO tile maps with static previews; drop flutter_map CARTO began enforcing API keys on basemaps.cartocdn.com (keyless requests now return watermarked tiles), and Omi has no CARTO key. All three flutter_map surfaces now render OmiMapPreview instead - zero remaining tile traffic, no keyed tile provider in the app: - conversation map page: static preview of every ~100m cluster anchor (tap opens the native map app) above a grouped 'conversations at this place' list; single-conversation places open the conversation directly, multi-conversation places keep the cluster bottom sheet; grouping logic and cluster-row keys unchanged - daily summary card: 96px preview strip of the day's pins in the recap carousel (the highest-frequency map surface) - now one cached server-side render per distinct pin set instead of dozens of tiles per card - daily summary detail 'Your Day's Journey': 200px preview; image tap still opens the day's first stop (Apple Maps cannot take waypoints via map_launcher), per-stop timeline rows unchanged flutter_map is removed from pubspec (latlong2 stays - journey grouping uses it). The client-embedded Env.googleMapsApiKey goes with it: all static maps come from the server-restricted key behind the proxy now. envied outputs regenerated; test EnvFields stubs updated to match. Verified: bash test.sh / flutter test -> 1758 passed, 5 skipped, 0 failed; scripts/analyze_ratchet.sh passed (9 lint counts improved). * test(app): update map-surface tests for static previews - daily summary card: assert the preview strip, proxy URL pins/quantize/ dedupe, offline canvas fallback, and no-map-for-invalid-coordinates (was: tile provider request counting) - daily summary detail page: drop the TileProvider seam (widget no longer takes one) - conversation map groups: group-card key + cluster sheet rows replace marker keys; journey/env stubs gain the EnvFields change Verified: flutter test on all four files -> passed. * test(backend): stub utils.conversations.location in the cloud-tasks loader pipeline.py now imports async_resolve_geolocation at module scope; the cloud-tasks stub loader replaces its parent packages with MagicMocks, so the submodule import fails in file isolation (the earlier green run leaned on a module cached by another test file). Adds the explicit stub with an identity passthrough - the real resolver's miss behavior. Verified: BACKEND_UNIT_TEST_FILE_LIST=<this file> bash test.sh -> 91 passed. * chore(app): drop GOOGLE_MAPS_API_KEY from the env template The client no longer embeds a Maps key - all static-map renders go through the server-restricted key behind GET /v1/static-map. * test(backend): provide redis r on the usage-tracking stub set external_integrations now imports utils.conversations.location (daily- summary address fill), which imports the redis client symbol; the usage-tracking stub environment replaces database.redis_db with an empty module, so the import fails in file isolation. Give the stub an r. Verified: BACKEND_UNIT_TEST_FILE_LIST=<this file> bash test.sh -> 21 passed. * docs(backend): note the static-map provider URL budget with current limits * chore(backend): declare the static-map route policy in the manifest * fix(backend): drop the redundant isinstance on the typed pins parameter * fix(app): hold the preview canvas until the auth header resolves The first build fired the authed proxy request before getAuthHeader() resolved; CachedNetworkImage keys its cache by URL, so the later header-arriving setState never re-fetched - a permanent 401 fallback canvas. The widget now renders the pin-dot canvas (which already doubles as the error/offline path) until the header resolves, and only then mounts the network image with the Authorization header attached. The header resolver is injectable (authHeaderProvider) so tests control when it completes; an explicit imageUrl (test seam) skips the gate. Tests: new regression case asserts no CachedNetworkImage while auth is unresolved and the authed image after the completer fires; the card tests repoint URL assertions at the preview widget's pins (URL building has its own unit tests). Verified: flutter analyze clean; flutter test (full) -> 1759 passed. * fix(app): restore the conversation_map_marker automation key on group rows The per-place tappable key predates the static preview; keep it stable for automation (the PR brief promises preserved keys). Verified: flutter test test/unit/conversation_map_groups_test.dart passed. * fix(backend): normalize static-map dimensions, dedup render stampedes, count canvas fallbacks Review findings on the static-map proxy: - Dimension handling: one proportional scale factor (min(1, 640/w, 640/h)) computed once in fetch_static_map replaces the independent per-axis clamps - aspect is preserved and every request that differs only by scale normalizes onto the SAME cache entry (cache key and provider URL both use the effective size). - Stampede dedup: after a miss, a per-key render lock (r.set nx, 30s TTL) elects one renderer; concurrent misses poll the cache (0.25s interval, 15s budget) for the holder's result. Lock-held-timeout and lock-unavailable (Redis broken) both fail OPEN to an unlocked render - a lost lock never becomes a 502, and a broken Redis never pays the 15s wait budget. - record_fallback (component=static_map, provider_static_map -> client_pin_canvas, outcome=degraded) fires before the 502 so the degrade is counted in the shared telemetry. Tests: proportional normalization + shared cache entry, oversized fetch normalizes (size=640x150), concurrent misses render once, waiter polls a foreign lock holder to its hit, wait-timeout renders unlocked, 422s for out-of-bounds width/height via TestClient (Query contract), fallback telemetry kwargs. Verified: pytest tests/routers/test_static_map.py -> 21 passed; pyright (scripts/typecheck.sh) 0 errors, no findings in changed files. * fix(backend): cap daily-summary geocode attempts; boost-exempt static_map:get - The read-time address fill now bounds geocode ATTEMPTS at 10 per summary generation: cache hits are cheap but attempts are the deterministic wall-clock bound (10 x the geocoder's 10s worst case stays inside the job budget). Pins past the cap keep an empty address and the app's 'Unknown' fallback. Test: 11 empty-address pins -> 10 filled, 11th untouched, 10 geocoder calls. - static_map:get joins the boost-exempt rate policies: under prod's RATE_LIMIT_BOOST the 240/h hot-loop cap would otherwise resolve to 24k/h and stop protecting the billable provider calls. Verified: pytest test_daily_summary_location_address_fill.py -> 5 passed; utils.rate_limit_config import shows static_map:get in BOOST_EXEMPT_POLICIES. * test(backend): self-contained enrichment fakes; model the resolver short-circuit - test_sync_geolocation_enrichment no longer imports the transcription-prefs fixture module: sharing it loaded real heavyweight modules (GCS/Firestore protos, anthropic/jiter) inside the stub window, and stub_modules' teardown evicted them so later files broke on re-import (duplicate proto registration, jiter NameError) when run in one process. The fakes are now minimal and self-contained - every heavy leaf is stubbed, light enum modules (stt.outcomes, sync.lanes, sync.telemetry) stay real. - The None-geolocation test now models the real resolver's short-circuit (falsy input returns immediately, attempts recorded only for truthy input) and asserts: resolver called exactly once with None AND zero geocode attempts. Docstring states what is actually proven. - One-line caveat at the cloud-tasks loader injection site: new submodule imports must be added to heavy_deps explicitly (MagicMock parents do not resolve submodules). Migrating that hand-rolled loader to stub_modules would cascade across its 37 call sites - deliberately left as-is. Verified: pytest on the combined focused group (static_map, summary fill, sync enrichment, cloud tasks, sync v2, transcription prefs, geocode resolve) -> 372 passed in one process. | 13 天前 |
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 13 天前 | ||
| 13 天前 | ||
| 1 个月前 | ||
| 13 天前 |