25 KiB
fusion_schedule — CODE MAP (where-is-what index)
Precise symbol-level index for the whole module. Companion to
CLAUDE.md(which is the narrative/guidance doc). This file = "where is X". Line numbers are exact at the time of writing (2026-06-03); re-grepdef/fields./@http.route/<template id=if they drift. Audit findings live inCLAUDE.md §16and in Supabasefusionapps.issues(project Fusion Schedule =576de219-57e6-4596-8c8c-0c093e4cb54a).
0. File tree (sizes approximate, by cat -n)
fusion_schedule/
├── __manifest__.py 61 deps, data load order, assets (v19.0.2.1.0)
├── __init__.py 3 → controllers, models
├── controllers/
│ ├── __init__.py 2 → portal_schedule
│ └── portal_schedule.py ~1607 PortalSchedule(CustomerPortal): 23 routes + helpers
├── models/
│ ├── __init__.py 6 load order (see below)
│ ├── fusion_calendar_account.py ~1191 sync engine + OAuth + cron (THE core)
│ ├── fusion_calendar_event_link.py 30 Odoo↔external join table
│ ├── calendar_event.py 89 inherit: sync fields + write/unlink push
│ ├── res_users.py 69 inherit: slug + work prefs + auto-slug create()
│ └── res_config_settings.py 74 inherit: OAuth creds + sync interval + defaults
├── data/
│ ├── ir_cron_data.xml 13 5-min sync cron
│ ├── mail_template_data.xml 155 booking confirmation email
│ └── appointment_invite_data.xml 10 default share invite (noupdate)
├── security/
│ ├── security.xml 17 2 record rules
│ └── ir.model.access.csv 5 4 ACL rows
├── views/
│ ├── fusion_calendar_account_views.xml 64 backend list/form/action/menu
│ ├── res_config_settings_views.xml 148 Settings app block
│ ├── portal_schedule_tile.xml 25 tile into fusion_portal home
│ ├── portal_schedule.xml 833 portal_schedule_page + portal_schedule_book
│ └── public_booking.xml 586 public_booking_page + public_manage_page (inline JS)
├── static/src/
│ ├── css/portal_schedule.css 48 responsive helpers only
│ ├── js/portal_schedule_booking.js ~575 booking form (authenticated)
│ ├── js/portal_schedule_accounts.js ~489 dashboard modals/toasts/optimize
│ └── views/fusion_calendar_controller.{js,xml} 68/44 backend AttendeeCalendarController patch
├── utils/__init__.py 1 empty placeholder
└── graphify-out/ — Cursor artifact, NOT loaded by Odoo
Model load order (models/__init__.py): fusion_calendar_account → fusion_calendar_event_link → calendar_event → res_users → res_config_settings.
1. Models
1.1 fusion.calendar.account — models/fusion_calendar_account.py
_order = 'x_fc_provider, x_fc_email'. Module constants (top of file): TIMEOUT=20 (14),
MAX_THROTTLE_RETRIES=3 (15), DEFAULT_RETRY_SECONDS=10 (16); Google endpoints 19–23,
Microsoft endpoints 26–34.
Fields
| line | field | type / notes |
|---|---|---|
| 42 | x_fc_user_id |
m2o res.users · required · cascade · default=current user · index |
| 46 | x_fc_provider |
sel google/microsoft · required |
| 50 | x_fc_email |
char |
| 51 | x_fc_name |
char · compute _compute_name · store |
| 52 | x_fc_active |
bool · default True |
| 55 | x_fc_rtoken |
char · groups=base.group_system |
| 56 | x_fc_token |
char · group_system |
| 57 | x_fc_token_validity |
datetime · group_system |
| 60 | x_fc_sync_token |
char · group_system (delta/sync token) |
| 61 | x_fc_calendar_id |
char · default 'primary' |
| 62 | x_fc_last_sync |
datetime |
| 63 | x_fc_sync_status |
sel active/error/paused · default active |
| 68 | x_fc_error_message |
text |
| 71 | x_fc_link_ids |
o2m → fusion.calendar.event.link |
Methods
| line | method | purpose |
|---|---|---|
| 76 | _compute_name |
"Provider — email" label |
| 85/92/99/106 | _get_{google,microsoft}_client_{id,secret} |
creds: fusion_schedule_* ICP → native *_calendar_* ICP fallback |
| 117 | _get_valid_token |
return token, refresh if <1 min to expiry |
| 130 | _refresh_token |
dispatch to provider refresh; on 400/401 mark error + clear |
| 149 / 170 | _refresh_google_token / _refresh_microsoft_token |
OAuth refresh (MS may rotate rtoken) |
| 200 / 213 | _exchange_{google,microsoft}_code |
code→tokens (called from callback) |
| 232 / 243 | _fetch_{google,microsoft}_email |
@api.model · whoami email |
| 258 | _sync_pull |
entry: dispatch pull per provider, catch+record errors |
| 293 | _sync_pull_google |
events.list paging; 410→drop token+full resync; window −14/+30d |
| 362 | _google_request_with_retry |
GET w/ 429/503 + connection retry |
| 389 | _silent_ctx |
context flags that suppress mail + re-push (load-bearing) |
| 401 | _find_existing_event |
dedup by name+start+stop (incl. archived) |
| 419 | _upsert_event_link |
create/update the join row |
| 447 | _process_google_event |
upsert one Google event (3-tier dedup) |
| 503 | _google_event_to_odoo_vals |
⚠ uses astimezone(None) — server-tz bug (CLAUDE §16.2) |
| 550 | _sync_pull_microsoft |
Graph calendarView/delta; page cap 2000/5000 |
| 642 | _microsoft_request_with_retry |
GET w/ retry |
| 671 | _process_microsoft_event |
upsert one MS event; @removed=changed→'skipped' (don't archive) |
| 738 | _microsoft_event_to_odoo_vals |
MS dict→Odoo vals |
| 798 | _fetch_microsoft_event_subject |
fallback fetch when delta omits subject |
| 821 | _sync_push_event |
push one Odoo event (insert/patch per provider) |
| 870/884/896 | _google_{insert,patch,delete}_event |
Google write API |
| 908/924/938 | _microsoft_{insert,patch,delete}_event |
Graph write API |
| 953 / 977 | _odoo_event_to_{google,microsoft} |
Odoo→external format |
| 1022 | _cross_calendar_push |
push unlinked Odoo-native events to first account (CLAUDE §16.4) |
| 1066 | get_user_accounts_status |
@api.model [backend RPC] — account chips |
| 1081 | sync_current_user |
@api.model [backend RPC] — "Sync now" (commits per account) |
| 1116 | _cron_sync_all_accounts |
@api.model [cron] — sync all, then cross-push per multi-acct user |
| 1161 | action_disconnect |
delete pushed external events, unlink, pause |
1.2 fusion.calendar.event.link — models/fusion_calendar_event_link.py
_order = 'x_fc_last_synced desc'. Fields: x_fc_event_id (11, m2o calendar.event, cascade),
x_fc_account_id (15, m2o account, cascade), x_fc_external_id (19, req, index),
x_fc_universal_id (22, iCalUID, index), x_fc_last_synced (25), x_fc_sync_direction
(26, pull/push/both). Constraint _unique_account_external = UNIQUE(x_fc_account_id, x_fc_external_id) (32).
1.3 calendar.event (inherit) — models/calendar_event.py
Sole extender of calendar.event in the whole repo. Fields: x_fc_source_account_id
(14), x_fc_is_external (18, compute+store), x_fc_link_ids (21), x_fc_manage_token
(24, index, copy=False), x_fc_client_email (28), x_fc_client_phone (29),
x_fc_address_lat (30), x_fc_address_lng (31), x_fc_travel_minutes_before (32),
x_fc_is_travel_block (36). Methods: _compute_is_external (42), _skip_fc_sync (46),
unlink (51, deletes from external), write (76, pushes to external) — both gated by
_skip_fc_sync() + presence of links. ⚠ external HTTP is synchronous (CLAUDE §16.7).
1.4 res.users (inherit) — models/res_users.py
Fields: x_fc_calendar_account_ids (12), x_fc_schedule_slug (16), x_fc_booking_enabled
(21), x_fc_work_start (26), x_fc_work_end (30), x_fc_break_start (34),
x_fc_break_duration (38), x_fc_travel_buffer (42), x_fc_home_address (46),
x_fc_home_lat (50), x_fc_home_lng (51). Constraint _unique_schedule_slug =
UNIQUE(x_fc_schedule_slug) (53). Methods: create (59, @api.model_create_multi,
auto-slug — ⚠ collision risk CLAUDE §16.5), _generate_schedule_slug (66).
1.5 res.config.settings (inherit) — models/res_config_settings.py
Fields (12): x_fc_google_client_id (10), _secret (14), _has_fallback (18);
x_fc_microsoft_client_id (24), _secret (28), _has_fallback (32);
x_fc_sync_interval_minutes (38, not wired to cron); x_fc_default_work_start (45),
_work_end (50), _break_start (55), _break_duration (60), _travel_buffer (65).
Methods: _compute_google_has_fallback (72), _compute_microsoft_has_fallback (79).
2. Controller — controllers/portal_schedule.py (PortalSchedule(CustomerPortal))
Helper methods
| line | method | purpose |
|---|---|---|
| 30 | _get_schedule_values |
portal gradient (fusion_claims params) + maps key |
| 43 | _get_user_timezone |
→ _resolve_timezone(env.user) |
| 46 | _resolve_timezone |
user.tz → tz cookie → company cal → UTC |
| 69 | _get_appointment_types |
types where current user is staff |
| 75 | _get_user_prefs |
per-user prefs w/ company-default fallback |
| 101 | _get_maps_api_key |
fusion.api.service → fusion_claims.google_maps_api_key |
| 114 | _call_ai |
fusion.api.service.call_openai → direct OpenAI HTTP |
| 147 | _get_travel_time |
Google Distance Matrix (min) |
| 178 | _geocode_address |
Google Geocoding (lat,lng) |
| 200 | _create_travel_blocks |
insert "Travel to …" placeholder events |
| 425 | _format_hour |
staticmethod · 13.5 → "1:30 PM" |
| 435 | _generate_available_slots |
shared slot core; emits UTC datetime (line 520) |
| 825 | _get_event_by_token |
manage-token lookup (len==32) |
| 932 | _build_schedule_context |
AI prompt context builder |
| 1336 | _find_recently_connected_account |
OAuth callback resilience |
Routes (23 total)
| line | verb | path | auth | handler |
|---|---|---|---|---|
| 288 | http | /my/schedule |
user | schedule_page |
| 363 | jsonrpc | /my/schedule/preferences |
user | schedule_save_preferences |
| 397 | http | /my/schedule/book |
user | schedule_book |
| 530 | jsonrpc | /my/schedule/available-slots |
user | schedule_available_slots |
| 560 | jsonrpc | /my/schedule/week-events |
user | schedule_week_events |
| 630 | http POST | /my/schedule/book/submit |
user | schedule_book_submit ✅ tz-correct |
| 777 | jsonrpc | /my/schedule/event/cancel |
user | schedule_event_cancel |
| 792 | jsonrpc | /my/schedule/event/reschedule |
user | schedule_event_reschedule ⚠ tz-bug |
| 834 | http | /schedule/manage/<token> |
public | public_manage_page |
| 860 | http POST | /schedule/manage/<token>/cancel |
public | public_manage_cancel |
| 876 | http POST | /schedule/manage/<token>/reschedule |
public | public_manage_reschedule ⚠ tz-bug |
| 907 | jsonrpc | /schedule/manage/<token>/available-slots |
public (csrf=False) | public_manage_slots |
| 982 | jsonrpc | /my/schedule/ai/suggest |
user | schedule_ai_suggest |
| 1093 | jsonrpc | /my/schedule/ai/optimize |
user | schedule_ai_optimize |
| 1155 | http | /my/schedule/connect/google |
user | connect_google |
| 1192 | http | /my/schedule/connect/microsoft |
user | connect_microsoft |
| 1230 | http | /my/schedule/oauth/callback |
user | oauth_callback |
| 1356 | jsonrpc | /my/schedule/disconnect |
user | schedule_disconnect |
| 1370 | jsonrpc | /my/schedule/sync-now |
user | schedule_sync_now |
| 1398 | http | /schedule/<slug> |
public | public_booking_page |
| 1431 | jsonrpc | /schedule/<slug>/available-slots |
public (csrf=False) | public_available_slots |
| 1465 | http POST | /schedule/<slug>/book |
public (csrf) | public_book_submit ⚠ tz-bug + no re-validate |
| 1602 | jsonrpc | /my/schedule/toggle-booking |
user | schedule_toggle_booking |
3. Frontend JS
3.1 backend patch — static/src/views/fusion_calendar_controller.js
patch(AttendeeCalendarController.prototype): setup, getters fusionAccounts /
fusionSyncing, _loadFusionAccounts (→ get_user_accounts_status), onFusionSyncNow
(→ sync_current_user). Template .xml hides #header_synchronization_settings, injects
account chips + sync button + cog→/my/schedule.
3.2 static/src/js/portal_schedule_booking.js (authenticated book page)
setTzCookie (4), getAppointmentTypeId (35), truncate (41), formatDateStr (46),
addDays (53), getMonday (59), selectDay (67), fetchWeekEvents (77) →
/my/schedule/week-events, navigateWeek (120), renderWeekCalendar (140), fetchSlots
(260) → /my/schedule/available-slots, renderGroup (319, nested), fetchAiSuggestions
(364) → /my/schedule/ai/suggest, setupAddressAutocomplete (516, Google Places).
3.3 static/src/js/portal_schedule_accounts.js (dashboard)
Utils: localDateStr (4), setTzCookie (12), jsonRpc (21), fusionConfirm (30),
fusionToast (87, builds #fusionToastLive — template #fusionToast is dead),
closeRescheduleModal (304), closeOptimizeModal (474). Event bindings: disconnect (112)
→ /disconnect, sync (141) → /sync-now, share (160), save-prefs (186) → /preferences,
cancel (231) → /event/cancel, reschedule open (274) + date-change (321) +
confirm (375) → /event/reschedule, optimize (413) → /ai/optimize.
Public pages (
public_booking_page,public_manage_page) carry their own inline<script>inpublic_booking.xml(a 2nd copy of slot-render + Places autocomplete + reschedule) — they do not use the files above. See CLAUDE §9.1.
3.4 DOM-id contract (templates ↔ JS)
Book page: bookingDate, appointmentTypeSelect, slotsContainer/slotsGrid/slotsLoading/ noSlots, slotDatetime, slotDuration, weekCalendar{Container,Loading,Grid,Header,Body, Empty,Nav}, btnPrevWeek/btnNextWeek/weekRangeLabel, aiSuggest{Section,Loading,Grid},
btnAiSuggest, clientStreet/City/Province/Postal/Lat/Lng, btnSubmitBooking.
Dashboard: fusionConfirmModal(+Title/Message/Ok), rescheduleModal(+Date/SlotsContainer/
SlotsGrid/EventId/SlotDatetime/EventDuration/EventName/AppTypeId/btnConfirmReschedule),
optimizeModal(+Loading/Result/CurrentTravel/NewTravel/Savings/ScheduleList/Error),
schedulePrefsForm/btnSavePrefs/prefsSavedMsg, btnOptimizeSchedule, .js-* classes.
Public: publicBookingDate, publicSlots*, publicSlotDatetime/Duration, publicBtnSubmit,
publicAppointmentType, publicClient*, publicReschedule*.
4. Templates / data / security / settings
Templates
| id | file:line | base layout |
|---|---|---|
portal_schedule_page |
portal_schedule.xml:6 | portal.portal_layout |
portal_schedule_book |
portal_schedule.xml:605 | portal.portal_layout |
public_booking_page |
public_booking.xml:6 | website.layout (+inline JS) |
public_manage_page |
public_booking.xml:345 | website.layout (+inline JS) |
portal_my_home_schedule |
portal_schedule_tile.xml:5 | inherit fusion_portal.portal_my_home_authorizer |
FusionCalendarController |
fusion_calendar_controller.xml | t-inherit calendar.AttendeeCalendarController |
res_config_settings_view_form_fusion_schedule |
res_config_settings_views.xml:4 | inherit base.res_config_settings_view_form |
Backend views — fusion_calendar_account_views.xml: list (5), form (24),
action_fusion_calendar_account (56), menu_fusion_calendar_account (63, under
base.menu_custom).
Data — ir_cron_fusion_calendar_sync (ir_cron_data.xml:4, 5 min, _cron_sync_all_accounts);
fusion_schedule_booking_confirmation (mail_template_data.xml:4, model calendar.event,
NOT noupdate); default_appointment_invite (appointment_invite_data.xml:8, noupdate,
short_code book-appointment, empty types).
Security — rules fusion_calendar_account_user_rule (security.xml:5),
fusion_calendar_event_link_user_rule (security.xml:13); ACL: 4 rows in
ir.model.access.csv (account: CRUD user / none public; link: CRU user / full system).
5. Config parameters (ir.config_parameter)
Owned — fusion_schedule_google_client_id, _google_client_secret,
fusion_schedule_microsoft_client_id, _microsoft_client_secret,
fusion_schedule_sync_interval; fusion_schedule.default_work_start / _work_end /
_break_start / _break_duration / _travel_buffer.
Fallback (not owned) — google_calendar_client_id / _secret,
microsoft_calendar_client_id / _secret, web.base.url; and the fusion_claims namespace
fusion_claims.portal_gradient_start/_mid/_end, fusion_claims.google_maps_api_key,
fusion_claims.ai_api_key.
6. External HTTP it talks to
- Google OAuth (
accounts.google.com/o/oauth2/auth,oauth2.googleapis.com/token), Calendar v3 (googleapis.com/calendar/v3), userinfo, Distance Matrix + Geocoding (maps.googleapis.com). Scopes:calendar+userinfo.email. - Microsoft OAuth (
login.microsoftonline.com/common/oauth2/v2.0/*), Graph (graph.microsoft.com/v1.0—me/calendarView/delta,me/events,me). Scopes:offline_access openid Calendars.ReadWrite User.Read. - OpenAI
api.openai.com/v1/chat/completions(gpt-4o-mini) — fallback only.
7. Cross-module touchpoints (full detail in CLAUDE §4)
| direction | what | where |
|---|---|---|
| depends ↓ | fusion_portal (→ fusion_claims stack) |
manifest.py:35 |
| inherit ↓ | fusion_portal.portal_my_home_authorizer |
portal_schedule_tile.xml:6 |
| soft-call ↓ | fusion.api.service (fusion_api) |
portal_schedule.py:104,117 |
| ICP read ↓ | fusion_claims.{portal_gradient_*,google_maps_api_key,ai_api_key} |
portal_schedule.py:33-35,111,126 |
| cookie ← | tz set by fusion_portal/.../timezone_detect.js |
portal_schedule.py:_resolve_timezone |
| shared table | calendar.event (also written by fusion_claims schedule wizard / appointment) |
models/calendar_event.py |
| reverse | none (only fusion_repairs lists it as deferred) | — |
8. Audit cross-reference
19 findings logged → Supabase fusionapps.issues, project Fusion Schedule
(576de219-57e6-4596-8c8c-0c093e4cb54a), all status='open'. Detail + fixes in
CLAUDE.md §16 (deep dives #1–#6). Provenance: AI-generated (Cursor + Claude 4.5 Opus) —
Odoo-19 syntax clean, bugs are semantic. Headlines: (a) timezone double-conversion on schedule_event_reschedule /
public_book_submit / public_manage_reschedule (slot string is UTC but they re-localize);
(b) the sync-dedup cluster — _find_existing_event (:401) and the iCalUID lookup
(:482/:715) are unscoped by user, so same-titled / shared-invite events merge across
different users and resurrect archived ones; (c) public booking mutates an existing
res.partner by attacker-supplied email.
9. Consumed contracts — the OTHER side of each cross-module link (integration boundary)
9.1 fusion.api.service broker (fusion_api, not a manifest dep)
request.env['fusion.api.service'] → KeyError if fusion_api absent (caught by
fusion_schedule's bare except → fallback). 7 models: fusion.api.service (AbstractModel,
broker), fusion.api.{provider,key,consumer,access,usage,user.limit} + usage.daily.
Public methods fusion_schedule uses: get_api_key(provider_type, consumer, feature) →
api_service.py:394; call_openai(consumer, feature, messages, model) → :278. Raises
UserError on 14 conditions (no active provider :62; consumer disabled :129; access
disabled :141; monthly/daily budget :157/167; rpm/rpd :185/194; user blocked/budget/rpd
:218/224/234; no key :81; package missing :280/335; downstream API error :319/381) —
any of these (or KeyError) triggers fusion_schedule's ICP fallback. provider_type enum:
openai, anthropic, google_maps, google_oauth, microsoft_oauth, twilio, custom. Consumer
auto-registers when fusion_api.auto_detect_consumers (default True).
⚠
get_api_keyreturnskey.api_key(agroup_admin-gated field) on a non-sudo recordset (api_service.py:407). For a portal/public request (non-admin/public user) this likely raisesAccessError→ fusion_schedule's fallback fires every time → in practice the maps key for portal/public callers comes fromfusion_claims.google_maps_api_key, not the broker. The broker path may effectively never succeed for raw-key access from the portal.
9.2 portal_gradient / fc_gradient / the tile target (fusion_portal)
portal_gradientcomputed infusion_portal/controllers/portal_main.py:81-87fromfusion_claims.portal_gradient_{start,mid,end}(defaults#5ba848/#3a8fb7/#2e7aad) — only set for portal personas (is_authorizer/is_sales_rep_portal/is_client_portal/is_technician_portal). fusion_schedule computes its own identical copy inportal_schedule.py:33-36, so its pages don't need the controller — only the tile does (viafc_gradient, set atportal_templates.xml:10=portal_gradient or <default>).- Tile xpath fragility: the tiles grid is
<div class="row g-3 mb-4">(portal_templates.xml:52-295); the anchor is the/my/funding-claimscard (:277-294). fusion_schedule's tile xpath (portal_schedule_tile.xml:8) needs both the/my/funding-claims<a>and the exactrow g-3 mb-4class triple — change either and the tile ParseErrors at install of fusion_schedule. tzcookie set byfusion_portal/static/src/js/timezone_detect.js:25: nametz, value raw IANA (America/Toronto),path=/ max-age=31536000 SameSite=Lax. Read atportal_schedule.py:_resolve_timezone(2nd priority afteruser.tz).
9.3 The borrowed fusion_claims.* params — ownership (defaults all match)
| ICP key | owning field | file:line | default |
|---|---|---|---|
fusion_claims.portal_gradient_start/_mid/_end |
fc_portal_gradient_* |
fusion_claims/.../res_config_settings.py:461-474 |
#5ba848/#3a8fb7/#2e7aad |
fusion_claims.ai_api_key |
fc_ai_api_key |
fusion_claims/.../res_config_settings.py:355 |
empty |
fusion_claims.google_maps_api_key |
fc_google_maps_api_key |
fusion_tasks/.../res_config_settings.py:12-16 |
empty |
⚠ The maps key is owned by
fusion_tasks, notfusion_claims(thefusion_claims.*prefix is kept for data continuity). Greppingfusion_claims/for it finds nothing. Both owners are transitive deps via fusion_portal, so the params are always present.
10. Sibling scheduling surfaces & how they interact with this module
Baseline: fusion_schedule is the only calendar.event extender; its write/unlink
push to external is gated by _skip_fc_sync() (context no_calendar_sync/dont_notify) +
presence of links, and _cross_calendar_push (cron) mirrors unlinked Odoo-native events
(−1d…+90d, on the user's partner) to the first account only if the user has >1 account.
| Writer | what it creates | interaction with fusion_schedule |
|---|---|---|
fusion_claims schedule_assessment_wizard.py:186 |
1 calendar.event/manual schedule (assessor partner, optional email alarm), plain create |
eligible for cron mirror; later edits fire the synchronous write() push |
fusion_portal portal_assessment.py:1194 |
1 calendar.event/public booking (sales-rep partner; sets accessibility.assessment.calendar_event_id), plain sudo create |
same as above |
fusion_tasks technician_task.py:1572 (_sync_calendar_event) |
HIGH volume — 1 event/task, re-synced on every schedule-field write/create; writes with silent_ctx (dont_notify=True) |
synchronous push suppressed; external mirror deferred to the 5-min cron. Protection hinges on dont_notify staying in silent_ctx — drop it and every task edit becomes an inline Google/Outlook round-trip |
- Reverse coupling:
fusion_tasksslot scheduler readscalendar.eventfor busy intervals (technician_task.py:495-540) and excludes its own task-linked events, so externally-synced calendar entries (pulled by fusion_schedule) correctly block technician availability. - Repo sweep: only these 4 modules touch
calendar.event/appointments; only fusion_schedule uses Enterpriseappointment.*(the others create rawcalendar.event).fusion_repairsmaintenance booking is still planned. Stale vendored copies of the task engine exist underEntech Plating/andfusion_plating/— not the canonical install path; flag for cleanup. - Maps key consumers:
fusion_taskstravel-time (_calculate_travel_time) andfusion_claimsgoogle_address_autocomplete.jsboth readfusion_claims.google_maps_api_key(owned by fusion_tasks) — same key fusion_schedule falls back to.