feat(map): add protected operations access

This commit is contained in:
lingniu
2026-08-11 18:30:16 +08:00
parent f535b1570b
commit d4593c44cd
9 changed files with 738 additions and 37 deletions
+263 -20
View File
@@ -22,11 +22,15 @@ const i18n = {
filterResult: '筛选', searchVehicle: '搜索车牌 / VIN', searchStation: '搜索站点名称 / 地址', nearbyVehicles: '附近车辆', nearbyStations: '附近加氢站',
detailVehicle: '车辆', detailStation: '加氢站', detailPlate: '车牌', detailVin: 'VIN', detailStatus: '车辆状态', detailActive: '今日上线',
detailSpeed: '当前速度', detailDailyMileage: '今日里程', detailTotalMileage: '累计里程', detailLocation: '实时位置', detailCoordinate: '坐标',
detailStationType: '站点类型', detailAdmin: '行政区域', detailAddress: '详细地址', detailMonthlyHydrogen: '本月加氢量', detailTotalHydrogen: '累计加氢量',
detailStationType: '站点类型', detailAdmin: '行政区域', detailAddress: '详细地址', detailMonthlyHydrogen: '本月加氢量', detailTotalHydrogen: '累计加氢量', detailNavigate: '导航',
valueYes: '是', valueNo: '否', valueUnavailable: '暂无数据', locatePermissionHint: '无法获取位置,请在浏览器中允许位置权限后重试',
locateUnavailableHint: '暂时无法获取设备位置,请稍后重试', locateTimeoutHint: '定位超时,请到开阔区域后重试',
searchUniversal: '搜索位置、站点、车牌或 VIN', searchLocations: '位置', searchEntities: '业务对象', searchEmpty: '没有匹配的地点或对象',
suggestionLocation: '地点', suggestionVehicle: '车辆', suggestionStation: '加氢站', detailMore: '展开全部信息', detailLess: '收起详细信息'
suggestionLocation: '地点', suggestionVehicle: '车辆', suggestionStation: '加氢站', detailMore: '展开全部信息', detailLess: '收起详细信息',
publicStationTitle: '加氢站网络', publicDataStatus: '公共站点目录 · 位置、地址与合作状态', publicDataSource: '公共站点网络',
authEntry: '运营授权', authGranted: '已授权', authTitle: '进入运营数据', authDescription: '车辆全量信息与加氢量仅向获得授权的用户展示。',
authCodeLabel: '访问码', authCodePlaceholder: '输入访问码', authSubmit: '验证并进入', authCancel: '暂不授权', authRequired: '运营数据需要授权访问', authExpiry: '本次授权有效期为 30 分钟,到期后将自动恢复为公开站点视图。',
authDenied: '访问码无效,请重试', authUnavailable: '授权服务暂不可用,请稍后重试', authHydrogenRequired: '授权后查看加氢量', publicHydrogenHidden: '运营数据受保护', authLogout: '退出授权'
},
en: {
vehicleModeTitle: 'Vehicle Network', stationModeTitle: 'H₂ Station Network',
@@ -49,11 +53,15 @@ const i18n = {
filterResult: 'Filtered', searchVehicle: 'Search plate / VIN', searchStation: 'Search station / address', nearbyVehicles: 'Nearby vehicles', nearbyStations: 'Nearby H₂ stations',
detailVehicle: 'Vehicle', detailStation: 'H₂ station', detailPlate: 'Plate', detailVin: 'VIN', detailStatus: 'Status', detailActive: 'Active today',
detailSpeed: 'Speed', detailDailyMileage: 'Today mileage', detailTotalMileage: 'Total mileage', detailLocation: 'Live location', detailCoordinate: 'Coordinates',
detailStationType: 'Station type', detailAdmin: 'Region', detailAddress: 'Address', detailMonthlyHydrogen: 'Monthly hydrogen', detailTotalHydrogen: 'Total hydrogen',
detailStationType: 'Station type', detailAdmin: 'Region', detailAddress: 'Address', detailMonthlyHydrogen: 'Monthly hydrogen', detailTotalHydrogen: 'Total hydrogen', detailNavigate: 'Navigate',
valueYes: 'Yes', valueNo: 'No', valueUnavailable: 'Unavailable', locatePermissionHint: 'Location access is blocked. Allow it in your browser and try again.',
locateUnavailableHint: 'Your location is temporarily unavailable. Please try again.', locateTimeoutHint: 'Location timed out. Move to an open area and try again.',
searchUniversal: 'Search location, station, plate, or VIN', searchLocations: 'Locations', searchEntities: 'Results', searchEmpty: 'No matching location or entity',
suggestionLocation: 'Location', suggestionVehicle: 'Vehicle', suggestionStation: 'H₂ station', detailMore: 'Show all details', detailLess: 'Collapse details'
suggestionLocation: 'Location', suggestionVehicle: 'Vehicle', suggestionStation: 'H₂ station', detailMore: 'Show all details', detailLess: 'Collapse details',
publicStationTitle: 'H₂ Station Network', publicDataStatus: 'Public station directory · location, address and partner status', publicDataSource: 'Public station network',
authEntry: 'Operations access', authGranted: 'Access granted', authTitle: 'Enter operations data', authDescription: 'Vehicle information and hydrogen volumes require authorized access.',
authCodeLabel: 'Access code', authCodePlaceholder: 'Enter access code', authSubmit: 'Verify and continue', authCancel: 'Not now', authRequired: 'Operations data requires authorized access', authExpiry: 'This authorization lasts 30 minutes. When it expires, the public station view is restored.',
authDenied: 'The access code is invalid. Try again.', authUnavailable: 'Authorization is temporarily unavailable. Try again later.', authHydrogenRequired: 'Authorize to view hydrogen volume', publicHydrogenHidden: 'Operations data protected', authLogout: 'Sign out'
}
};
@@ -88,7 +96,7 @@ let map = null;
let markerList = [];
let infoWindowList = [];
let is3DPitch = true;
let currentMode = 'vehicle';
let currentMode = 'station';
let currentTheme = 'theme-white';
let currentLang = 'zh';
let currentRankType = 'primary';
@@ -109,6 +117,7 @@ let exploreSuggestionItems = [];
let activeExploreSuggestionIndex = -1;
let detailExpanded = false;
const filterState = { province: '', city: '', district: '', query: '' };
let accessState = { authorized: false, principal: null, pendingMode: null };
const REGION_ZOOM = { city: 7, district: 9.5, vehicle: 12 };
const MUNICIPALITIES = new Set(['北京', '天津', '上海', '重庆']);
@@ -116,27 +125,75 @@ const MUNICIPALITIES = new Set(['北京', '天津', '上海', '重庆']);
document.addEventListener('DOMContentLoaded', () => {
initClock();
initAMapInstance();
loadDashboard();
refreshTimer = window.setInterval(loadDashboard, 15000);
initializeDataAccess();
});
document.addEventListener('pointerdown', event => {
if (!event.target?.closest?.('.map-explore-toolbar')) closeExploreSuggestions();
});
async function loadDashboard() {
async function initializeDataAccess() {
await loadSession();
await loadPublicStations();
if (accessState.authorized) loadOperationalDashboard();
refreshTimer = window.setInterval(() => {
if (accessState.authorized) loadOperationalDashboard();
else loadPublicStations();
}, 15000);
}
async function loadSession() {
try {
const response = await fetch('/api/session', { headers: { Accept: 'application/json' } });
const payload = await response.json();
accessState.authorized = Boolean(payload.authorized);
accessState.principal = payload.principal || null;
} catch (error) {
accessState.authorized = false;
accessState.principal = null;
}
updateAccessUI();
}
async function loadPublicStations() {
setDataState('loading');
try {
const response = await fetch('/api/public/stations', { headers: { Accept: 'application/json' } });
const payload = await response.json();
if (!response.ok || payload.status !== 'ok') throw new Error(payload.message || `HTTP ${response.status}`);
if (!accessState.authorized || currentMode === 'station') dashboard = payload;
void syncFilterControls(payload);
updateDashboardUI();
if (currentMode === 'station') refreshStationViewport();
setDataState('ready');
} catch (error) {
console.error('public station refresh failed', error);
setDataState('error');
}
}
async function loadOperationalDashboard() {
setDataState('loading');
try {
const response = await fetch('/api/dashboard', { headers: { Accept: 'application/json' } });
const payload = await response.json();
if (response.status === 401) {
accessState.authorized = false;
accessState.principal = null;
updateAccessUI();
if (currentMode === 'vehicle') switchMode('station');
await loadPublicStations();
return;
}
if (!response.ok || payload.status !== 'ok') throw new Error(payload.message || `HTTP ${response.status}`);
dashboard = payload;
void syncFilterControls(payload);
updateDashboardUI();
refreshVehicleRegionNodes(payload);
if (currentMode === 'vehicle') refreshVehicleRegionNodes(payload);
else refreshStationViewport();
setDataState('ready');
} catch (error) {
console.error('dashboard refresh failed', error);
console.error('operational dashboard refresh failed', error);
setDataState('error');
}
}
@@ -148,6 +205,97 @@ function setDataState(state) {
if (state === 'error') el.textContent = currentLang === 'zh' ? '数据同步失败 · 将自动重试' : 'Data sync failed · Retrying';
}
function operationalAccessGranted() {
return Boolean(accessState.authorized);
}
function updateAccessUI() {
const dict = i18n[currentLang];
const trigger = document.getElementById('accessTrigger');
if (trigger) {
trigger.classList.toggle('is-authorized', operationalAccessGranted());
trigger.textContent = operationalAccessGranted() ? dict.authGranted : dict.authEntry;
trigger.setAttribute('aria-label', operationalAccessGranted() ? dict.authLogout : dict.authEntry);
trigger.title = operationalAccessGranted() ? dict.authLogout : dict.authEntry;
}
document.getElementById('btnModeVehicle')?.classList.toggle('is-locked', !operationalAccessGranted());
}
function openAccessDialog(targetMode = 'vehicle', message = '') {
accessState.pendingMode = targetMode;
const dict = i18n[currentLang];
const dialog = document.getElementById('accessDialog');
const hint = document.getElementById('accessDialogHint');
const input = document.getElementById('accessCodeInput');
if (hint) hint.textContent = message || dict.authDescription;
if (input) input.value = '';
if (dialog?.showModal) dialog.showModal();
else dialog?.setAttribute('open', '');
window.setTimeout(() => input?.focus(), 0);
}
function closeAccessDialog() {
const dialog = document.getElementById('accessDialog');
if (dialog?.close) dialog.close();
else dialog?.removeAttribute('open');
const error = document.getElementById('accessDialogError');
if (error) { error.hidden = true; error.textContent = ''; }
}
async function authorizeOperationsAccess() {
const dict = i18n[currentLang];
const input = document.getElementById('accessCodeInput');
const submit = document.getElementById('accessSubmitBtn');
const error = document.getElementById('accessDialogError');
const accessCode = input?.value?.trim();
if (!accessCode) {
if (error) { error.hidden = false; error.textContent = dict.authDenied; }
input?.focus();
return;
}
if (submit) submit.disabled = true;
if (error) error.hidden = true;
try {
const response = await fetch('/api/session', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({ accessCode }),
});
const payload = await response.json();
if (!response.ok || !payload.authorized) {
if (error) { error.hidden = false; error.textContent = response.status === 503 ? dict.authUnavailable : dict.authDenied; }
return;
}
accessState.authorized = true;
accessState.principal = payload.principal || null;
updateAccessUI();
closeAccessDialog();
await loadOperationalDashboard();
const targetMode = accessState.pendingMode || 'vehicle';
accessState.pendingMode = null;
if (targetMode === 'vehicle') switchMode('vehicle', { skipAuthCheck: true });
else updateDashboardUI();
} catch (requestError) {
if (error) { error.hidden = false; error.textContent = dict.authUnavailable; }
} finally {
if (submit) submit.disabled = false;
}
}
async function toggleOperationsAccess() {
if (!operationalAccessGranted()) {
openAccessDialog('vehicle');
return;
}
try { await fetch('/api/session', { method: 'DELETE', headers: { Accept: 'application/json' } }); } catch (error) { /* local state still resets safely */ }
accessState.authorized = false;
accessState.principal = null;
accessState.pendingMode = null;
updateAccessUI();
if (currentMode === 'vehicle') switchMode('station', { skipAuthCheck: true });
await loadPublicStations();
}
function normalizeSearch(value) {
return String(value || '').trim().toLocaleLowerCase();
}
@@ -181,6 +329,10 @@ function selectedVehicleFilterGroup() {
return null;
}
function hasPinnedVehicleDistrictFilter() {
return currentMode === 'vehicle' && Boolean(filterState.district) && !normalizeSearch(filterState.query);
}
function filteredVehicles(vehicles = dashboard?.vehicles || []) {
const group = selectedVehicleFilterGroup();
const allowedVins = group ? new Set(group.vehicles.map(vehicle => vehicle.vin)) : null;
@@ -572,10 +724,40 @@ function focusCoordinates(items, zoom) {
map.setZoomAndCenter(zoom, [center[0] / located.length, center[1] / located.length]);
}
function focusVehicleSet(vehicles) {
const points = locatedVehicles(vehicles).map(vehicle => wgs84ToGcj02(Number(vehicle.longitude), Number(vehicle.latitude)));
if (!map || !points.length) return;
if (points.length === 1) {
map.setZoomAndCenter(14, points[0]);
return;
}
const longitudes = points.map(point => point[0]);
const latitudes = points.map(point => point[1]);
const minLng = Math.min(...longitudes); const maxLng = Math.max(...longitudes);
const minLat = Math.min(...latitudes); const maxLat = Math.max(...latitudes);
const center = [(minLng + maxLng) / 2, (minLat + maxLat) / 2];
const latitudeScale = Math.cos(center[1] * Math.PI / 180);
const spanMeters = Math.max((maxLat - minLat) * 111320, (maxLng - minLng) * 111320 * latitudeScale, 400);
const size = map.getSize?.();
const width = Number(size?.getWidth?.()) || 900;
const height = Number(size?.getHeight?.()) || 560;
// Reserve room for the toolbar, detail card and map controls on every viewport.
const usablePixels = Math.max(180, Math.min(width * 0.72, height * 0.68));
const metersPerPixel = spanMeters / usablePixels;
const zoom = Math.max(7, Math.min(14, Math.log2((156543.03392 * latitudeScale) / metersPerPixel)));
map.setZoomAndCenter(zoom, center);
}
function focusCurrentRegion(level) {
if (!map) return;
if (currentMode === 'vehicle') {
const group = selectedVehicleFilterGroup();
if (level === 'district' && hasPinnedVehicleDistrictFilter() && locatedVehicles(filteredVehicles()).length) {
// District borders can be elongated. Fit the actual filtered fleet rather
// than centering on the administrative centroid, which can hide a valid car.
focusVehicleSet(filteredVehicles());
return;
}
if (group?.lnglat) map.setZoomAndCenter(level === 'province' ? 7.4 : level === 'city' ? 10 : 12.5, group.lnglat);
return;
}
@@ -682,10 +864,12 @@ function updateDashboardUI() {
document.getElementById('kpiFleetOnline').innerHTML = `${formatNumber(active)} <small>${countUnit(active, currentMode, dict)} (${activeRate}%)</small>`;
const activity = currentMode === 'vehicle'
? { label: dict.kpiDailyMileage, value: summary.todayMileageKm, unit: 'km' }
: { label: dict.kpiMonthlyHydrogen, value: summary.monthlyHydrogenKg, unit: 'kg' };
: operationalAccessGranted()
? { label: dict.kpiMonthlyHydrogen, value: summary.monthlyHydrogenKg, unit: 'kg' }
: { label: dict.publicHydrogenHidden, value: null, unit: '' };
document.getElementById('kpiActivityLabel').textContent = activity.label;
document.getElementById('kpiDailyDist').innerHTML = activity.value == null
? `— <small>${activity.unit}</small>`
? `${activity.unit ? ` <small>${activity.unit}</small>` : ''}`
: `${formatNumber(activity.value, 1)} <small>${activity.unit}</small>`;
const regionSummary = buildVehicleRegionNodes();
document.getElementById('mapStatusText').textContent = mapStatusSummaryText(summary, regionSummary, dashboard.asOf);
@@ -700,9 +884,12 @@ function mapStatusSummaryText(summary, regionSummary, asOf, mode = currentMode,
if (mode === 'station') {
const cooperative = Number(summary.cooperativeStations || 0);
const external = Math.max(0, Number(summary.totalStations || 0) - cooperative);
const accessHint = operationalAccessGranted()
? ''
: lang === 'zh' ? ' · 站点目录公开展示' : ' · public station directory';
return lang === 'zh'
? `开放平台已同步 · ${summary.totalStations}座加氢站 · ${cooperative}座合作站点 · ${external}座外部站点 · ${asOf}`
: `Open platform synced · ${summary.totalStations} H₂ stations · ${cooperative} partner stations · ${external} external stations · ${asOf}`;
? `开放平台已同步 · ${summary.totalStations}座加氢站 · ${cooperative}座合作站点 · ${external}座外部站点${accessHint} · ${asOf}`
: `Open platform synced · ${summary.totalStations} H₂ stations · ${cooperative} partner stations · ${external} external stations${accessHint} · ${asOf}`;
}
const regionText = regionSummary.loading ? regionLoadingText(regionSummary.level, lang) : regionSummaryText(regionSummary, lang);
return lang === 'zh'
@@ -818,6 +1005,7 @@ function stationAdministrativePath(station, lang = currentLang) {
}
function hierarchyLevelForZoom(zoom = map?.getZoom?.() || 0) {
if (hasPinnedVehicleDistrictFilter()) return 'vehicle';
if (zoom < REGION_ZOOM.city) return 'province';
if (zoom < REGION_ZOOM.district) return 'city';
if (zoom < REGION_ZOOM.vehicle) return 'district';
@@ -1005,7 +1193,7 @@ function vehiclePointNodes(vehicles = filteredVehicles()) {
return locatedVehicles(vehicles).map(vehicle => {
const lnglat = wgs84ToGcj02(Number(vehicle.longitude), Number(vehicle.latitude));
return { vehicle, lnglat };
}).filter(({ lnglat }) => !bounds || (lnglat[0] >= bounds.west && lnglat[0] <= bounds.east && lnglat[1] >= bounds.south && lnglat[1] <= bounds.north))
}).filter(({ lnglat }) => hasPinnedVehicleDistrictFilter() || !bounds || (lnglat[0] >= bounds.west && lnglat[0] <= bounds.east && lnglat[1] >= bounds.south && lnglat[1] <= bounds.north))
.map(({ vehicle, lnglat }) => ({
id: `vehicle-point-${vehicle.vin}`, kind: 'vehiclePoint', level: 'vehicle',
nameZh: vehicle.plateNumber || vehicle.vin, nameEn: vehicle.plateNumber || vehicle.vin,
@@ -1171,6 +1359,42 @@ function vehicleStatusLabel(vehicle) {
return status;
}
function navigationCoordinates(mode, entity) {
const longitude = Number(entity?.longitude);
const latitude = Number(entity?.latitude);
if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) return null;
// Vehicle telemetry is WGS-84 and is converted before rendering on AMap.
// Station assets already use AMap (GCJ-02) coordinates.
return mode === 'vehicle' ? wgs84ToGcj02(longitude, latitude) : [longitude, latitude];
}
function navigationUrlForEntity(mode, entity) {
const coordinates = navigationCoordinates(mode, entity);
if (!coordinates?.every(Number.isFinite)) return null;
const name = mode === 'station'
? entity?.name || entity?.shortName || i18n[currentLang].detailStation
: entity?.plateNumber || entity?.vin || i18n[currentLang].detailVehicle;
const [longitude, latitude] = coordinates.map(value => Number(value).toFixed(6));
return `https://uri.amap.com/navigation?to=${longitude},${latitude},${encodeURIComponent(name)}&mode=car&coordinate=gaode&callnative=1`;
}
function updateDetailNavigation(mode, entity) {
const button = document.getElementById('detailNavigateBtn');
if (!button) return;
const url = navigationUrlForEntity(mode, entity);
const label = i18n[currentLang].detailNavigate;
button.hidden = !url;
button.disabled = !url;
button.dataset.navigationUrl = url || '';
button.setAttribute('aria-label', label);
button.title = label;
}
function navigateToSelectedEntity() {
const url = selectedEntity && navigationUrlForEntity(selectedEntity.mode, selectedEntity.entity);
if (url) window.location.assign(url);
}
function showEntityDetails(mode, entity) {
if (!entity) return;
const dict = i18n[currentLang];
@@ -1193,14 +1417,17 @@ function showEntityDetails(mode, entity) {
title.textContent = entity.name || entity.shortName || dict.detailStation;
subtitle.textContent = entity.shortName && entity.shortName !== entity.name ? entity.shortName : stationAdministrativePath(entity);
const admin = [stationAdministrativePath(entity), stationDistrictName(entity)].filter(Boolean).filter((value, index, items) => index === 0 || !items[0].includes(value)).join(' · ');
grid.innerHTML = [
const publicFields = [
detailField(dict.detailStationType, entity.cooperative ? dict.stationCooperative : dict.stationExternal),
detailField(dict.detailAdmin, detailValue(admin)),
detailField(dict.detailAddress, detailValue(entity.address), true),
detailField(dict.detailCoordinate, `${formatNumber(entity.longitude, 6)}, ${formatNumber(entity.latitude, 6)}`, true, true)
];
const operationalFields = operationalAccessGranted() ? [
detailField(dict.detailMonthlyHydrogen, entity.monthlyHydrogenKg == null ? dict.valueUnavailable : `${formatNumber(entity.monthlyHydrogenKg, 1)} kg`),
detailField(dict.detailTotalHydrogen, entity.totalHydrogenKg == null ? dict.valueUnavailable : `${formatNumber(entity.totalHydrogenKg, 1)} kg`, false, true),
detailField(dict.detailCoordinate, `${formatNumber(entity.longitude, 6)}, ${formatNumber(entity.latitude, 6)}`, true, true)
].join('');
] : [];
grid.innerHTML = [...publicFields, ...operationalFields].join('');
} else {
title.textContent = entity.plateNumber || entity.vin || dict.detailVehicle;
subtitle.textContent = entity.vin || '';
@@ -1219,6 +1446,7 @@ function showEntityDetails(mode, entity) {
}
expand.hidden = false;
expand.textContent = dict.detailMore;
updateDetailNavigation(mode, entity);
card.hidden = false;
renderAMapMarkers();
renderRankingList(currentRankType);
@@ -1385,17 +1613,27 @@ function updateRankingControls() {
document.getElementById('panelRankTitle').textContent = title;
if (currentMode === 'vehicle') updateStationViewportMeta(0, 0);
document.getElementById('rankPrimaryTab').textContent = currentMode === 'vehicle' ? dict.rankByFleet : dict.rankByStations;
document.getElementById('rankSecondaryTab').textContent = currentMode === 'vehicle' ? dict.rankByDist : dict.rankByHydrogen;
document.getElementById('rankSecondaryTab').textContent = currentMode === 'vehicle'
? dict.rankByDist
: operationalAccessGranted() ? dict.rankByHydrogen : dict.authHydrogenRequired;
document.querySelectorAll('.gtab').forEach(button => button.classList.toggle('active', button.id === (currentRankType === 'primary' ? 'rankPrimaryTab' : 'rankSecondaryTab')));
}
function switchRankTab(type) {
if (currentMode === 'station' && type === 'secondary' && !operationalAccessGranted()) {
openAccessDialog('station', i18n[currentLang].authHydrogenRequired);
return;
}
currentRankType = type;
updateRankingControls();
renderRankingList(type);
}
function switchMode(mode) {
function switchMode(mode, { skipAuthCheck = false } = {}) {
if (mode === 'vehicle' && !skipAuthCheck && !operationalAccessGranted()) {
openAccessDialog('vehicle', i18n[currentLang].authRequired);
return;
}
currentMode = mode;
filterState.province = ''; filterState.city = ''; filterState.district = ''; filterState.query = '';
closeExploreSuggestions();
@@ -1409,6 +1647,7 @@ function switchMode(mode) {
updateLocateButton(userLocation ? 'active' : 'idle');
updateDashboardUI();
if (mode === 'vehicle') refreshVehicleRegionNodes(dashboard);
else refreshStationViewport();
}
function setLanguage(lang) {
@@ -1420,6 +1659,9 @@ function setLanguage(lang) {
document.querySelectorAll('[data-i18n]').forEach(element => {
const key = element.getAttribute('data-i18n'); if (dict[key]) element.textContent = dict[key];
});
document.querySelectorAll('[data-i18n-placeholder]').forEach(element => {
const key = element.getAttribute('data-i18n-placeholder'); if (dict[key]) element.placeholder = dict[key];
});
const brandLogo = document.getElementById('brandLogo');
const brandLogoEnglish = document.getElementById('brandLogoEnglish');
if (brandLogo) {
@@ -1430,6 +1672,7 @@ function setLanguage(lang) {
}
void syncFilterControls(dashboard);
updateLocateButton(userLocation ? 'active' : 'idle');
updateAccessUI();
updateDashboardUI();
if (selectedEntity?.entity) showEntityDetails(selectedEntity.mode, selectedEntity.entity);
}