feat: Telegram OSINT map layer, Osiris intel ports, and maritime settings

Add Telegram OSINT with hourly incremental t.me scraping, metro geocoding
separate from news centroids, threat-intercept popup UI with inline media,
and HTML markers above alert boxes so pins stay clickable. Expose GFW_API_TOKEN
in onboarding and Settings Maritime; harden GFW/CCTV/geo fetchers. Port Osiris-
derived recon, SCM, entity graph, malware/cyber feeds, sanctions, and submarine
cable layers with tests and documentation.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
BigBodyCobain
2026-06-08 21:04:08 -06:00
parent b64b9e0962
commit af9b3d08cc
76 changed files with 5769 additions and 218 deletions
+22
View File
@@ -0,0 +1,22 @@
import type { SelectedEntity } from '@/types/dashboard';
const GRAPH_TYPES = new Set(['aircraft', 'vessel', 'company', 'person', 'ip', 'country']);
const SELECTION_TO_GRAPH: Record<string, string> = {
flight: 'aircraft',
private_flight: 'aircraft',
military_flight: 'aircraft',
private_jet: 'aircraft',
tracked_flight: 'aircraft',
ship: 'vessel',
};
export function mapEntityToGraphType(type: string): string | null {
const mapped = SELECTION_TO_GRAPH[type] || type;
return GRAPH_TYPES.has(mapped) ? mapped : null;
}
export function isEntityGraphEligible(entity: SelectedEntity | null | undefined): boolean {
if (!entity) return false;
return mapEntityToGraphType(entity.type) !== null;
}
+13
View File
@@ -67,6 +67,19 @@ export function getLiveDataBounds(): LiveDataBounds | null {
return _current;
}
/** Stable cache key for the active bbox-scoped fetch window (1° quantization,
* matching appendLiveDataBoundsParams / backend ETag). Returns null when
* world-scale fetching is active. */
export function liveDataBoundsKey(): string | null {
const b = _current;
if (!b) return null;
const s = Math.floor(b.south);
const w = Math.floor(b.west);
const n = Math.ceil(b.north);
const e = Math.ceil(b.east);
return `${s},${w},${n},${e}`;
}
/** Append `s/w/n/e` query params to a URL when bounds are set, otherwise
* return the URL unchanged. Centralised so all live-data callers stay in
* sync about quantization and the world-scale skip rule. */
+97
View File
@@ -0,0 +1,97 @@
/** Synthetic TeleGeography corridor overlays — not real cable routes. */
const SYNTHETIC_CABLE_NAMES = new Set([
'SEA-ME-WE Corridor',
'Trans-Atlantic North',
'Trans-Atlantic South',
'WACS / SAT-3 Corridor',
'EASSy / SEACOM',
'East Asia Corridor',
'Asia-Australia',
'Trans-Pacific',
'South Atlantic',
]);
type LngLat = [number, number];
function lonJumpDegrees(a: LngLat, b: LngLat): number {
const d = Math.abs(b[0] - a[0]);
return Math.min(d, 360 - d);
}
function iterParts(geometry: GeoJSON.Geometry): LngLat[][] {
if (geometry.type === 'LineString') {
return [geometry.coordinates as LngLat[]];
}
if (geometry.type === 'MultiLineString') {
return geometry.coordinates as LngLat[][];
}
return [];
}
/** Split a path when consecutive vertices jump across continents / dateline. */
function splitAtJumps(coords: LngLat[], maxJumpDeg = 90): LngLat[][] {
if (coords.length < 2) return coords.length ? [coords] : [];
const segments: LngLat[][] = [[coords[0]]];
for (let i = 1; i < coords.length; i += 1) {
const prev = segments[segments.length - 1][segments[segments.length - 1].length - 1];
const next = coords[i];
if (lonJumpDegrees(prev, next) > maxJumpDeg) {
segments.push([next]);
} else {
segments[segments.length - 1].push(next);
}
}
return segments.filter((seg) => seg.length >= 2);
}
function partsToGeometry(parts: LngLat[][]): GeoJSON.LineString | GeoJSON.MultiLineString | null {
if (!parts.length) return null;
if (parts.length === 1) {
return { type: 'LineString', coordinates: parts[0] };
}
return { type: 'MultiLineString', coordinates: parts };
}
/**
* Drop synthetic corridor junk and split lines that cut across the dateline.
* Land-crossing segments are stripped at build time (see scripts/sanitize_submarine_cables.py).
*/
export function sanitizeSubmarineCables(
collection: GeoJSON.FeatureCollection,
): GeoJSON.FeatureCollection {
const byName = new Map<string, GeoJSON.Feature>();
for (const feature of collection.features) {
const name = String(feature.properties?.name || '').trim();
if (!name || SYNTHETIC_CABLE_NAMES.has(name)) continue;
if (!feature.geometry || feature.geometry.type === 'GeometryCollection') continue;
const splitParts: LngLat[][] = [];
for (const part of iterParts(feature.geometry)) {
splitParts.push(...splitAtJumps(part));
}
const geometry = partsToGeometry(splitParts);
if (!geometry) continue;
const cleaned: GeoJSON.Feature = {
type: 'Feature',
properties: feature.properties ?? {},
geometry,
};
const existing = byName.get(name);
if (!existing) {
byName.set(name, cleaned);
continue;
}
const existingPts = iterParts(existing.geometry!).flat().length;
const newPts = splitParts.flat().length;
if (newPts > existingPts) byName.set(name, cleaned);
}
return {
type: 'FeatureCollection',
features: Array.from(byName.values()),
};
}
+6
View File
@@ -0,0 +1,6 @@
/** Proxy Telegram CDN media through the backend (host allowlist + range requests). */
export function buildTelegramMediaProxyUrl(rawUrl: string): string {
return rawUrl.startsWith('http')
? `/api/telegram/media?url=${encodeURIComponent(rawUrl)}`
: rawUrl;
}