feat: add Spanish CCTV feeds and fix image loading

- Add 5 native ingestors to cctv_pipeline.py: DGT (~1,917 cameras),
  Madrid (~357), Málaga (~134), Vigo (~59), Vitoria-Gasteiz (~17)
- Fix DGT DATEX2 parser to match actual XML schema (device elements,
  not CctvCameraRecord)
- Wire all new ingestors into the scheduler via data_fetcher.py
- Remove standalone spain_cctv.py by Alborz Nazari, replaced by native
  pipeline ingestors that integrate with the existing scheduler pattern
- Fix CCTV image loading for servers with Referer-based hotlink
  protection (referrerPolicy="no-referrer")
- Replace external via.placeholder.com fallbacks with inline SVG data
  URIs to avoid dependency on unreachable third-party service
- Surface source_agency attribution in CCTV panel UI for open data
  license compliance (CC BY / Spain Ley 37/2007)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Singular Failure
2026-03-21 15:10:43 +01:00
parent 42a800a683
commit 3a2d8ddd75
5 changed files with 339 additions and 288 deletions
+6 -4
View File
@@ -857,7 +857,7 @@ function NewsFeedInner({ data, selectedEntity, regionDossier, regionDossierLoadi
? new Date(selectedEntity.extra.last_updated + 'Z').toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, timeZoneName: 'short' }).toUpperCase() + ' — OPTIC INTERCEPT'
: 'OPTIC INTERCEPT'}
</h2>
<span className="text-[10px] text-[var(--text-muted)] font-mono">ID: {selectedEntity.id}</span>
<span className="text-[10px] text-[var(--text-muted)] font-mono">ID: {selectedEntity.id}{selectedEntity.extra?.source_agency ? ` | ${selectedEntity.extra.source_agency}` : ''}</span>
</div>
<div className="relative w-full h-48 bg-black flex items-center justify-center p-1">
{(() => {
@@ -898,22 +898,24 @@ function NewsFeedInner({ data, selectedEntity, regionDossier, regionDossierLoadi
<img
src={url}
alt="MJPEG Feed"
referrerPolicy="no-referrer"
className="w-full h-full object-cover border border-cyan-900/50 rounded-sm filter contrast-125 saturate-50"
onError={(e) => {
const target = e.target as HTMLImageElement;
target.src = "https://via.placeholder.com/400x300.png?text=FEED+UNAVAILABLE";
target.src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='300'%3E%3Crect fill='%23111' width='400' height='300'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' fill='%2306b6d4' font-family='monospace' font-size='14'%3EFEED UNAVAILABLE%3C/text%3E%3C/svg%3E";
}}
/>
);
// satellite / image — standard img with referrer policy for external tiles
// satellite / image
return (
<img
src={url}
alt="CCTV Feed"
referrerPolicy="no-referrer"
className="w-full h-full object-cover border border-cyan-900/50 rounded-sm filter contrast-125 saturate-50"
onError={(e) => {
const target = e.target as HTMLImageElement;
target.src = "https://via.placeholder.com/400x300.png?text=NO+SIGNAL";
target.src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='300'%3E%3Crect fill='%23111' width='400' height='300'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' fill='%2306b6d4' font-family='monospace' font-size='14'%3ENO SIGNAL%3C/text%3E%3C/svg%3E";
}}
/>
);