2026-04-16-2
This commit is contained in:
@@ -40,4 +40,13 @@ export class MapCandidate extends BasePlace {
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
metadata: any; // For photos, extra contact info, etc.
|
||||
|
||||
@Column({ nullable: true })
|
||||
verified_name: string;
|
||||
|
||||
@Column({ type: 'int', default: 0 })
|
||||
confidence_score: number;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
verification_metadata: any;
|
||||
}
|
||||
|
||||
@@ -77,8 +77,13 @@ export class MapRefinementService {
|
||||
});
|
||||
|
||||
const saved = await this.candidateRepository.save(candidate);
|
||||
this.logger.log(`✅ Success: Candidate ${saved.id} enriched with Gov:${saved.governorate_id}, Neigh:${saved.neighborhood_id}`);
|
||||
this.logger.log(`✅ Success: Candidate ${saved.id} enriched with Gov:${saved.governorate_id}, Neigh:${spatialData.neigh_id}`);
|
||||
|
||||
// Trigger automated verification in background (don't block the response)
|
||||
this.runExternalVerification(saved.id).catch(err =>
|
||||
this.logger.error(`❌ Verification failed for ${saved.id}: ${err.message}`)
|
||||
);
|
||||
|
||||
// Return with enriched names for immediate feedback
|
||||
return {
|
||||
...saved,
|
||||
@@ -89,6 +94,67 @@ export class MapRefinementService {
|
||||
} as any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Automated verification using Search Engine Metadata Scraping
|
||||
*/
|
||||
async runExternalVerification(candidateId: number): Promise<void> {
|
||||
const candidate = await this.candidateRepository.findOne({ where: { id: candidateId } });
|
||||
if (!candidate) return;
|
||||
|
||||
this.logger.log(`🔍 Starting automated verification for candidate ${candidate.id}...`);
|
||||
|
||||
try {
|
||||
const axios = require('axios');
|
||||
// Using DuckDuckGo HTML search for coordinates - very stable for scraping names
|
||||
const searchUrl = `https://duckduckgo.com/html/?q=${candidate.latitude},${candidate.longitude}`;
|
||||
|
||||
const response = await axios.get(searchUrl, {
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
||||
},
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
const html = response.data;
|
||||
const titleMatch = html.match(/<title>([^<]+)<\/title>/);
|
||||
|
||||
if (titleMatch) {
|
||||
let officialName = titleMatch[1].replace(' at DuckDuckGo', '').replace(' - DuckDuckGo', '').trim();
|
||||
|
||||
// If the name is just coordinates, it means no POI was specifically identified
|
||||
const isCoordinates = /^[-+]?([1-8]?\d(\.\d+)?|90(\.0+)?),\s*[-+]?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?)$/.test(officialName);
|
||||
|
||||
if (!isCoordinates && officialName.length > 5) {
|
||||
candidate.verified_name = officialName;
|
||||
|
||||
// Basic confidence: if the user's name is partially in the official name, high trust
|
||||
const cleanUser = candidate.name.toLowerCase().trim();
|
||||
const cleanOfficial = officialName.toLowerCase();
|
||||
|
||||
if (cleanOfficial.includes(cleanUser) || cleanUser.includes(cleanOfficial)) {
|
||||
candidate.confidence_score = 90;
|
||||
} else {
|
||||
candidate.confidence_score = 60; // Found a name, but didn't match perfectly
|
||||
}
|
||||
|
||||
this.logger.log(`🎯 Verified: Found "${officialName}" near suggestion. Trust: ${candidate.confidence_score}%`);
|
||||
} else {
|
||||
candidate.confidence_score = 10; // Only coordinates found
|
||||
this.logger.log(`⚠️ No specific POI name found for coordinates ${candidate.latitude},${candidate.longitude}`);
|
||||
}
|
||||
}
|
||||
|
||||
candidate.verification_metadata = {
|
||||
last_check: new Date().toISOString(),
|
||||
source: 'DDG_SCRAPE'
|
||||
};
|
||||
|
||||
await this.candidateRepository.save(candidate);
|
||||
} catch (error) {
|
||||
this.logger.error(`Automated verification error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getCandidates(status?: CandidateStatus): Promise<any[]> {
|
||||
const query = `
|
||||
SELECT
|
||||
|
||||
@@ -10,8 +10,12 @@ async function bootstrap() {
|
||||
logger: ['error', 'warn', 'log', 'debug', 'verbose'],
|
||||
});
|
||||
|
||||
// 1. Modern Security Headers (Prevention of XSS, Clickjacking, etc.)
|
||||
app.enableCors();
|
||||
// 1. Modern Security Headers & Permissive CORS for Production Dashboard
|
||||
app.enableCors({
|
||||
origin: true, // Reflect request origin
|
||||
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
|
||||
credentials: true,
|
||||
});
|
||||
|
||||
// 3. Global API prefix
|
||||
app.setGlobalPrefix('api');
|
||||
|
||||
@@ -435,9 +435,8 @@
|
||||
<thead>
|
||||
<tr class="text-xs uppercase tracking-widest text-slate-500 bg-slate-900/40">
|
||||
<th class="px-8 py-5 font-black">Location Name</th>
|
||||
<th class="px-8 py-5 font-black">Category</th>
|
||||
<th class="px-8 py-5 font-black">Coordinates</th>
|
||||
<th class="px-8 py-5 font-black">Submitter</th>
|
||||
<th class="px-8 py-5 font-black">Automated Match</th>
|
||||
<th class="px-8 py-5 font-black">Spatial Context</th>
|
||||
<th class="px-8 py-5 font-black text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -66,34 +66,60 @@ const refinement = {
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = refinement.state.candidates.map(c => `
|
||||
<tr class="hover:bg-white/[0.02] transition-colors border-b border-white/[0.03]">
|
||||
<td class="px-8 py-6">
|
||||
<div class="font-bold text-white">${c.name_ar || c.name}</div>
|
||||
<div class="text-[10px] text-blue-400 font-extrabold uppercase mt-1">ID: ${c.id}</div>
|
||||
</td>
|
||||
<td class="px-8 py-6">
|
||||
<span class="px-2 py-1 rounded-lg bg-blue-500/10 text-blue-400 text-[10px] font-black uppercase tracking-wider">${c.category || 'General'}</span>
|
||||
</td>
|
||||
<td class="px-8 py-6">
|
||||
<div class="text-sm text-slate-300 font-medium">${c.governorate_name || 'Unknown Region'}</div>
|
||||
<div class="text-[11px] text-slate-500 mt-1">${c.neighborhood_name || 'Unknown Neighborhood'}</div>
|
||||
</td>
|
||||
<td class="px-8 py-6 text-right space-x-2">
|
||||
<button onclick="refinement.showMapModal('${c.latitude}', '${c.longitude}', '${c.name_ar || c.name}')" class="p-2.5 rounded-xl bg-blue-500/10 text-blue-400 hover:bg-blue-500/20 transition-all" title="View on Google Maps">
|
||||
<i data-lucide="eye" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<button onclick="refinement.reject('${c.id}')" class="p-2.5 rounded-xl bg-red-500/10 text-red-400 hover:bg-red-500/20 transition-all" title="Reject">
|
||||
<i data-lucide="x" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<button onclick="refinement.approve('${c.id}')" class="p-2.5 rounded-xl bg-emerald-500/10 text-emerald-400 hover:bg-emerald-500/20 transition-all border border-emerald-500/20 shadow-lg shadow-emerald-500/10" title="Approve">
|
||||
<i data-lucide="check" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
try {
|
||||
tbody.innerHTML = refinement.state.candidates.map(c => {
|
||||
const trustColor = c.confidence_score >= 80 ? 'emerald' : (c.confidence_score >= 50 ? 'amber' : 'slate');
|
||||
const trustText = c.confidence_score >= 80 ? 'High Trust' : (c.confidence_score >= 50 ? 'Partial Match' : 'Unverified');
|
||||
|
||||
// Safety checks for coordinates
|
||||
const lat = typeof c.latitude === 'number' ? c.latitude.toFixed(4) : 'N/A';
|
||||
const lng = typeof c.longitude === 'number' ? c.longitude.toFixed(4) : 'N/A';
|
||||
|
||||
lucide.createIcons();
|
||||
return `
|
||||
<tr class="border-b border-white/5 hover:bg-white/[0.02] transition-colors group">
|
||||
<td class="px-8 py-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-xl bg-${trustColor}-500/10 flex items-center justify-center text-${trustColor}-400">
|
||||
<i data-lucide="${c.confidence_score >= 50 ? 'shield-check' : 'shield-alert'}" class="w-5 h-5"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold text-white">${c.name_ar || c.name || 'Untitled'}</div>
|
||||
<div class="text-xs text-slate-500 mt-1">${c.category || 'Location'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-8 py-6">
|
||||
<div class="text-sm text-slate-300 font-medium">${c.verified_name || '<span class="text-slate-600 italic">No external match</span>'}</div>
|
||||
<div class="flex items-center gap-2 mt-1.5">
|
||||
<span class="px-2 py-0.5 rounded-full bg-${trustColor}-500/10 text-${trustColor}-400 text-[10px] font-bold uppercase tracking-wider border border-${trustColor}-500/20">
|
||||
${trustText} (${c.confidence_score || 0}%)
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-8 py-6">
|
||||
<div class="text-sm text-slate-300 font-mono">${lat}, ${lng}</div>
|
||||
<div class="text-[11px] text-slate-500 mt-1">${c.neighborhood_name || 'Unknown Neighborhood'}</div>
|
||||
</td>
|
||||
<td class="px-8 py-6 text-right space-x-2">
|
||||
<button onclick="refinement.showMapModal('${c.latitude}', '${c.longitude}', '${(c.name_ar || c.name || "Location").replace(/'/g, "\\'")}')" class="p-2.5 rounded-xl bg-blue-500/10 text-blue-400 hover:bg-blue-500/20 transition-all" title="View on Google Maps">
|
||||
<i data-lucide="eye" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<button onclick="refinement.reject('${c.id}')" class="p-2.5 rounded-xl bg-red-500/10 text-red-400 hover:bg-red-500/20 transition-all" title="Reject">
|
||||
<i data-lucide="x" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<button onclick="refinement.approve('${c.id}')" class="p-2.5 rounded-xl bg-emerald-500/10 text-emerald-400 hover:bg-emerald-500/20 transition-all border border-emerald-500/20 shadow-lg shadow-emerald-500/10" title="Approve">
|
||||
<i data-lucide="check" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
lucide.createIcons();
|
||||
} catch (error) {
|
||||
console.error('❌ [PlaceAudit] Rendering error:', error);
|
||||
tbody.innerHTML = `<tr><td colspan="4" class="py-20 text-center text-red-500">Rendering Error: See console for details</td></tr>`;
|
||||
}
|
||||
},
|
||||
|
||||
showMapModal: (lat, lng, name) => {
|
||||
|
||||
Reference in New Issue
Block a user