Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8717f8f
feat: add demo page
SamanPandey-in Apr 28, 2026
e5ce5bd
fix: fixed all the build errors
SamanPandey-in Apr 28, 2026
8bfc089
Fix: lint errors in demo fixed
SamanPandey-in Apr 28, 2026
19dfbde
fix: lint errors and page load issues
SamanPandey-in Apr 28, 2026
f86a9aa
fix: demo page layout
SamanPandey-in Apr 28, 2026
2ee6cff
fix: added supabase dependency to news-intel
SamanPandey-in Apr 28, 2026
ab5b7fe
fix: all issues on page demo
SamanPandey-in Apr 28, 2026
75f94f4
fix: disruptions on demo page
SamanPandey-in Apr 28, 2026
e660bb6
Merge branch 'test' into demo
SamanPandey-in Apr 28, 2026
681ccae
Merge pull request #6 from SamanPandey-in/demo
SamanPandey-in Apr 28, 2026
a0c83fe
refactor(firestore): implement fire-and-forget to firestore for all t…
SamanPandey-in Apr 28, 2026
1f7e4a8
fix: changed agent settings and impact flow. Impact works till end
SamanPandey-in Apr 29, 2026
1106534
fix: monitor agent working, logs show working of pipeline. UI is stil…
SamanPandey-in Apr 30, 2026
194a8b8
feat: Human decision appearing properly, pipeline working till there.…
SamanPandey-in Apr 30, 2026
18c3838
fix: fixed protocol appling and pdf download. Demo works now
SamanPandey-in Apr 30, 2026
aecfad1
fix: cicd useeffect for themetoggle
SamanPandey-in Apr 30, 2026
ee0d462
imp: added supabase admin auth to inject
SamanPandey-in Apr 30, 2026
516e476
fix: Brevo email issues and errors fixed
SamanPandey-in Apr 30, 2026
1a2c8be
fix: lint of err to _err
SamanPandey-in Apr 30, 2026
29138c7
feat: added simulation trigger from GlobeScreen
SamanPandey-in Apr 30, 2026
88c5eb7
fix: lint errors and deployment errors
SamanPandey-in Apr 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,11 @@ VAPID_PUBLIC_KEY=
VAPID_PRIVATE_KEY=
NEXT_PUBLIC_VAPID_PUBLIC_KEY=

# ---- Email Digest (Resend) ----
RESEND_API_KEY=

# ---- Email Digest (Brevo) ----
BREVO_API_KEY=
BREVO_SENDER_EMAIL=
BREVO_SENDER_NAME=
DIGEST_EMAIL=

# ---- App URL (for email links) ----
Expand Down
245 changes: 245 additions & 0 deletions dashboard/app/api/demo/inject/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
import { NextResponse } from 'next/server';
import { randomUUID } from 'node:crypto';
import { db } from '../../../../lib/firebase-admin.js';
import { createClient } from '@supabase/supabase-js';

const _supabaseUrl = process.env.SUPABASE_URL || process.env.NEXT_PUBLIC_SUPABASE_URL || '';
const _supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY || '';
const supabaseAdmin = _supabaseUrl && _supabaseKey ? createClient(_supabaseUrl, _supabaseKey) : null;

async function writeDisruptionToSupabase(disruptionEvent) {
if (!supabaseAdmin) return;

const { error } = await supabaseAdmin.from('disruptions').upsert({
id: disruptionEvent.id,
trace_id: disruptionEvent.id,
type: disruptionEvent.type,
severity: disruptionEvent.severity,
location: disruptionEvent.location,
epicenter_lat: disruptionEvent.epicenterLat,
epicenter_lng: disruptionEvent.epicenterLng,
affected_zones: disruptionEvent.affectedZones || [],
confidence: disruptionEvent.confidence,
raw_description: disruptionEvent.rawDescription || disruptionEvent.description || '',
published: true,
resolved: false,
detected_at: disruptionEvent.detectedAt || new Date().toISOString(),
}, { onConflict: 'id' });
if (error) throw new Error(`Supabase disruption write failed: ${error.message}`);
}

const INJECT_TIMEOUT_MS = 15_000;
const EVENT_BUS_TIMEOUT_MS = 5_000;

const configuredDisruptionAgentUrl =
process.env.DISRUPTION_AGENT_URL ||
process.env.NEXT_PUBLIC_DISRUPTION_AGENT_URL ||
'';

const DISRUPTION_AGENT_URL =
configuredDisruptionAgentUrl || (process.env.NODE_ENV === 'development' ? 'http://localhost:3001' : '');

const SCENARIOS = {
pacific_storm: {
label: 'Super Typhoon Mawar',
type: 'WEATHER',
severity: 8,
location: 'Western Pacific Shipping Corridor',
epicenterLat: 28.2,
epicenterLng: 143.4,
confidence: 0.86,
affectedZones: ['Western Pacific', 'North Pacific Route'],
description:
'Super Typhoon approaching Western Pacific, Category 5. Maximum sustained winds 185 km/h. Direct path over major trans-Pacific shipping corridors. 12 vessels currently in projected storm path between Japan and Los Angeles. Port of Yokohama issuing storm warnings.',
},
suez_closure: {
label: 'Suez Canal Emergency',
type: 'GEOPOLITICAL',
severity: 9,
location: 'Suez Canal / Red Sea',
epicenterLat: 29.9668,
epicenterLng: 32.5498,
confidence: 0.88,
affectedZones: ['Red Sea', 'Gulf of Aden', 'Suez Canal'],
description:
'The Suez Canal Authority has announced an emergency closure. Houthi missile attacks on Red Sea vessels. Forty-three vessels held. $12B daily trade affected. Minimum 21-day closure expected. All Asia-Europe shipments via southern route ordered to divert via Cape of Good Hope.',
},
port_strike: {
label: 'Mumbai JNPT Strike',
type: 'STRIKE',
severity: 7,
location: 'North Sea Port Cluster',
epicenterLat: 51.9225,
epicenterLng: 4.4792,
confidence: 0.83,
affectedZones: ['Rotterdam', 'Hamburg', 'Antwerp'],
description:
'International Transport Workers Federation confirms indefinite strike action at Port of Rotterdam, Hamburg, and Antwerp. All container terminal operations suspended. 80+ vessels at anchor awaiting berth. Estimated 2-week minimum disruption to Europe-bound cargo.',
},
};

