| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import type { |
| ZonesResponse, |
| Zone, |
| IndicesResponse, |
| IndexData, |
| DailyHeat, |
| TriggersResponse, |
| Trigger, |
| BasisRiskResponse, |
| BasisRisk, |
| NotificationsResponse, |
| Notification, |
| PipelineRunsResponse, |
| PipelineRun, |
| PipelineStep, |
| PipelineStats, |
| EnrolledResponse, |
| CoverageResponse, |
| CoverageZone, |
| CalibrateResponse, |
| CalibrateZoneResult, |
| CalibrateParams, |
| } from './api' |
| import type { DisbursementsResponse, DisbursementRecord } from '../types' |
|
|
| |
| |
| |
| |
|
|
| const NOW = new Date('2026-04-13T08:42:00Z') |
|
|
| function isoDaysAgo(days: number, hour = 14, minute = 18): string { |
| const d = new Date(NOW.getTime() - days * 86_400_000) |
| d.setUTCHours(hour, minute, 0, 0) |
| return d.toISOString() |
| } |
|
|
| function dateOnly(days: number): string { |
| const d = new Date(NOW.getTime() - days * 86_400_000) |
| return d.toISOString().slice(0, 10) |
| } |
|
|
| |
| |
| function rng(seed: number) { |
| let t = seed |
| return () => { |
| t = (t + 0x6d2b79f5) | 0 |
| let r = Math.imul(t ^ (t >>> 15), 1 | t) |
| r = (r + Math.imul(r ^ (r >>> 7), 61 | r)) ^ r |
| return ((r ^ (r >>> 14)) >>> 0) / 4294967296 |
| } |
| } |
|
|
| |
| |
| |
| |
| |
|
|
| interface ZoneSeed { |
| zone_id: string |
| name: string |
| settlement_type: 'informal' | 'mixed' | 'formal' |
| heat_vulnerability: 'low' | 'medium' | 'high' | 'severe' |
| latitude: number |
| longitude: number |
| elevation_m: number |
| worker_population_est: number |
| outdoor_exposure_pct: number |
| |
| corrected_temp_c: number |
| current_wbgt_c: number |
| grid_temp_c: number |
| uhi_delta_c: number |
| |
| trigger_probability_7d: number |
| model_tier: 'ensemble' | 'full_model' | 'persistence' | 'climatology' |
| consecutive_hot_days: number |
| |
| annual_trigger_days: number |
| } |
|
|
| const ZONE_SEEDS: ZoneSeed[] = [ |
| |
| { |
| zone_id: 'DAR-MAB', name: 'Mabibo', settlement_type: 'informal', heat_vulnerability: 'severe', |
| latitude: -6.798, longitude: 39.232, elevation_m: 28, worker_population_est: 11_950, |
| outdoor_exposure_pct: 0.78, corrected_temp_c: 37.4, current_wbgt_c: 39.2, grid_temp_c: 34.9, |
| uhi_delta_c: 2.5, trigger_probability_7d: 0.92, model_tier: 'ensemble', |
| consecutive_hot_days: 6, annual_trigger_days: 74, |
| }, |
| { |
| zone_id: 'DAR-MNZ', name: 'Manzese', settlement_type: 'informal', heat_vulnerability: 'severe', |
| latitude: -6.79, longitude: 39.245, elevation_m: 34, worker_population_est: 14_820, |
| outdoor_exposure_pct: 0.74, corrected_temp_c: 37.1, current_wbgt_c: 39.0, grid_temp_c: 34.7, |
| uhi_delta_c: 2.4, trigger_probability_7d: 0.88, model_tier: 'ensemble', |
| consecutive_hot_days: 5, annual_trigger_days: 71, |
| }, |
| { |
| zone_id: 'DAR-TAN', name: 'Tandale', settlement_type: 'informal', heat_vulnerability: 'severe', |
| latitude: -6.785, longitude: 39.255, elevation_m: 32, worker_population_est: 12_640, |
| outdoor_exposure_pct: 0.76, corrected_temp_c: 37.2, current_wbgt_c: 38.9, grid_temp_c: 34.6, |
| uhi_delta_c: 2.6, trigger_probability_7d: 0.85, model_tier: 'ensemble', |
| consecutive_hot_days: 5, annual_trigger_days: 68, |
| }, |
| { |
| zone_id: 'DAR-BUG', name: 'Buguruni', settlement_type: 'informal', heat_vulnerability: 'high', |
| latitude: -6.82, longitude: 39.26, elevation_m: 21, worker_population_est: 8_720, |
| outdoor_exposure_pct: 0.71, corrected_temp_c: 36.8, current_wbgt_c: 38.9, grid_temp_c: 34.3, |
| uhi_delta_c: 2.5, trigger_probability_7d: 0.81, model_tier: 'full_model', |
| consecutive_hot_days: 4, annual_trigger_days: 62, |
| }, |
| { |
| zone_id: 'DAR-VIN', name: 'Vingunguti', settlement_type: 'informal', heat_vulnerability: 'high', |
| latitude: -6.835, longitude: 39.25, elevation_m: 24, worker_population_est: 9_840, |
| outdoor_exposure_pct: 0.73, corrected_temp_c: 36.9, current_wbgt_c: 38.6, grid_temp_c: 34.4, |
| uhi_delta_c: 2.5, trigger_probability_7d: 0.77, model_tier: 'full_model', |
| consecutive_hot_days: 4, annual_trigger_days: 59, |
| }, |
| { |
| zone_id: 'DAR-JAN', name: 'Jangwani', settlement_type: 'informal', heat_vulnerability: 'severe', |
| latitude: -6.8, longitude: 39.27, elevation_m: 6, worker_population_est: 7_310, |
| outdoor_exposure_pct: 0.79, corrected_temp_c: 37.0, current_wbgt_c: 38.5, grid_temp_c: 34.1, |
| uhi_delta_c: 2.9, trigger_probability_7d: 0.74, model_tier: 'ensemble', |
| consecutive_hot_days: 3, annual_trigger_days: 83, |
| }, |
| { |
| zone_id: 'DAR-MBU', name: 'Mburahati', settlement_type: 'informal', heat_vulnerability: 'high', |
| latitude: -6.805, longitude: 39.25, elevation_m: 29, worker_population_est: 6_520, |
| outdoor_exposure_pct: 0.7, corrected_temp_c: 36.6, current_wbgt_c: 38.2, grid_temp_c: 34.1, |
| uhi_delta_c: 2.5, trigger_probability_7d: 0.68, model_tier: 'full_model', |
| consecutive_hot_days: 3, annual_trigger_days: 56, |
| }, |
|
|
| |
| { |
| zone_id: 'DAR-KIG', name: 'Kigogo', settlement_type: 'mixed', heat_vulnerability: 'medium', |
| latitude: -6.79, longitude: 39.265, elevation_m: 26, worker_population_est: 8_140, |
| outdoor_exposure_pct: 0.58, corrected_temp_c: 35.9, current_wbgt_c: 37.9, grid_temp_c: 34.0, |
| uhi_delta_c: 1.9, trigger_probability_7d: 0.54, model_tier: 'full_model', |
| consecutive_hot_days: 2, annual_trigger_days: 42, |
| }, |
| { |
| zone_id: 'DAR-MAG', name: 'Magomeni', settlement_type: 'mixed', heat_vulnerability: 'medium', |
| latitude: -6.8, longitude: 39.26, elevation_m: 18, worker_population_est: 6_980, |
| outdoor_exposure_pct: 0.55, corrected_temp_c: 35.6, current_wbgt_c: 37.6, grid_temp_c: 33.8, |
| uhi_delta_c: 1.8, trigger_probability_7d: 0.47, model_tier: 'full_model', |
| consecutive_hot_days: 2, annual_trigger_days: 38, |
| }, |
| { |
| zone_id: 'DAR-MWA', name: 'Mwananyamala', settlement_type: 'mixed', heat_vulnerability: 'medium', |
| latitude: -6.778, longitude: 39.255, elevation_m: 31, worker_population_est: 5_860, |
| outdoor_exposure_pct: 0.53, corrected_temp_c: 35.4, current_wbgt_c: 37.3, grid_temp_c: 33.7, |
| uhi_delta_c: 1.7, trigger_probability_7d: 0.41, model_tier: 'persistence', |
| consecutive_hot_days: 1, annual_trigger_days: 34, |
| }, |
| { |
| zone_id: 'DAR-MSA', name: 'Msasani', settlement_type: 'mixed', heat_vulnerability: 'medium', |
| latitude: -6.76, longitude: 39.265, elevation_m: 14, worker_population_est: 4_910, |
| outdoor_exposure_pct: 0.49, corrected_temp_c: 34.9, current_wbgt_c: 36.8, grid_temp_c: 33.4, |
| uhi_delta_c: 1.5, trigger_probability_7d: 0.35, model_tier: 'persistence', |
| consecutive_hot_days: 1, annual_trigger_days: 27, |
| }, |
|
|
| |
| { |
| zone_id: 'DAR-KIN', name: 'Kinondoni', settlement_type: 'formal', heat_vulnerability: 'low', |
| latitude: -6.77, longitude: 39.24, elevation_m: 42, worker_population_est: 4_280, |
| outdoor_exposure_pct: 0.34, corrected_temp_c: 33.8, current_wbgt_c: 35.9, grid_temp_c: 33.1, |
| uhi_delta_c: 0.7, trigger_probability_7d: 0.18, model_tier: 'persistence', |
| consecutive_hot_days: 0, annual_trigger_days: 12, |
| }, |
| { |
| zone_id: 'DAR-SIN', name: 'Sinza', settlement_type: 'formal', heat_vulnerability: 'low', |
| latitude: -6.775, longitude: 39.225, elevation_m: 48, worker_population_est: 3_760, |
| outdoor_exposure_pct: 0.31, corrected_temp_c: 33.5, current_wbgt_c: 35.6, grid_temp_c: 32.9, |
| uhi_delta_c: 0.6, trigger_probability_7d: 0.14, model_tier: 'climatology', |
| consecutive_hot_days: 0, annual_trigger_days: 9, |
| }, |
| { |
| zone_id: 'DAR-KAW', name: 'Kawe', settlement_type: 'formal', heat_vulnerability: 'low', |
| latitude: -6.755, longitude: 39.235, elevation_m: 12, worker_population_est: 3_240, |
| outdoor_exposure_pct: 0.29, corrected_temp_c: 33.1, current_wbgt_c: 35.2, grid_temp_c: 32.6, |
| uhi_delta_c: 0.5, trigger_probability_7d: 0.11, model_tier: 'climatology', |
| consecutive_hot_days: 0, annual_trigger_days: 6, |
| }, |
| { |
| zone_id: 'DAR-MIK', name: 'Mikocheni', settlement_type: 'formal', heat_vulnerability: 'low', |
| latitude: -6.765, longitude: 39.27, elevation_m: 8, worker_population_est: 2_178, |
| outdoor_exposure_pct: 0.26, corrected_temp_c: 32.7, current_wbgt_c: 34.8, grid_temp_c: 32.3, |
| uhi_delta_c: 0.4, trigger_probability_7d: 0.08, model_tier: 'climatology', |
| consecutive_hot_days: 0, annual_trigger_days: 4, |
| }, |
| ] |
|
|
| function riskLevelFor(wbgt: number): string { |
| if (wbgt > 38.8) return 'critical' |
| if (wbgt > 37.5) return 'high' |
| if (wbgt > 36) return 'moderate' |
| return 'low' |
| } |
|
|
| |
|
|
| const TOTAL_WORKERS = ZONE_SEEDS.reduce((s, z) => s + z.worker_population_est, 0) |
|
|
| const ZONES: Zone[] = ZONE_SEEDS.map((z) => ({ |
| zone_id: z.zone_id, |
| name: z.name, |
| city: 'Dar es Salaam', |
| country: 'Tanzania', |
| latitude: z.latitude, |
| longitude: z.longitude, |
| elevation_m: z.elevation_m, |
| settlement_type: z.settlement_type, |
| worker_population_est: z.worker_population_est, |
| outdoor_exposure_pct: z.outdoor_exposure_pct, |
| heat_vulnerability: z.heat_vulnerability, |
| risk_level: riskLevelFor(z.current_wbgt_c), |
| corrected_temp_c: z.corrected_temp_c, |
| current_wbgt_c: z.current_wbgt_c, |
| current_heat_index_c: z.corrected_temp_c + 2.1, |
| max_temp_c: z.corrected_temp_c + 1.4, |
| max_wbgt_c: z.current_wbgt_c + 0.8, |
| consecutive_hot_days: z.consecutive_hot_days, |
| trigger_probability_7d: z.trigger_probability_7d, |
| prediction_confidence: 0.72 + (z.model_tier === 'ensemble' ? 0.18 : z.model_tier === 'full_model' ? 0.12 : 0.04), |
| model_tier: z.model_tier, |
| })) |
|
|
| export const ZONES_RESPONSE: ZonesResponse = { |
| zones: ZONES, |
| total: ZONES.length, |
| cities: ['Dar es Salaam'], |
| } |
|
|
| |
|
|
| function buildDailyHistory(seed: ZoneSeed): DailyHeat[] { |
| const rand = rng(seed.zone_id.split('').reduce((a, c) => a + c.charCodeAt(0), 0)) |
| const baseTemp = seed.corrected_temp_c - 1.2 |
| const baseWbgt = seed.current_wbgt_c - 1.0 |
| const out: DailyHeat[] = [] |
| for (let i = 89; i >= 0; i--) { |
| |
| const seasonal = Math.sin(((89 - i) / 89) * Math.PI * 0.9) * 1.4 |
| const noise = (rand() - 0.5) * 1.4 |
| const temp = baseTemp + seasonal + noise |
| const wbgt = baseWbgt + seasonal * 0.9 + noise * 0.7 |
| const gridTemp = temp - seed.uhi_delta_c - (rand() - 0.5) * 0.3 |
| out.push({ |
| date: dateOnly(i), |
| temp_c: Math.round(temp * 10) / 10, |
| grid_temp_c: Math.round(gridTemp * 10) / 10, |
| wbgt_c: Math.round(wbgt * 10) / 10, |
| }) |
| } |
| return out |
| } |
|
|
| const INDICES: IndexData[] = ZONE_SEEDS.map((z) => ({ |
| zone_id: z.zone_id, |
| zone_name: z.name, |
| city: 'Dar es Salaam', |
| temp_current: z.corrected_temp_c, |
| wbgt_current: z.current_wbgt_c, |
| risk_level: riskLevelFor(z.current_wbgt_c), |
| daily_history: buildDailyHistory(z), |
| })) |
|
|
| export const INDICES_RESPONSE: IndicesResponse = { |
| indices: INDICES, |
| total: INDICES.length, |
| } |
|
|
| |
| |
|
|
| interface TriggerSeed { |
| zone_id: string |
| daysAgo: number |
| level: 'critical' | 'high' | 'moderate' |
| max_temp_c: number |
| max_wbgt_c: number |
| consecutive_days: number |
| payout_per_worker_usd: number |
| } |
|
|
| const TRIGGER_SEEDS: TriggerSeed[] = [ |
| { zone_id: 'DAR-MAB', daysAgo: 0, level: 'critical', max_temp_c: 38.1, max_wbgt_c: 39.6, consecutive_days: 6, payout_per_worker_usd: 12 }, |
| { zone_id: 'DAR-MNZ', daysAgo: 0, level: 'critical', max_temp_c: 37.8, max_wbgt_c: 39.4, consecutive_days: 5, payout_per_worker_usd: 12 }, |
| { zone_id: 'DAR-TAN', daysAgo: 1, level: 'critical', max_temp_c: 37.6, max_wbgt_c: 39.2, consecutive_days: 5, payout_per_worker_usd: 12 }, |
| { zone_id: 'DAR-BUG', daysAgo: 1, level: 'high', max_temp_c: 37.3, max_wbgt_c: 39.0, consecutive_days: 4, payout_per_worker_usd: 12 }, |
| { zone_id: 'DAR-VIN', daysAgo: 2, level: 'high', max_temp_c: 37.4, max_wbgt_c: 38.8, consecutive_days: 4, payout_per_worker_usd: 12 }, |
| { zone_id: 'DAR-JAN', daysAgo: 3, level: 'critical', max_temp_c: 37.5, max_wbgt_c: 38.7, consecutive_days: 3, payout_per_worker_usd: 12 }, |
| { zone_id: 'DAR-MBU', daysAgo: 4, level: 'high', max_temp_c: 36.9, max_wbgt_c: 38.4, consecutive_days: 3, payout_per_worker_usd: 12 }, |
| { zone_id: 'DAR-MAB', daysAgo: 11, level: 'critical', max_temp_c: 37.9, max_wbgt_c: 39.1, consecutive_days: 4, payout_per_worker_usd: 12 }, |
| { zone_id: 'DAR-MNZ', daysAgo: 12, level: 'critical', max_temp_c: 37.6, max_wbgt_c: 38.9, consecutive_days: 3, payout_per_worker_usd: 12 }, |
| { zone_id: 'DAR-BUG', daysAgo: 18, level: 'high', max_temp_c: 36.8, max_wbgt_c: 38.7, consecutive_days: 3, payout_per_worker_usd: 12 }, |
| { zone_id: 'DAR-KIG', daysAgo: 22, level: 'moderate', max_temp_c: 36.2, max_wbgt_c: 38.2, consecutive_days: 2, payout_per_worker_usd: 8 }, |
| { zone_id: 'DAR-TAN', daysAgo: 27, level: 'critical', max_temp_c: 37.7, max_wbgt_c: 39.0, consecutive_days: 4, payout_per_worker_usd: 12 }, |
| { zone_id: 'DAR-MAG', daysAgo: 31, level: 'moderate', max_temp_c: 35.9, max_wbgt_c: 37.9, consecutive_days: 2, payout_per_worker_usd: 8 }, |
| { zone_id: 'DAR-VIN', daysAgo: 38, level: 'high', max_temp_c: 37.1, max_wbgt_c: 38.6, consecutive_days: 3, payout_per_worker_usd: 12 }, |
| { zone_id: 'DAR-MAB', daysAgo: 44, level: 'critical', max_temp_c: 37.8, max_wbgt_c: 39.2, consecutive_days: 4, payout_per_worker_usd: 12 }, |
| { zone_id: 'DAR-MWA', daysAgo: 47, level: 'moderate', max_temp_c: 35.6, max_wbgt_c: 37.7, consecutive_days: 2, payout_per_worker_usd: 8 }, |
| { zone_id: 'DAR-MNZ', daysAgo: 52, level: 'critical', max_temp_c: 37.4, max_wbgt_c: 39.0, consecutive_days: 3, payout_per_worker_usd: 12 }, |
| { zone_id: 'DAR-BUG', daysAgo: 58, level: 'high', max_temp_c: 36.9, max_wbgt_c: 38.8, consecutive_days: 3, payout_per_worker_usd: 12 }, |
| { zone_id: 'DAR-JAN', daysAgo: 64, level: 'critical', max_temp_c: 37.2, max_wbgt_c: 38.9, consecutive_days: 3, payout_per_worker_usd: 12 }, |
| { zone_id: 'DAR-MBU', daysAgo: 71, level: 'high', max_temp_c: 36.7, max_wbgt_c: 38.5, consecutive_days: 2, payout_per_worker_usd: 12 }, |
| { zone_id: 'DAR-MAB', daysAgo: 78, level: 'critical', max_temp_c: 37.6, max_wbgt_c: 39.1, consecutive_days: 3, payout_per_worker_usd: 12 }, |
| { zone_id: 'DAR-TAN', daysAgo: 84, level: 'high', max_temp_c: 36.9, max_wbgt_c: 38.7, consecutive_days: 2, payout_per_worker_usd: 12 }, |
| { zone_id: 'DAR-MSA', daysAgo: 89, level: 'moderate', max_temp_c: 35.2, max_wbgt_c: 37.4, consecutive_days: 2, payout_per_worker_usd: 8 }, |
| { zone_id: 'DAR-MNZ', daysAgo: 96, level: 'critical', max_temp_c: 37.5, max_wbgt_c: 39.0, consecutive_days: 4, payout_per_worker_usd: 12 }, |
| { zone_id: 'DAR-VIN', daysAgo: 102, level: 'high', max_temp_c: 37.0, max_wbgt_c: 38.6, consecutive_days: 3, payout_per_worker_usd: 12 }, |
| { zone_id: 'DAR-MAB', daysAgo: 112, level: 'critical', max_temp_c: 37.7, max_wbgt_c: 39.2, consecutive_days: 4, payout_per_worker_usd: 12 }, |
| { zone_id: 'DAR-BUG', daysAgo: 118, level: 'high', max_temp_c: 36.8, max_wbgt_c: 38.7, consecutive_days: 3, payout_per_worker_usd: 12 }, |
| { zone_id: 'DAR-JAN', daysAgo: 126, level: 'critical', max_temp_c: 37.3, max_wbgt_c: 38.9, consecutive_days: 3, payout_per_worker_usd: 12 }, |
| ] |
|
|
| const ZONE_BY_ID: Record<string, (typeof ZONE_SEEDS)[number]> = Object.fromEntries( |
| ZONE_SEEDS.map((z) => [z.zone_id, z]) |
| ) |
|
|
| |
| |
| |
| |
| const TRIGGERS: Trigger[] = TRIGGER_SEEDS.map((t) => { |
| const z = ZONE_BY_ID[t.zone_id] |
| return { |
| zone_id: t.zone_id, |
| zone_name: z.name, |
| city: 'Dar es Salaam', |
| trigger_level: t.level, |
| triggered_at: isoDaysAgo(t.daysAgo, 9, 0), |
| heat_risk_score: Math.round((t.max_wbgt_c - 32) * 18), |
| max_temp_c: t.max_temp_c, |
| max_wbgt_c: t.max_wbgt_c, |
| consecutive_days: t.consecutive_days, |
| settlement_type: z.settlement_type, |
| payout_per_worker_usd: t.payout_per_worker_usd, |
| } |
| }) |
|
|
| export const TRIGGERS_RESPONSE: TriggersResponse = { |
| triggers: TRIGGERS, |
| total: TRIGGERS.length, |
| } |
|
|
| |
| const PAYOUTS_THIS_SEASON = TRIGGER_SEEDS.reduce( |
| (s, t) => s + Math.round(ZONE_BY_ID[t.zone_id].worker_population_est * 0.35), |
| 0 |
| ) |
|
|
| |
| |
| |
|
|
| const BASIS_RISK: BasisRisk[] = ZONE_SEEDS.map((z) => { |
| |
| |
| |
| const fpr = z.settlement_type === 'formal' ? 0.12 : z.settlement_type === 'mixed' ? 0.09 : 0.07 |
| const fnr = z.settlement_type === 'informal' ? 0.11 : z.settlement_type === 'mixed' ? 0.08 : 0.05 |
| const correlation = z.settlement_type === 'informal' ? 0.78 : z.settlement_type === 'mixed' ? 0.71 : 0.64 |
| const mae = Math.round((1 - correlation) * 100) / 100 |
| return { |
| zone_id: z.zone_id, |
| zone_name: z.name, |
| city: 'Dar es Salaam', |
| settlement_type: z.settlement_type, |
| false_positive_rate: fpr, |
| false_negative_rate: fnr, |
| correlation, |
| mae, |
| accuracy_by_tier: { |
| persistence: 0.62, |
| full_model: 0.74, |
| ensemble: 0.82, |
| }, |
| recommendations: null, |
| } |
| }) |
|
|
| export const BASIS_RISK_RESPONSE: BasisRiskResponse = { |
| basis_risk: BASIS_RISK, |
| total: BASIS_RISK.length, |
| } |
|
|
| |
|
|
| |
| |
| |
| const NOTIFICATIONS: Notification[] = [ |
| { id: 'msg_3201', zone_id: 'DAR-MAB', zone_name: 'Mabibo', city: 'Dar es Salaam', trigger_level: 'critical', |
| channel: 'SMS', language: 'sw', recipient_count: 4182, status: 'delivered', |
| message_preview: 'Tahadhari: joto kali Mabibo. Payout TSh 30,000 imetumwa kwa M-Pesa.', |
| delivered_at: isoDaysAgo(0, 9, 12) }, |
| { id: 'msg_3202', zone_id: 'DAR-MNZ', zone_name: 'Manzese', city: 'Dar es Salaam', trigger_level: 'critical', |
| channel: 'SMS', language: 'sw', recipient_count: 5187, status: 'delivered', |
| message_preview: 'Tahadhari: joto kali Manzese. Payout TSh 30,000 imetumwa kwa M-Pesa.', |
| delivered_at: isoDaysAgo(0, 9, 14) }, |
| { id: 'msg_3203', zone_id: 'DAR-TAN', zone_name: 'Tandale', city: 'Dar es Salaam', trigger_level: 'critical', |
| channel: 'SMS', language: 'sw', recipient_count: 4424, status: 'delivered', |
| message_preview: 'Tahadhari: joto kali Tandale. Payout TSh 30,000 imetumwa kwa M-Pesa.', |
| delivered_at: isoDaysAgo(1, 9, 10) }, |
| { id: 'msg_3204', zone_id: 'DAR-BUG', zone_name: 'Buguruni', city: 'Dar es Salaam', trigger_level: 'high', |
| channel: 'SMS', language: 'sw', recipient_count: 3052, status: 'delivered', |
| message_preview: 'Onyo: joto juu Buguruni. Fuata kanuni za usalama. Payout TSh 30,000.', |
| delivered_at: isoDaysAgo(1, 9, 15) }, |
| { id: 'msg_3205', zone_id: 'DAR-VIN', zone_name: 'Vingunguti', city: 'Dar es Salaam', trigger_level: 'high', |
| channel: 'SMS', language: 'sw', recipient_count: 3444, status: 'delivered', |
| message_preview: 'Onyo: joto juu Vingunguti. Payout TSh 30,000 imetumwa kwa M-Pesa.', |
| delivered_at: isoDaysAgo(2, 9, 8) }, |
| { id: 'msg_3206', zone_id: 'DAR-JAN', zone_name: 'Jangwani', city: 'Dar es Salaam', trigger_level: 'critical', |
| channel: 'SMS', language: 'sw', recipient_count: 2559, status: 'delivered', |
| message_preview: 'Tahadhari: joto kali Jangwani. Payout TSh 30,000 imetumwa kwa M-Pesa.', |
| delivered_at: isoDaysAgo(3, 9, 22) }, |
| { id: 'msg_3207', zone_id: 'DAR-MBU', zone_name: 'Mburahati', city: 'Dar es Salaam', trigger_level: 'high', |
| channel: 'SMS', language: 'sw', recipient_count: 2282, status: 'delivered', |
| message_preview: 'Onyo: joto juu Mburahati. Pigeni simu 199 kwa usaidizi.', |
| delivered_at: isoDaysAgo(4, 9, 18) }, |
| ] |
|
|
| const NOTIFICATIONS_BY_LANGUAGE: Record<string, number> = NOTIFICATIONS.reduce( |
| (acc, n) => { |
| acc[n.language] = (acc[n.language] ?? 0) + 1 |
| return acc |
| }, |
| {} as Record<string, number> |
| ) |
|
|
| export const NOTIFICATIONS_RESPONSE: NotificationsResponse = { |
| notifications: NOTIFICATIONS, |
| by_language: NOTIFICATIONS_BY_LANGUAGE, |
| } |
|
|
| |
| |
| |
| |
|
|
| const CITY_P97_WBGT = 38.8 |
| const TZS_PER_USD = 2_500 |
| const PROVIDER_CYCLE = ['M-Pesa', 'Tigo Pesa', 'Airtel Money'] as const |
|
|
| const DISBURSEMENT_RECORDS: DisbursementRecord[] = ZONE_SEEDS.map((z, idx) => { |
| const triggers = z.current_wbgt_c > CITY_P97_WBGT |
| const enrolled = Math.round(z.worker_population_est * 0.35) |
| |
| |
| const payoutUsd = z.settlement_type === 'informal' ? 12 : z.settlement_type === 'mixed' ? 8 : 6 |
| const payoutTzs = payoutUsd * TZS_PER_USD |
| |
| const pastEvents = Math.round(z.annual_trigger_days / 8) |
| const pastTotalUsd = pastEvents * enrolled * payoutUsd * 0.92 |
| return { |
| zone_id: z.zone_id, |
| zone_name: z.name, |
| city: 'Dar es Salaam', |
| settlement_type: z.settlement_type, |
| heat_vulnerability: z.heat_vulnerability, |
| current_wbgt: z.current_wbgt_c, |
| threshold: CITY_P97_WBGT, |
| triggers_payout: triggers, |
| workers: enrolled, |
| payout_per_worker_usd: payoutUsd, |
| payout_per_worker_tzs: payoutTzs, |
| zone_total_usd: triggers ? enrolled * payoutUsd : 0, |
| zone_total_tzs: triggers ? enrolled * payoutTzs : 0, |
| provider: PROVIDER_CYCLE[idx % PROVIDER_CYCLE.length], |
| status: triggers ? 'pending' : 'no_trigger', |
| past_disbursements: pastEvents, |
| past_total_usd: Math.round(pastTotalUsd), |
| } |
| }) |
|
|
| const TRIGGERING_NOW = DISBURSEMENT_RECORDS.filter((d) => d.triggers_payout) |
|
|
| export const DISBURSEMENTS_RESPONSE: DisbursementsResponse = { |
| disbursements: DISBURSEMENT_RECORDS, |
| stats: { |
| total_usd: TRIGGERING_NOW.reduce((s, d) => s + d.zone_total_usd, 0), |
| total_tzs: TRIGGERING_NOW.reduce((s, d) => s + d.zone_total_tzs, 0), |
| total_workers_paid: TRIGGERING_NOW.reduce((s, d) => s + d.workers, 0), |
| zones_paying: TRIGGERING_NOW.length, |
| zones_total: DISBURSEMENT_RECORDS.length, |
| threshold_wbgt: CITY_P97_WBGT, |
| }, |
| } |
|
|
| |
|
|
| interface RunSeed { |
| daysAgo: number |
| status: 'success' | 'partial' | 'failed' |
| zones_processed: number |
| triggers_found: number |
| notifications_sent: number |
| total_cost_usd: number |
| duration_s: number |
| note?: string |
| } |
|
|
| const RUN_SEEDS: RunSeed[] = [ |
| { daysAgo: 0, status: 'success', zones_processed: 15, triggers_found: 7, notifications_sent: 25130, total_cost_usd: 0.1842, duration_s: 412 }, |
| { daysAgo: 1, status: 'success', zones_processed: 15, triggers_found: 4, notifications_sent: 15102, total_cost_usd: 0.1531, duration_s: 388 }, |
| { daysAgo: 2, status: 'success', zones_processed: 15, triggers_found: 3, notifications_sent: 12440, total_cost_usd: 0.1482, duration_s: 401 }, |
| { daysAgo: 3, status: 'success', zones_processed: 15, triggers_found: 2, notifications_sent: 9284, total_cost_usd: 0.1394, duration_s: 376 }, |
| { daysAgo: 4, status: 'partial', zones_processed: 15, triggers_found: 5, notifications_sent: 18421, total_cost_usd: 0.1612, duration_s: 442, |
| note: 'M-Pesa B2C rate-limited 312 transfers; retried successfully' }, |
| { daysAgo: 5, status: 'success', zones_processed: 15, triggers_found: 1, notifications_sent: 4182, total_cost_usd: 0.1328, duration_s: 369 }, |
| { daysAgo: 6, status: 'success', zones_processed: 15, triggers_found: 0, notifications_sent: 0, total_cost_usd: 0.1284, duration_s: 372 }, |
| { daysAgo: 7, status: 'success', zones_processed: 15, triggers_found: 2, notifications_sent: 8112, total_cost_usd: 0.1366, duration_s: 384 }, |
| { daysAgo: 8, status: 'success', zones_processed: 15, triggers_found: 1, notifications_sent: 3428, total_cost_usd: 0.1298, duration_s: 358 }, |
| { daysAgo: 9, status: 'success', zones_processed: 15, triggers_found: 2, notifications_sent: 7860, total_cost_usd: 0.1341, duration_s: 391 }, |
| { daysAgo: 10, status: 'failed', zones_processed: 4, triggers_found: 0, notifications_sent: 0, total_cost_usd: 0.0226, duration_s: 96, |
| note: 'NASA POWER timeout β retry scheduled' }, |
| { daysAgo: 11, status: 'success', zones_processed: 15, triggers_found: 3, notifications_sent: 11248, total_cost_usd: 0.1407, duration_s: 380 }, |
| { daysAgo: 12, status: 'success', zones_processed: 15, triggers_found: 1, notifications_sent: 3860, total_cost_usd: 0.1288, duration_s: 365 }, |
| { daysAgo: 13, status: 'success', zones_processed: 15, triggers_found: 2, notifications_sent: 6964, total_cost_usd: 0.1349, duration_s: 379 }, |
| { daysAgo: 14, status: 'success', zones_processed: 15, triggers_found: 0, notifications_sent: 0, total_cost_usd: 0.1271, duration_s: 371 }, |
| ] |
|
|
| function buildSteps(seed: RunSeed): PipelineStep[] { |
| const names: [string, number][] = [ |
| ['ingest', 48], ['heal', 72], ['index', 61], |
| ['calibrate', 94], ['explain', 82], ['notify', 51], |
| ] |
| return names.map(([step, share]) => { |
| |
| if (seed.status === 'failed' && step !== 'ingest') { |
| return { step, status: step === 'heal' ? 'failed' : 'skipped', duration_s: 0 } |
| } |
| const frac = share / names.reduce((s, [, v]) => s + v, 0) |
| const stepStatus = seed.status === 'partial' && step === 'notify' ? 'partial' : 'success' |
| return { step, status: stepStatus, duration_s: Math.round(seed.duration_s * frac * 10) / 10 } |
| }) |
| } |
|
|
| const PIPELINE_RUNS: PipelineRun[] = RUN_SEEDS.map((seed) => { |
| const startedAt = isoDaysAgo(seed.daysAgo, 2, 0) |
| const endedAt = isoDaysAgo(seed.daysAgo, 2, Math.floor(seed.duration_s / 60)) |
| return { |
| run_id: `run_${dateOnly(seed.daysAgo).replace(/-/g, '')}`, |
| started_at: startedAt, |
| ended_at: endedAt, |
| status: seed.status === 'success' ? 'success' : seed.status, |
| duration_s: seed.duration_s, |
| zones_processed: seed.zones_processed, |
| triggers_found: seed.triggers_found, |
| notifications_sent: seed.notifications_sent, |
| total_cost_usd: seed.total_cost_usd, |
| steps: buildSteps(seed), |
| } |
| }) |
|
|
| export const PIPELINE_RUNS_RESPONSE: PipelineRunsResponse = { |
| runs: PIPELINE_RUNS, |
| total: PIPELINE_RUNS.length, |
| } |
|
|
| const SUCCESSFUL_RUNS = RUN_SEEDS.filter((r) => r.status === 'success').length |
| const TOTAL_COST = Math.round(RUN_SEEDS.reduce((s, r) => s + r.total_cost_usd, 0) * 10000) / 10000 |
| const TOTAL_ENROLLED = ZONE_SEEDS.reduce((s, z) => s + Math.round(z.worker_population_est * 0.35), 0) |
|
|
| export const PIPELINE_STATS_RESPONSE: PipelineStats = { |
| total_runs: RUN_SEEDS.length, |
| successful_runs: SUCCESSFUL_RUNS, |
| success_rate: Math.round((SUCCESSFUL_RUNS / RUN_SEEDS.length) * 100) / 100, |
| zones_monitored: ZONES.length, |
| cities: 1, |
| active_triggers: TRIGGERING_NOW.length, |
| total_enrolled: TOTAL_ENROLLED, |
| total_cost_usd: TOTAL_COST, |
| avg_cost_per_run_usd: Math.round((TOTAL_COST / RUN_SEEDS.length) * 10000) / 10000, |
| last_run: isoDaysAgo(0, 2, 7), |
| data_sources: ['ERA5-Land', 'NASA POWER', 'Tanzania Met'], |
| } |
|
|
| |
|
|
| export const ENROLLED_RESPONSE: EnrolledResponse = { |
| by_zone: ZONE_SEEDS.map((z) => ({ |
| zone_id: z.zone_id, |
| enrolled: Math.round(z.worker_population_est * 0.35), |
| worker_population: z.worker_population_est, |
| })), |
| total_enrolled: TOTAL_ENROLLED, |
| } |
|
|
| |
| |
| |
| |
|
|
| interface ZonePopulation { |
| total_pop: number |
| outdoor_informal: number |
| pct_women: number |
| } |
|
|
| const ZONE_POPULATIONS: Record<string, ZonePopulation> = { |
| 'DAR-MAB': { total_pop: 120000, outdoor_informal: 11950, pct_women: 0.62 }, |
| 'DAR-MNZ': { total_pop: 150000, outdoor_informal: 14820, pct_women: 0.61 }, |
| 'DAR-TAN': { total_pop: 128000, outdoor_informal: 12640, pct_women: 0.6 }, |
| 'DAR-BUG': { total_pop: 88000, outdoor_informal: 8720, pct_women: 0.56 }, |
| 'DAR-VIN': { total_pop: 102000, outdoor_informal: 9840, pct_women: 0.57 }, |
| 'DAR-JAN': { total_pop: 78000, outdoor_informal: 7310, pct_women: 0.58 }, |
| 'DAR-MBU': { total_pop: 66000, outdoor_informal: 6520, pct_women: 0.55 }, |
| 'DAR-KIG': { total_pop: 82000, outdoor_informal: 8140, pct_women: 0.54 }, |
| 'DAR-MAG': { total_pop: 71000, outdoor_informal: 6980, pct_women: 0.55 }, |
| 'DAR-MWA': { total_pop: 59000, outdoor_informal: 5860, pct_women: 0.53 }, |
| 'DAR-MSA': { total_pop: 49000, outdoor_informal: 4910, pct_women: 0.52 }, |
| 'DAR-KIN': { total_pop: 108000, outdoor_informal: 4280, pct_women: 0.5 }, |
| 'DAR-SIN': { total_pop: 95000, outdoor_informal: 3760, pct_women: 0.49 }, |
| 'DAR-KAW': { total_pop: 82000, outdoor_informal: 3240, pct_women: 0.48 }, |
| 'DAR-MIK': { total_pop: 55000, outdoor_informal: 2178, pct_women: 0.47 }, |
| } |
|
|
| type Gender = 'all' | 'women' | 'men' |
| type SettlementFilter = 'all' | 'informal' | 'mixed' | 'formal' |
|
|
| export function buildCoverage( |
| _payoutUsd: number, |
| gender: Gender, |
| settlementFilter: SettlementFilter |
| ): CoverageResponse { |
| const scenarioLabel = [ |
| gender === 'all' ? 'All' : gender === 'women' ? 'Women' : 'Men', |
| settlementFilter === 'all' ? 'outdoor workers' : `in ${settlementFilter} settlements`, |
| ].join(' ') |
|
|
| let totalWorkers = 0 |
| let weeklyCostTotal = 0 |
| let annualCostTotal = 0 |
| let zonesTriggering = 0 |
|
|
| const zoneRecs: CoverageZone[] = [] |
| for (const z of ZONE_SEEDS) { |
| if (settlementFilter !== 'all' && z.settlement_type !== settlementFilter) continue |
| const pop = ZONE_POPULATIONS[z.zone_id] |
| if (!pop) continue |
| let enrolled = pop.outdoor_informal |
| if (gender === 'women') enrolled = Math.floor(enrolled * pop.pct_women) |
| if (gender === 'men') enrolled = Math.floor(enrolled * (1 - pop.pct_women)) |
| if (enrolled === 0) continue |
|
|
| const payoutTriggered = z.current_wbgt_c > CITY_P97_WBGT |
| const pf = z.trigger_probability_7d |
| const alertPayout = Math.round((2 + pf * 3) * 100) / 100 |
| const insurancePayout = Math.round((7 + pf * 13) * 100) / 100 |
| const weeklyCostPerWorker = payoutTriggered ? insurancePayout : 0 |
| |
| |
| const annualTriggerDays = Math.min(z.annual_trigger_days, 90) |
| const annualCostPerWorker = Math.round(insurancePayout * (annualTriggerDays / 7) * 100) / 100 |
| const annualWorkerShare = 3 |
| const annualPhilanthropy = Math.round(annualCostPerWorker * 0.45 * 100) / 100 |
| const annualInsurer = Math.round(Math.max(0, annualCostPerWorker - annualWorkerShare - annualPhilanthropy) * 100) / 100 |
|
|
| if (payoutTriggered) zonesTriggering++ |
| totalWorkers += enrolled |
| weeklyCostTotal += weeklyCostPerWorker * enrolled |
| annualCostTotal += annualCostPerWorker * enrolled |
|
|
| const urgency = pf > 0.8 ? 'critical' : pf > 0.5 ? 'high' : pf > 0.3 ? 'moderate' : 'low' |
|
|
| zoneRecs.push({ |
| zone_id: z.zone_id, |
| zone_name: z.name, |
| city: 'Dar es Salaam', |
| settlement_type: z.settlement_type, |
| heat_vulnerability: z.heat_vulnerability, |
| urgency, |
| current_temp_c: Math.round(z.corrected_temp_c * 10) / 10, |
| current_wbgt_c: Math.round(z.current_wbgt_c * 10) / 10, |
| trigger_probability_7d: pf, |
| triggers_this_week: payoutTriggered, |
| risk_level: urgency, |
| outdoor_exposure_pct: z.outdoor_exposure_pct, |
| enrolled_workers: enrolled, |
| weekly_cost_per_worker: Math.round(weeklyCostPerWorker * 100) / 100, |
| alert_payout: payoutTriggered ? alertPayout : 0, |
| insurance_payout: payoutTriggered && pf > 0.8 ? insurancePayout : 0, |
| total_payout_this_week: Math.round(weeklyCostPerWorker * 100) / 100, |
| worker_contribution: Math.round((3 / 52) * 100) / 100, |
| philanthropy_share: 0, |
| insurer_premium: weeklyCostPerWorker, |
| neural_model: z.model_tier === 'ensemble' || z.model_tier === 'full_model', |
| learned_frequency: z.annual_trigger_days, |
| payout_factor: pf, |
| annual_cost_per_worker: annualCostPerWorker, |
| annual_worker_share: annualWorkerShare, |
| annual_philanthropy_share: annualPhilanthropy, |
| annual_insurer_share: annualInsurer, |
| annual_trigger_days: annualTriggerDays, |
| }) |
| } |
|
|
| return { |
| recommendation: { |
| weekly_cost_per_worker: |
| totalWorkers > 0 ? Math.round((weeklyCostTotal / totalWorkers) * 100) / 100 : 0, |
| weekly_cost_total: Math.round(weeklyCostTotal), |
| annual_cost_total: Math.round(annualCostTotal), |
| annual_cost_per_worker: |
| totalWorkers > 0 ? Math.round((annualCostTotal / totalWorkers) * 100) / 100 : 0, |
| total_workers: totalWorkers, |
| zones_triggering: zonesTriggering, |
| zones_total: zoneRecs.length, |
| model_type: 'neural_evt', |
| scenario: scenarioLabel, |
| payout_threshold_wbgt: CITY_P97_WBGT, |
| }, |
| zones: zoneRecs, |
| cost_summary: { |
| total_to_workers_pct: 85, |
| total_admin_pct: 7, |
| total_basis_risk_pct: 8, |
| worker_contribution_pct: 15, |
| philanthropy_pct: 45, |
| insurer_pct: 40, |
| }, |
| } |
| } |
|
|
| |
| |
|
|
| export function buildCalibrate(params: CalibrateParams): CalibrateResponse { |
| const zones: CalibrateZoneResult[] = ZONE_SEEDS.map((z, idx) => { |
| const enrolled = Math.round(z.worker_population_est * 0.35) |
| const daysAboveTemp = Math.round(z.annual_trigger_days * 1.1) |
| const daysAboveWbgt = z.annual_trigger_days |
| const events = Math.max(1, Math.round(z.annual_trigger_days / 3.4)) |
| const eventsPerYear = events |
| const annualPayoutPerWorker = Math.round(events * params.payout_usd * 100) / 100 |
| const annualPayoutTotal = Math.round(annualPayoutPerWorker * enrolled) |
| return { |
| zone_id: z.zone_id, |
| zone_name: z.name, |
| city: 'Dar es Salaam', |
| settlement_type: z.settlement_type, |
| heat_vulnerability: z.heat_vulnerability, |
| enrolled_workers: enrolled, |
| days_above_temp: daysAboveTemp, |
| days_above_wbgt: daysAboveWbgt, |
| consecutive_days_temp: z.consecutive_hot_days + 1, |
| consecutive_days_wbgt: z.consecutive_hot_days, |
| trigger_events: events, |
| events_per_year: eventsPerYear, |
| annual_payout_per_worker: annualPayoutPerWorker, |
| annual_payout_total: annualPayoutTotal, |
| basis_risk_score: Math.round((0.74 - (z.settlement_type === 'formal' ? 0.12 : 0) - (z.settlement_type === 'mixed' ? 0.06 : 0)) * 100) / 100, |
| triggered: z.current_wbgt_c > params.wbgt_threshold, |
| actuarial_cost_per_worker: Math.round(annualPayoutPerWorker * 1.17 * 100) / 100, |
| cost_breakdown: { payouts: annualPayoutTotal, admin: Math.round(annualPayoutTotal * 0.08), reserves: Math.round(annualPayoutTotal * 0.09) }, |
| allocated_budget: Math.round(annualPayoutTotal * 1.17), |
| workers_covered: enrolled, |
| coverage_pct: 1, |
| priority_rank: idx + 1, |
| } |
| }) |
|
|
| const totalAnnualCost = zones.reduce((s, z) => s + z.annual_payout_total, 0) |
| return { |
| zones, |
| summary: { |
| total_zones: zones.length, |
| zones_triggered: zones.filter((z) => z.triggered).length, |
| total_trigger_days: zones.reduce((s, z) => s + z.days_above_wbgt, 0), |
| avg_events_per_year: Math.round( |
| (zones.reduce((s, z) => s + z.events_per_year, 0) / zones.length) * 10 |
| ) / 10, |
| total_annual_cost: totalAnnualCost, |
| avg_cost_per_worker: |
| Math.round( |
| (zones.reduce((s, z) => s + z.annual_payout_per_worker, 0) / zones.length) * 100 |
| ) / 100, |
| total_enrolled: TOTAL_ENROLLED, |
| avg_basis_risk: |
| Math.round( |
| (zones.reduce((s, z) => s + z.basis_risk_score, 0) / zones.length) * 100 |
| ) / 100, |
| }, |
| allocation: { |
| budget_usd: params.budget_usd, |
| worker_contribution_usd: params.worker_contribution_usd, |
| workers_covered: TOTAL_ENROLLED, |
| overall_coverage_pct: 1, |
| zones_fully_funded: zones.length, |
| zones_partially_funded: 0, |
| zones_unfunded: 0, |
| stretch_analysis: { |
| additional_workers_coverable: 18_400, |
| stretch_budget_usd: Math.round(params.budget_usd * 1.25), |
| }, |
| }, |
| thresholds: params, |
| } |
| } |
|
|
| |
| |
| |
| |
|
|
| export const METRICS = { |
| total_workers: TOTAL_WORKERS, |
| total_enrolled: TOTAL_ENROLLED, |
| zones_monitored: ZONES.length, |
| payouts_this_season: PAYOUTS_THIS_SEASON, |
| annual_cost_usd: 2_680_000, |
| average_payout_usd: 10, |
| } |
|
|