2026-04-13-3
This commit is contained in:
Vendored
BIN
Binary file not shown.
Generated
+36
@@ -14,6 +14,7 @@
|
|||||||
"@nestjs/core": "^11.0.1",
|
"@nestjs/core": "^11.0.1",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
"@nestjs/schedule": "^6.1.1",
|
"@nestjs/schedule": "^6.1.1",
|
||||||
|
"@nestjs/serve-static": "^5.0.1",
|
||||||
"@nestjs/swagger": "^11.2.6",
|
"@nestjs/swagger": "^11.2.6",
|
||||||
"@nestjs/typeorm": "^11.0.0",
|
"@nestjs/typeorm": "^11.0.0",
|
||||||
"axios": "^1.13.6",
|
"axios": "^1.13.6",
|
||||||
@@ -2364,6 +2365,41 @@
|
|||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@nestjs/serve-static": {
|
||||||
|
"version": "5.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nestjs/serve-static/-/serve-static-5.0.5.tgz",
|
||||||
|
"integrity": "sha512-AhYx3N9aMwR2cb0w5Nlb5nHNYiAcF74ea/D/xna+PxlXwjmwGN/PpC/5fuMtOwmPBMgOTxNPOnB8C9LDZBSgyw==",
|
||||||
|
"dependencies": {
|
||||||
|
"path-to-regexp": "8.4.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@fastify/static": "^8.0.4 || ^9.0.0",
|
||||||
|
"@nestjs/common": "^11.0.2",
|
||||||
|
"@nestjs/core": "^11.0.2",
|
||||||
|
"express": "^5.0.1",
|
||||||
|
"fastify": "^5.2.1"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@fastify/static": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"express": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"fastify": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nestjs/serve-static/node_modules/path-to-regexp": {
|
||||||
|
"version": "8.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
|
||||||
|
"integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nestjs/swagger": {
|
"node_modules/@nestjs/swagger": {
|
||||||
"version": "11.2.6",
|
"version": "11.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.2.6.tgz",
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"@nestjs/core": "^11.0.1",
|
"@nestjs/core": "^11.0.1",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
"@nestjs/schedule": "^6.1.1",
|
"@nestjs/schedule": "^6.1.1",
|
||||||
|
"@nestjs/serve-static": "^5.0.1",
|
||||||
"@nestjs/swagger": "^11.2.6",
|
"@nestjs/swagger": "^11.2.6",
|
||||||
"@nestjs/typeorm": "^11.0.0",
|
"@nestjs/typeorm": "^11.0.0",
|
||||||
"axios": "^1.13.6",
|
"axios": "^1.13.6",
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
// Intaleq Intelligence Dashboard Control
|
||||||
|
// المتحكم البرمجي في لوحة ذكاء الخرائط
|
||||||
|
|
||||||
|
const API_BASE = '/api';
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
fetchSummary();
|
||||||
|
fetchCandidates();
|
||||||
|
fetchClosures();
|
||||||
|
|
||||||
|
// Event Listeners
|
||||||
|
document.getElementById('run-analysis-btn').addEventListener('click', runAnalysis);
|
||||||
|
document.getElementById('discover-closures-btn').addEventListener('click', discoverClosures);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- API FETCHERS ---
|
||||||
|
|
||||||
|
async function fetchSummary() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/map-refinement/summary`);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
document.getElementById('stat-telemetry').innerText = Number(data.telemetry.total).toLocaleString();
|
||||||
|
document.getElementById('stat-roads').innerText = Number(data.roads.analyzed).toLocaleString();
|
||||||
|
document.getElementById('stat-pending').innerText = Number(data.candidates.pending).toLocaleString();
|
||||||
|
document.getElementById('stat-closed').innerText = Number(data.roads.closed || 0).toLocaleString();
|
||||||
|
} catch (e) {
|
||||||
|
showToast('Failed to fetch summary');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchCandidates() {
|
||||||
|
const tableBody = document.querySelector('#candidates-table tbody');
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/map-refinement/candidates?status=pending`);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
tableBody.innerHTML = '<tr class="empty-state"><td colspan="6">No pending discoveries 🏜️</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tableBody.innerHTML = data.map(c => `
|
||||||
|
<tr>
|
||||||
|
<td title="${c.id}">${c.id.substring(0,8)}...</td>
|
||||||
|
<td><i class="fas fa-user-tag"></i> ${c.uniqueDriverCount}</td>
|
||||||
|
<td>${c.totalPoints}</td>
|
||||||
|
<td>${Math.round(c.lengthMeters)}m</td>
|
||||||
|
<td><span class="badge-conf ${getConfClass(c.confidence)}">${Math.round(c.confidence * 100)}%</span></td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm-success" onclick="approveCandidate('${c.id}')"><i class="fas fa-check"></i></button>
|
||||||
|
<button class="btn btn-sm-danger" onclick="rejectCandidate('${c.id}')"><i class="fas fa-times"></i></button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
} catch (e) {
|
||||||
|
tableBody.innerHTML = '<tr class="error-state"><td colspan="6">Error loading candidates</td></tr>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchClosures() {
|
||||||
|
const tableBody = document.querySelector('#closures-table tbody');
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/map-refinement/closures`);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
tableBody.innerHTML = '<tr class="empty-state"><td colspan="3">No segments flagged</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tableBody.innerHTML = data.map(s => `
|
||||||
|
<tr>
|
||||||
|
<td>${s.segmentId}</td>
|
||||||
|
<td>${s.sampleCount} pts</td>
|
||||||
|
<td><span class="badge-conf low">CLOSED</span></td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
} catch (e) {
|
||||||
|
tableBody.innerHTML = '<tr><td colspan="3">Error loading closures</td></tr>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ACTIONS ---
|
||||||
|
|
||||||
|
async function approveCandidate(id) {
|
||||||
|
if (!confirm('Add this path to the approved map queue?')) return;
|
||||||
|
try {
|
||||||
|
await fetch(`${API_BASE}/map-refinement/candidates/${id}/approve`, { method: 'PATCH' });
|
||||||
|
showToast('Road Approved ✅');
|
||||||
|
fetchCandidates();
|
||||||
|
fetchSummary();
|
||||||
|
} catch (e) {
|
||||||
|
showToast('Approval failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rejectCandidate(id) {
|
||||||
|
try {
|
||||||
|
await fetch(`${API_BASE}/map-refinement/candidates/${id}/reject`, { method: 'PATCH' });
|
||||||
|
showToast('Discovery Rejected ❌');
|
||||||
|
fetchCandidates();
|
||||||
|
fetchSummary();
|
||||||
|
} catch (e) {
|
||||||
|
showToast('Rejection failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runAnalysis() {
|
||||||
|
const btn = document.getElementById('run-analysis-btn');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Analyzing...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetch(`${API_BASE}/telemetry/process-intelligence?days=2`, { method: 'POST' });
|
||||||
|
showToast('Deep Intelligence started 🧠. Check Telegram for report soon!');
|
||||||
|
} catch (e) {
|
||||||
|
showToast('Analysis trigger failed');
|
||||||
|
} finally {
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = '<i class="fas fa-play"></i> Run Intelligence';
|
||||||
|
fetchSummary();
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function discoverClosures() {
|
||||||
|
showToast('Scanning for closures... 🚧');
|
||||||
|
try {
|
||||||
|
await fetch(`${API_BASE}/map-refinement/discover-closures`, { method: 'POST' });
|
||||||
|
showToast('Closure scan complete');
|
||||||
|
fetchClosures();
|
||||||
|
fetchSummary();
|
||||||
|
} catch (e) {
|
||||||
|
showToast('Closure discovery failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- HELPERS ---
|
||||||
|
|
||||||
|
function getConfClass(conf) {
|
||||||
|
if (conf > 0.8) return 'high';
|
||||||
|
if (conf > 0.5) return 'med';
|
||||||
|
return 'low';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showToast(msg) {
|
||||||
|
const container = document.getElementById('toast-container');
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = 'toast';
|
||||||
|
toast.innerText = msg;
|
||||||
|
container.appendChild(toast);
|
||||||
|
setTimeout(() => toast.remove(), 4000);
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Intaleq | Map Intelligence Dashboard</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
</head>
|
||||||
|
<body class="dark-theme">
|
||||||
|
<div class="app-container">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="logo-container">
|
||||||
|
<div class="logo-icon"><i class="fas fa-brain-circuit"></i></div>
|
||||||
|
<div class="logo-text">Intaleq<span>Map AI</span></div>
|
||||||
|
</div>
|
||||||
|
<nav class="main-nav">
|
||||||
|
<a href="#" class="nav-item active"><i class="fas fa-chart-line"></i> Summary</a>
|
||||||
|
<a href="#" class="nav-item"><i class="fas fa-road"></i> Candidates</a>
|
||||||
|
<a href="#" class="nav-item"><i class="fas fa-barrier-solid"></i> Closures</a>
|
||||||
|
<a href="/api/docs" target="_blank" class="nav-item"><i class="fas fa-book"></i> API Docs</a>
|
||||||
|
</nav>
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<div class="status-indicator">
|
||||||
|
<span class="dot pulse"></span>
|
||||||
|
<span>Live Traffic Engine</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="main-content">
|
||||||
|
<header class="top-bar">
|
||||||
|
<div class="search-container">
|
||||||
|
<i class="fas fa-search"></i>
|
||||||
|
<input type="text" placeholder="Search segments or drivers...">
|
||||||
|
</div>
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button id="run-analysis-btn" class="btn btn-primary">
|
||||||
|
<i class="fas fa-play"></i> Run Intelligence
|
||||||
|
</button>
|
||||||
|
<div class="user-profile">
|
||||||
|
<img src="https://ui-avatars.com/api/?name=Admin&background=6366f1&color=fff" alt="Admin">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="dashboard-scroll">
|
||||||
|
<!-- Stats Overview -->
|
||||||
|
<section class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon purple"><i class="fas fa-satellite-dish"></i></div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<span class="stat-label">Telemetry Points</span>
|
||||||
|
<h2 id="stat-telemetry">0</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon blue"><i class="fas fa-route"></i></div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<span class="stat-label">Analyzed Segments</span>
|
||||||
|
<h2 id="stat-roads">0</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon orange"><i class="fas fa-sparkles"></i></div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<span class="stat-label">Pending Roads</span>
|
||||||
|
<h2 id="stat-pending">0</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon red"><i class="fas fa-road-barrier"></i></div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<span class="stat-label">Active Closures</span>
|
||||||
|
<h2 id="stat-closed">0</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="content-grid">
|
||||||
|
<!-- Road Candidates Table -->
|
||||||
|
<section class="card candidate-section">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3><i class="fas fa-map-location-dot"></i> Discovered Road Candidates</h3>
|
||||||
|
<button class="btn btn-ghost" onclick="fetchCandidates()">Refresh</button>
|
||||||
|
</div>
|
||||||
|
<div class="table-container">
|
||||||
|
<table id="candidates-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Drivers</th>
|
||||||
|
<th>Points</th>
|
||||||
|
<th>L (m)</th>
|
||||||
|
<th>Confidence</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="empty-state">
|
||||||
|
<td colspan="6">Loading discoveries...</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Road Closures -->
|
||||||
|
<section class="card closure-section">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3><i class="fas fa-road-lock"></i> Road Closures</h3>
|
||||||
|
<div class="card-actions">
|
||||||
|
<button class="btn btn-ghost" onclick="fetchClosures()">Refresh</button>
|
||||||
|
<button id="discover-closures-btn" class="btn btn-secondary-sm">Check Now</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="table-container">
|
||||||
|
<table id="closures-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Segment ID</th>
|
||||||
|
<th>Historical Vol</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="empty-state">
|
||||||
|
<td colspan="3">No closures detected</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Notification Container -->
|
||||||
|
<div id="toast-container"></div>
|
||||||
|
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,365 @@
|
|||||||
|
:root {
|
||||||
|
--bg-dark: #0f172a;
|
||||||
|
--bg-navbar: #1e293b;
|
||||||
|
--card-bg: rgba(30, 41, 59, 0.7);
|
||||||
|
--border-color: #334155;
|
||||||
|
--text-primary: #f8fafc;
|
||||||
|
--text-secondary: #94a3b8;
|
||||||
|
--accent-purple: #8b5cf6;
|
||||||
|
--accent-blue: #3b82f6;
|
||||||
|
--accent-orange: #f59e0b;
|
||||||
|
--accent-red: #ef4444;
|
||||||
|
--accent-green: #10b981;
|
||||||
|
--sidebar-width: 260px;
|
||||||
|
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||||
|
background-color: var(--bg-dark);
|
||||||
|
color: var(--text-primary);
|
||||||
|
overflow: hidden;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-container {
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar Styling */
|
||||||
|
.sidebar {
|
||||||
|
width: var(--sidebar-width);
|
||||||
|
background-color: var(--bg-navbar);
|
||||||
|
border-right: 1px solid var(--border-color);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 1.5rem;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: linear-gradient(135deg, var(--accent-purple), var(--accent-blue));
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text span {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: 400;
|
||||||
|
margin-top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-nav {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item i {
|
||||||
|
width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover, .nav-item.active {
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
background-color: rgba(139, 92, 246, 0.1);
|
||||||
|
color: var(--accent-purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background-color: var(--accent-green);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pulse {
|
||||||
|
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7);
|
||||||
|
animation: pulse 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); }
|
||||||
|
70% { box-shadow: 0 0 0 8px rgba(16, 185, 129, 0); }
|
||||||
|
100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content Area */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: var(--bg-dark);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-bar {
|
||||||
|
height: 70px;
|
||||||
|
padding: 0 2rem;
|
||||||
|
background-color: rgba(15, 23, 42, 0.8);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
position: relative;
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container i {
|
||||||
|
position: absolute;
|
||||||
|
left: 1rem;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.6rem 1rem 0.6rem 2.8rem;
|
||||||
|
background-color: var(--bg-navbar);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 10px;
|
||||||
|
color: white;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-profile {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-profile img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard Content */
|
||||||
|
.dashboard-scroll {
|
||||||
|
flex: 1;
|
||||||
|
padding: 2rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background-color: var(--card-bg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
width: 45px;
|
||||||
|
height: 45px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon.purple { background: rgba(139, 92, 246, 0.15); color: var(--accent-purple); }
|
||||||
|
.stat-icon.blue { background: rgba(59, 130, 246, 0.15); color: var(--accent-blue); }
|
||||||
|
.stat-icon.orange { background: rgba(245, 158, 11, 0.15); color: var(--accent-orange); }
|
||||||
|
.stat-icon.red { background: rgba(239, 68, 68, 0.15); color: var(--accent-red); }
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
display: block;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-info h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content Sections */
|
||||||
|
.content-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.5fr 1fr;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background-color: var(--card-bg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
padding: 1.25rem 1.5rem;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header h3 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table Styling */
|
||||||
|
.table-container {
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:last-child td { border-bottom: none; }
|
||||||
|
|
||||||
|
.empty-state { text-align: center; color: var(--text-secondary); padding: 3rem; }
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btn {
|
||||||
|
padding: 0.6rem 1.2rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary { background-color: var(--accent-purple); color: white; }
|
||||||
|
.btn-primary:hover { background-color: #7c3aed; filter: brightness(1.1); }
|
||||||
|
|
||||||
|
.btn-ghost { background: transparent; color: var(--text-secondary); border: 1px solid var(--border-color); }
|
||||||
|
.btn-ghost:hover { border-color: var(--text-primary); color: white; }
|
||||||
|
|
||||||
|
.btn-secondary-sm { padding: 0.4rem 0.8rem; font-size: 0.8rem; background: var(--bg-navbar); color: var(--accent-blue); border: 1px solid var(--accent-blue); }
|
||||||
|
|
||||||
|
.btn-sm-success { background: rgba(16, 185, 129, 0.1); color: var(--accent-green); padding: 0.3rem 0.7rem; font-size: 0.75rem; }
|
||||||
|
.btn-sm-danger { background: rgba(239, 68, 68, 0.1); color: var(--accent-red); padding: 0.3rem 0.7rem; font-size: 0.75rem; }
|
||||||
|
|
||||||
|
/* Confidence Badge */
|
||||||
|
.badge-conf {
|
||||||
|
padding: 0.2rem 0.6rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.high { background: rgba(16, 185, 129, 0.2); color: var(--accent-green); }
|
||||||
|
.med { background: rgba(245, 158, 11, 0.2); color: var(--accent-orange); }
|
||||||
|
.low { background: rgba(239, 68, 68, 0.2); color: var(--accent-red); }
|
||||||
|
|
||||||
|
/* Custom Scrollbar */
|
||||||
|
::-webkit-scrollbar { width: 6px; }
|
||||||
|
::-webkit-scrollbar-track { background: transparent; }
|
||||||
|
::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 10px; }
|
||||||
|
::-webkit-scrollbar-thumb:hover { background: var(--text-secondary); }
|
||||||
|
|
||||||
|
/* Toasts */
|
||||||
|
#toast-container { position: fixed; bottom: 2rem; right: 2rem; display: flex; flex-direction: column; gap: 0.5rem; z-index: 1000; }
|
||||||
|
.toast { background: var(--bg-navbar); color: white; padding: 1rem 1.5rem; border-radius: 10px; border-left: 4px solid var(--accent-purple); box-shadow: 0 10px 15px rgba(0,0,0,0.5); animation: slideIn 0.3s ease-out; }
|
||||||
|
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
|
||||||
@@ -2,6 +2,8 @@ import { Module } from '@nestjs/common';
|
|||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { ScheduleModule } from '@nestjs/schedule';
|
import { ScheduleModule } from '@nestjs/schedule';
|
||||||
|
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||||
|
import { join } from 'path';
|
||||||
import { TelemetryModule } from './telemetry/telemetry.module';
|
import { TelemetryModule } from './telemetry/telemetry.module';
|
||||||
import { MapsModule } from './maps/maps.module';
|
import { MapsModule } from './maps/maps.module';
|
||||||
import { GeocodingModule } from './geocoding/geocoding.module';
|
import { GeocodingModule } from './geocoding/geocoding.module';
|
||||||
@@ -12,6 +14,10 @@ import { GeocodingModule } from './geocoding/geocoding.module';
|
|||||||
isGlobal: true,
|
isGlobal: true,
|
||||||
}),
|
}),
|
||||||
ScheduleModule.forRoot(),
|
ScheduleModule.forRoot(),
|
||||||
|
ServeStaticModule.forRoot({
|
||||||
|
rootPath: join(__dirname, '..', 'public', 'intel'),
|
||||||
|
serveRoot: '/api/intel',
|
||||||
|
}),
|
||||||
TypeOrmModule.forRootAsync({
|
TypeOrmModule.forRootAsync({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ export class RoadSegmentStat {
|
|||||||
@Column('float', { default: 1.0 })
|
@Column('float', { default: 1.0 })
|
||||||
congestionFactor: number; // 1.0 = Free flow, >1.0 = Congested
|
congestionFactor: number; // 1.0 = Free flow, >1.0 = Congested
|
||||||
|
|
||||||
|
@Column('boolean', { default: false })
|
||||||
|
@Index()
|
||||||
|
isClosed: boolean;
|
||||||
|
|
||||||
@Column('int', { default: 0 })
|
@Column('int', { default: 0 })
|
||||||
sampleCount: number;
|
sampleCount: number;
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_map/flutter_map.dart';
|
|
||||||
import 'package:latlong2/latlong.dart';
|
|
||||||
|
|
||||||
class IntaleqMapWidget extends StatelessWidget {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return FlutterMap(
|
|
||||||
options: MapOptions(
|
|
||||||
initialCenter: LatLng(33.5138, 36.2765), // Damascus / دمشق
|
|
||||||
initialZoom: 13.0,
|
|
||||||
),
|
|
||||||
children: [
|
|
||||||
TileLayer(
|
|
||||||
// هذا هو الرابط الذي يجلب الخريطة من السيرفر الخاص بك!
|
|
||||||
urlTemplate: 'https://app.intaleqapp.com/martin/planet_osm_line/{z}/{x}/{y}.pbf',
|
|
||||||
userAgentPackageName: 'com.intaleq.app',
|
|
||||||
subdomains: const ['a', 'b', 'c'],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────
|
|
||||||
// Helper Class for API Interaction (Intelligence API)
|
|
||||||
// ─────────────────────────────────────────────────────────
|
|
||||||
class IntaleqApiService {
|
|
||||||
static const String baseUrl = 'https://app.intaleqapp.com/api';
|
|
||||||
static const String apiKey = 'intaleq_secret_2026';
|
|
||||||
|
|
||||||
Future<void> triggerDailyIntelligence() async {
|
|
||||||
// مثال لطلب معالجة البيانات من داخل تطبيق فلاتر (اختياري)
|
|
||||||
print('Triggering 3 AM process manually...');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────
|
|
||||||
// Intaleq Vector Map Widget (Professional Setup)
|
|
||||||
// Uses MapLibre GL for High Performance Vector Tiles (.pbf)
|
|
||||||
// ─────────────────────────────────────────────────────────
|
|
||||||
class IntaleqMapWidget extends StatefulWidget {
|
|
||||||
const IntaleqMapWidget({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<IntaleqMapWidget> createState() => _IntaleqMapWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _IntaleqMapWidgetState extends State<IntaleqMapWidget> {
|
|
||||||
MaplibreMapController? mapController;
|
|
||||||
|
|
||||||
void _onMapCreated(MaplibreMapController controller) {
|
|
||||||
mapController = controller;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Example: Downloading an offline region for caching
|
|
||||||
Future<void> _downloadOfflineRegion() async {
|
|
||||||
final bounds = LatLngBounds(
|
|
||||||
southwest: const LatLng(33.4, 36.1), // Southwest corner of Damascus
|
|
||||||
northeast: const LatLng(33.6, 36.4), // Northeast corner of Damascus
|
|
||||||
);
|
|
||||||
|
|
||||||
// Mapbox/MapLibre has built-in offline caching for vector tiles!
|
|
||||||
// It caches the vectors very efficiently.
|
|
||||||
await downloadOfflineRegion(
|
|
||||||
OfflineRegionDefinition(
|
|
||||||
bounds: bounds,
|
|
||||||
mapStyleUrl: 'https://app.intaleqapp.com/style-mobile.json', // Your custom style linking to .pbf
|
|
||||||
minZoom: 10.0,
|
|
||||||
maxZoom: 16.0,
|
|
||||||
),
|
|
||||||
metadata: {
|
|
||||||
'name': 'Damascus Region',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
print('✅ Offline region downloaded and cached successfully!');
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
body: MaplibreMap(
|
|
||||||
onMapCreated: _onMapCreated,
|
|
||||||
initialCameraPosition: const CameraPosition(
|
|
||||||
target: LatLng(33.5138, 36.2765), // Damascus / دمشق
|
|
||||||
zoom: 13.0,
|
|
||||||
),
|
|
||||||
// The style URL points to a JSON file on your server.
|
|
||||||
// This JSON defines colors, line widths, and the tile source (Martin .pbf URLs)
|
|
||||||
styleString: 'https://app.intaleqapp.com/style-mobile.json',
|
|
||||||
|
|
||||||
// Optimizations for smooth vector rendering
|
|
||||||
minMaxZoomPreference: const MinMaxZoomPreference(5, 18),
|
|
||||||
myLocationEnabled: true,
|
|
||||||
),
|
|
||||||
floatingActionButton: FloatingActionButton(
|
|
||||||
onPressed: _downloadOfflineRegion,
|
|
||||||
tooltip: 'Download Offline Map',
|
|
||||||
child: const Icon(Icons.download),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user