function isTimeoutError(error) {
const name = String(error?.name || '').toLowerCase();
const message = String(error?.message || '').toLowerCase();
return name.includes('timeout') || name.includes('abort') || message.includes('timeout') || message.includes('aborted');
}

async function parseUpstreamBody(response) {
const contentType = String(response.headers.get('content-type') || '').toLowerCase();
if (contentType.includes('application/json')) {
return response.json().catch(() => null);
}

const text = await response.text().catch(() => '');
if (!text) return null;
return { message: text.slice(0, 500) };
}

function buildSyntheticDisruption(scenarioMeta) {
return {
id: `disruption-${randomUUID()}`,
type: scenarioMeta.type,
severity: scenarioMeta.severity,
location: scenarioMeta.location,
epicenterLat: scenarioMeta.epicenterLat,
epicenterLng: scenarioMeta.epicenterLng,
confidence: scenarioMeta.confidence,
affectedZones: scenarioMeta.affectedZones,
rawDescription: scenarioMeta.description,
detectedAt: new Date().toISOString(),
source: 'dashboard-demo-fallback',
unverified: false,
corroboratingSources: 1,
};
}

async function publishSyntheticDisruption(disruptionEvent, traceId) {
const eventBusUrl = process.env.EVENT_BUS_URL || 'http://localhost:4000';
const response = await fetch(`${eventBusUrl}/publish`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
topic: 'disruption-events',
payload: {
agentId: 'monitor',
traceId,
timestamp: new Date().toISOString(),
payload: disruptionEvent,
},
}),
signal: AbortSignal.timeout(EVENT_BUS_TIMEOUT_MS),
});

if (!response.ok) {
const msg = await response.text().catch(() => '');
throw new Error(`Event bus publish failed [${response.status}]${msg ? `: ${msg}` : ''}`);
}
}

async function injectSyntheticDisruption(scenario, scenarioMeta, reason = 'upstream unavailable') {
const traceId = randomUUID();
const disruptionEvent = buildSyntheticDisruption(scenarioMeta);
const results = await Promise.allSettled([
db.collection('disruptions').doc(disruptionEvent.id).set(disruptionEvent, { merge: true }),
publishSyntheticDisruption(disruptionEvent, traceId),
writeDisruptionToSupabase(disruptionEvent),
]);

const persisted = results[0].status === 'fulfilled' || results[2].status === 'fulfilled';
const published = results[1].status === 'fulfilled';
if (results[2].status === 'rejected') {
console.warn('[InjectRoute] Supabase disruption write failed:', results[2].reason?.message);
}

if (!persisted && !published) {
const persistErr = results[0].reason?.message || 'persist failed';
const publishErr = results[1].reason?.message || 'publish failed';
throw new Error(`Synthetic injection failed: ${persistErr}; ${publishErr}`);
}

return NextResponse.json(
{
ok: true,
synthetic: true,
scenario,
label: scenarioMeta.label,
traceId,
disruptionId: disruptionEvent.id,
persisted,
published,
warning: reason,
},
{ status: 202 }
);
}

export async function POST(req) {
try {
const { scenario } = await req.json();
const scenarioKey = String(scenario || '').trim().toLowerCase();
const scenarioMeta = SCENARIOS[scenarioKey];

if (!scenarioMeta) {
return NextResponse.json(
{ error: `Unknown scenario. Available: ${Object.keys(SCENARIOS).join(', ')}` },
{ status: 400 }
);
}

if (!DISRUPTION_AGENT_URL) {
return injectSyntheticDisruption(
scenarioKey,
scenarioMeta,
'DISRUPTION_AGENT_URL is not configured; synthetic fallback used'
);
}

const headers = { 'Content-Type': 'application/json' };
if (process.env.INTERNAL_TOKEN) {
headers.Authorization = `Bearer ${process.env.INTERNAL_TOKEN}`;
}

try {
const upstream = await fetch(`${DISRUPTION_AGENT_URL}/events`, {
method: 'POST',
headers,
body: JSON.stringify({ description: scenarioMeta.description }),
signal: AbortSignal.timeout(INJECT_TIMEOUT_MS),
});

const result = await parseUpstreamBody(upstream);
if (!upstream.ok) {
return injectSyntheticDisruption(
scenarioKey,
scenarioMeta,
`Disruption agent returned ${upstream.status}; synthetic fallback used`
);
}

return NextResponse.json({
ok: true,
synthetic: false,
disruptionId: result?.data?.id || null,
traceId: result?.traceId || null,
scenario: scenarioKey,
label: scenarioMeta.label,
published: result?.published ?? false,
});
} catch (err) {
if (isTimeoutError(err)) {
return injectSyntheticDisruption(
scenarioKey,
scenarioMeta,
`Disruption agent timed out after ${Math.floor(INJECT_TIMEOUT_MS / 1000)} seconds; synthetic fallback used`
);
}
return injectSyntheticDisruption(
scenarioKey,
scenarioMeta,
`Disruption agent unavailable (${err.message}); synthetic fallback used`
);
}
} catch (err) {
return NextResponse.json({ error: err.message || 'Inject failed' }, { status: 500 });
}
}
Loading
Loading