Files
boc/iom/landvex-dashboard/dashboard.js
T
Bernt bae705aa97 ARCHITECTURE: NFC roadmap, edge AI, audit logging
- Add NFC ePassport roadmap (ICAO 9303, eIDAS)
- Add TensorFlow.js edge face detection (BlazeFace)
- Add structured audit logger (GDPR-compliant)
- Risk scoring support

Part of KYC Apple Native UX v1.1.0
2026-06-29 16:24:48 +00:00

304 lines
8.7 KiB
JavaScript

/**
* Landvex Dashboard
* Interactive web dashboard for urban intelligence
*/
// API Configuration
const API_URL = 'https://api.landvex.com/v1';
// State
let currentView = 'overview';
let selectedLocation = null;
let observations = [];
let map = null;
let charts = {};
// Initialize dashboard
document.addEventListener('DOMContentLoaded', () => {
initDashboard();
});
function initDashboard() {
loadStats();
loadObservations();
initMap();
initCharts();
setupEventListeners();
startRealtimeUpdates();
}
// Load statistics
async function loadStats() {
try {
const response = await fetch(`${API_URL}/stats`);
const data = await response.json();
updateStatCard('total-observations', data.total_observations, '+12%');
updateStatCard('active-zoomers', data.active_zoomers, '+8%');
updateStatCard('avg-rgi', data.avg_rgi.toFixed(1), '+0.3');
updateStatCard('pending-actions', data.pending_actions, '-5');
} catch (error) {
console.log('Using mock data');
updateStatCard('total-observations', 1247, '+12%');
updateStatCard('active-zoomers', 89, '+8%');
updateStatCard('avg-rgi', 4.2, '+0.3');
updateStatCard('pending-actions', 23, '-5');
}
}
function updateStatCard(id, value, change) {
const card = document.getElementById(id);
if (!card) return;
const valueEl = card.querySelector('.stat-value');
const changeEl = card.querySelector('.stat-change');
if (valueEl) valueEl.textContent = value.toLocaleString();
if (changeEl) {
changeEl.textContent = change;
changeEl.className = 'stat-change ' + (change.startsWith('+') ? 'positive' : 'negative');
}
}
// Load observations
async function loadObservations() {
try {
const response = await fetch(`${API_URL}/observations?limit=10`);
const data = await response.json();
observations = data.observations || [];
renderObservationsTable();
} catch (error) {
console.log('Using mock observations');
observations = getMockObservations();
renderObservationsTable();
}
}
function getMockObservations() {
return [
{ id: 'OBS-001247', lat: 59.3293, lng: 18.0686, type: 'Street Light', condition: 3, zoomer: 'Marcus K.', status: 'approved', date: '2026-06-26' },
{ id: 'OBS-001246', lat: 59.3301, lng: 18.0692, type: 'Sidewalk', condition: 4, zoomer: 'Sophie V.', status: 'pending', date: '2026-06-26' },
{ id: 'OBS-001245', lat: 59.3289, lng: 18.0678, type: 'Building', condition: 2, zoomer: 'Alicia L.', status: 'approved', date: '2026-06-25' },
{ id: 'OBS-001244', lat: 59.3312, lng: 18.0701, type: 'Road', condition: 5, zoomer: 'James R.', status: 'pending', date: '2026-06-25' },
{ id: 'OBS-001243', lat: 59.3278, lng: 18.0665, type: 'Bridge', condition: 3, zoomer: 'Emma T.', status: 'rejected', date: '2026-06-24' },
];
}
function renderObservationsTable() {
const tbody = document.getElementById('observations-tbody');
if (!tbody) return;
tbody.innerHTML = observations.map(obs => `
<tr data-id="${obs.id}">
<td>${obs.id}</td>
<td>${obs.lat.toFixed(4)}, ${obs.lng.toFixed(4)}</td>
<td>${obs.type}</td>
<td><span class="condition-indicator condition-${obs.condition}"></span>${obs.condition}/5</td>
<td>${obs.zoomer}</td>
<td><span class="status-badge status-${obs.status}">${obs.status}</span></td>
<td>${obs.date}</td>
</tr>
`).join('');
// Add click handlers
tbody.querySelectorAll('tr').forEach(row => {
row.addEventListener('click', () => {
const id = row.dataset.id;
showObservationDetail(id);
});
});
}
// Initialize map (placeholder for real map integration)
function initMap() {
const mapContainer = document.getElementById('map-container');
if (!mapContainer) return;
// Placeholder - integrate with Mapbox, Google Maps, or Leaflet
mapContainer.innerHTML = `
<div class="map-placeholder">
<div class="map-icon">🗺️</div>
<p>Interactive Map</p>
<p>${observations.length} observations</p>
<button class="btn btn-primary" onclick="loadRealMap()">Load Map</button>
</div>
`;
}
function loadRealMap() {
// Integrate with actual map library
console.log('Loading real map...');
}
// Initialize charts (placeholder for Chart.js or similar)
function initCharts() {
const rgiChart = document.getElementById('rgi-chart');
if (rgiChart) {
renderRGIChart();
}
}
function renderRGIChart() {
const dimensions = [
{ name: 'Physical', value: 5.5, color: '#007AFF' },
{ name: 'Organizational', value: 3.8, color: '#34C759' },
{ name: 'Safety', value: 4.1, color: '#FF9500' },
{ name: 'Economic', value: 2.9, color: '#FF3B30' },
{ name: 'Maintenance', value: 4.7, color: '#5856D6' },
{ name: 'Environmental', value: 3.2, color: '#AF52DE' },
{ name: 'Informational', value: 3.5, color: '#5AC8FA' },
];
const chartContainer = document.getElementById('rgi-chart');
if (!chartContainer) return;
chartContainer.innerHTML = dimensions.map(d => `
<div class="rgi-bar-container">
<div class="rgi-bar-label">${d.name}</div>
<div class="rgi-bar-wrapper">
<div class="rgi-bar" style="width: ${(d.value / 7 * 100).toFixed(0)}%; background-color: ${d.color};"></div>
</div>
<div class="rgi-bar-value">${d.value}</div>
</div>
`).join('');
}
// Setup event listeners
function setupEventListeners() {
// Navigation
document.querySelectorAll('.nav-link').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const view = e.target.dataset.view;
switchView(view);
});
});
// Filter buttons
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const filter = e.target.dataset.filter;
applyFilter(filter);
});
});
// Export button
const exportBtn = document.getElementById('export-btn');
if (exportBtn) {
exportBtn.addEventListener('click', exportData);
}
}
// Switch view
function switchView(view) {
currentView = view;
// Hide all views
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
// Show selected view
const selectedView = document.getElementById(`view-${view}`);
if (selectedView) {
selectedView.classList.add('active');
}
// Update nav
document.querySelectorAll('.nav-link').forEach(link => {
link.classList.toggle('active', link.dataset.view === view);
});
}
// Apply filter
function applyFilter(filter) {
console.log('Applying filter:', filter);
// Filter observations and update table
}
// Show observation detail
function showObservationDetail(id) {
const obs = observations.find(o => o.id === id);
if (!obs) return;
// Show modal or navigate to detail view
console.log('Observation detail:', obs);
}
// Export data
function exportData() {
const data = {
observations,
exported_at: new Date().toISOString(),
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `landvex-export-${new Date().toISOString().split('T')[0]}.json`;
a.click();
URL.revokeObjectURL(url);
}
// Real-time updates
function startRealtimeUpdates() {
// Poll for new observations every 30 seconds
setInterval(() => {
loadObservations();
}, 30000);
// Update stats every minute
setInterval(() => {
loadStats();
}, 60000);
}
// Search functionality
function searchObservations(query) {
const filtered = observations.filter(obs =>
obs.id.toLowerCase().includes(query.toLowerCase()) ||
obs.type.toLowerCase().includes(query.toLowerCase()) ||
obs.zoomer.toLowerCase().includes(query.toLowerCase())
);
renderFilteredObservations(filtered);
}
function renderFilteredObservations(filtered) {
const tbody = document.getElementById('observations-tbody');
if (!tbody) return;
tbody.innerHTML = filtered.map(obs => `
<tr data-id="${obs.id}">
<td>${obs.id}</td>
<td>${obs.lat.toFixed(4)}, ${obs.lng.toFixed(4)}</td>
<td>${obs.type}</td>
<td><span class="condition-indicator condition-${obs.condition}"></span>${obs.condition}/5</td>
<td>${obs.zoomer}</td>
<td><span class="status-badge status-${obs.status}">${obs.status}</span></td>
<td>${obs.date}</td>
</tr>
`).join('');
}
// RGI calculation
function calculateRGI(observations) {
if (!observations.length) return 0;
const conditions = observations.map(o => o.condition);
const avgCondition = conditions.reduce((a, b) => a + b, 0) / conditions.length;
// RGI = 7 - average condition (inverted)
return Math.max(0, 7 - avgCondition).toFixed(1);
}
// Export for global access
window.LandvexDashboard = {
switchView,
applyFilter,
exportData,
searchObservations,
calculateRGI,
};