An open-source analytical platform monitoring regime transformation dynamics in Venezuela. Umbral tracks democratic erosion through real-time data, historical indices, curated news, and expert scenario analysis.
- Scenario Analysis — Five evidence-based regime transformation scenarios rated by experts and the public on a Likert 1–5 scale, with aggregated probability indicators
- STAR Voting Consensus — Daily-computed consensus scenario panels for experts and the public using STAR voting (Score Then Automatic Runoff); stored as daily snapshots in Supabase
- Historical Trajectory — V-Dem style democracy index chart spanning 1900–2024
- Live News Feed — Aggregated news with category filtering, source filtering, full-text search, and per-scenario voting
- Political Prisoners Tracker — Aggregate detention statistics with demographic breakdowns by reporting organization
- Interactive Timeline — Democratic Episodes Event Dataset (DEED) with bilingual (Spanish/English) events
- Reading Room — Curated archive of books, articles, reports, and journalism
- Fact-Checking Feed — Tweets from three Venezuelan fact-checking accounts (@cazamosfakenews, @cotejoinfo, @Factchequeado), refreshed daily
- GDELT Media Signals — Daily-archived instability index, media tone, and article volume from GDELT, with annotated key events timeline
- Internet Connectivity Monitor (National) — IODA-powered BGP, Active Probing, and Network Telescope signal charts for Venezuela as a whole; daily cron archives data to Supabase
- Internet Connectivity Monitor (Subnational) — State-level IODA dashboard with a horizon heatmap, Leaflet choropleth map (25 states + Guayana Esequiba), and ranked outage score list
- Official Gazette Dashboard — Analytical dashboard for Venezuela's Gaceta Oficial with 5 tabs: summary (KPIs, daily activity chart, change-type donut, top organisms), appointments, military analysis, institutional breakdown with structural events, and a searchable full-record table. Includes bilingual field-level translation for organisms, positions, military ranks, and summaries
- Internet Censorship Monitor — Blocked domains dashboard tracking ISP-level censorship by CANTV, Movistar, and Digitel
- Prediction Markets — Polymarket contract dashboard for Venezuela-related markets
- Participate — Multi-step survey for experts and the public to submit scenario probability assessments
- Bilingual — Full Spanish/English internationalization throughout
- Privacy-first Analytics — GA4 integration gated behind explicit cookie consent
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router, Turbopack) |
| Language | TypeScript |
| Styling | Tailwind CSS, Framer Motion |
| Database | Supabase (PostgreSQL) with Row Level Security |
| Charts | Recharts |
| Maps | Leaflet + react-leaflet 4.x (React 18 compatible) |
| AI | Anthropic SDK (Claude Haiku for news translation) |
| Scraping | RSS Parser (rss-parser) |
| Icons | Lucide React |
- Node.js 18+
- npm
- A Supabase project (optional — the app runs on local mock data without one)
git clone https://github.com/pablohernandezb/umbral-project.git
cd umbral-project
npm installCreate a .env.local file in the project root. Without it, the app runs in Mock Mode using local sample data — no database required.
# Supabase (required for live data)
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key # seed script only, never expose client-side
# Google Analytics (optional)
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
# Anthropic (optional, for AI features)
ANTHROPIC_API_KEY=your-anthropic-key
# X (Twitter) API — required for fact-checking feed (Basic plan bearer token)
X_BEARER_TOKEN=your-x-bearer-token
# Cron job authorization secret
CRON_SECRET=your-cron-secretSecurity note:
SUPABASE_SERVICE_ROLE_KEYis only used by the seed script and must never be exposed to the browser.
npm run devOpen http://localhost:3000.
The app detects which mode to use automatically:
| Mode | Condition | Data Source |
|---|---|---|
| Mock Mode | No valid .env.local |
data/mock.ts (static local data) |
| Supabase Mode | Valid Supabase credentials in .env.local |
PostgreSQL via Supabase |
Switch modes by adding or removing .env.local, then restart the dev server.
In Supabase mode the national IODA dashboard reads exclusively from the database (populated by daily cron). The subnational dashboard always fetches live from IODA.
If you want to connect a real Supabase database:
- Deploy the schema — Copy the
SCHEMA_SQLexport fromlib/supabase.tsinto the Supabase SQL Editor and run it - Configure credentials — Add your Supabase URL and keys to
.env.local - Seed the database — Run the seed script:
npm run seedThe schema creates 18 tables with RLS policies. Real-time subscriptions are enabled for news_feed, political_prisoners, and scenarios.
Automated data collection runs on Vercel's scheduler (configured in vercel.json). All jobs respect Vercel Hobby's 10-second function limit by using parallel fetches.
| Schedule (UTC) | Endpoint | Purpose |
|---|---|---|
| Daily 04:59 | /api/gdelt?force=true |
Archive GDELT media signals (120-day rolling window) |
| Daily 06:00 | /api/ioda/sync |
Archive national IODA connectivity signals + events |
| Daily 08:00 | /api/ioda/sync-subnational |
Archive subnational IODA region signals + outage scores |
| Daily 10:00 | /api/fact-check/refresh |
Fetch latest tweets from 3 fact-checking accounts |
| Daily 12:00 | /api/news/scrape |
Scrape and store latest news articles |
| Daily 14:00 | /api/analytics/snapshot |
Compute & store STAR voting + submission averages snapshots |
You can trigger any cron job manually during development:
# On macOS/Linux
curl "http://localhost:3000/api/analytics/snapshot?secret=<CRON_SECRET>"
curl "http://localhost:3000/api/news/scrape?secret=<CRON_SECRET>"
curl "http://localhost:3000/api/fact-check/refresh?secret=<CRON_SECRET>"
curl "http://localhost:3000/api/gdelt?force=true"
# On Windows PowerShell use curl.exe
curl.exe "http://localhost:3000/api/analytics/snapshot?secret=<CRON_SECRET>"umbral-project/
├── app/ # Next.js App Router pages and API routes
│ ├── page.tsx # Landing page (Command Center)
│ ├── about/
│ ├── how-did-we-get-here/
│ ├── news/
│ ├── participate/
│ ├── reading-room/
│ ├── privacy-terms/
│ ├── admin/ # Protected admin dashboard
│ └── api/
│ ├── fact-check/refresh/ # Cron: X fact-checking tweets
│ ├── gdelt/ # Cron + on-demand: GDELT media signals
│ ├── ioda/ # IODA proxy + cron sync + batch endpoints
│ │ ├── route.ts # Proxy to IODA API (never call IODA directly from client)
│ │ ├── sync/ # Cron: archive national signals + events to Supabase
│ │ ├── regions/ # Batch signals for all 25 VE regions
│ │ └── outages/ # Batch outage scores for all 25 VE regions
│ ├── news/scrape/ # Cron: news scraping
│ ├── analytics/snapshot/ # Cron: STAR voting + averages snapshots
│ └── gazette/ # Gazette batch upload + record management
│ ├── upload/ # CSV batch upload for gazette records
│ ├── records/ # CRUD for gazette records
│ └── batches/ # Batch management
├── components/
│ ├── layout/ # Header (with share menu), Footer
│ ├── ui/ # ScenarioCard, NewsCard, GdeltDashboard, PolymarketDashboard, etc.
│ ├── ioda/ # IODA connectivity dashboards (national + subnational)
│ │ ├── IodaDashboard.tsx # National: 3 signal charts + outage events
│ │ ├── SubnationalDashboard.tsx # State-level: heatmap + map + score list
│ │ ├── StateHeatmap.tsx # Horizon heatmap (25 states × time)
│ │ ├── VenezuelaMap.tsx # Leaflet choropleth (25 states + Esequibo)
│ │ ├── OutageScoreList.tsx # Ranked outage scores
│ │ ├── OutageEventList.tsx # Discrete outage events
│ │ ├── SignalChart.tsx # Single-signal AreaChart card
│ │ ├── StatusBadge.tsx # Connectivity status pill
│ │ └── RegionSelector.tsx # Region dropdown
│ ├── gaceta/ # Official Gazette dashboard
│ │ ├── GacetaDashboard.tsx # 5-tab container (resumen, designaciones, militares, instituciones, buscador)
│ │ ├── ResumenTab.tsx # Summary: KPIs, daily activity area chart, change-type donut, organisms bar
│ │ ├── DesignacionesTab.tsx# Appointments analysis: subtypes bar chart + recent table
│ │ ├── MilitaresTab.tsx # Military analysis: comparison bars, ministry breakdown, full table
│ │ ├── InstitucionesTab.tsx# Institutional analysis: top organisms chart + structural events
│ │ ├── BuscadorTab.tsx # Searchable/filterable full-record table with pagination
│ │ ├── gaceta-utils.ts # Summary computation, label classification, organism helpers
│ │ └── gaceta-i18n.ts # Field-level ES→EN translation dictionaries (organisms, positions, ranks)
│ ├── connectivity/ # Internet censorship/blocking dashboard
│ ├── charts/ # TrajectoryChart, GdeltSignalChart
│ ├── GoogleAnalytics.tsx
│ ├── CookieBanner.tsx
│ └── CookiePreferences.tsx
├── data/
│ ├── mock.ts # Local mock data for all tables
│ ├── seed.ts # Database seed script
│ ├── venezuela-states.ts # 25 VE states with IODA numeric codes (4482–4506)
│ └── gdelt-annotations.ts # Key political events for GDELT chart overlay
├── hooks/
│ └── useIoda.ts # Generic IODA data-fetching hook with auto-refresh
├── i18n/
│ ├── es/common.json # Spanish translations (default)
│ └── en/common.json # English translations
├── lib/
│ ├── supabase.ts # DB client, IS_MOCK_MODE flag, SCHEMA_SQL
│ ├── data.ts # Data access layer with mock fallback
│ ├── ioda.ts # IODA API functions, severity utilities, score computation
│ ├── x-api.ts # X API client for fact-checking tweets
│ └── cookie-consent.ts # Cookie consent context
├── public/data/
│ └── venezuela-geo.json # GADM 4.1 GeoJSON — 26 features, 319KB, full precision
├── types/
│ ├── index.ts # Core database TypeScript interfaces
│ └── ioda.ts # IODA-specific types (signals, outages, severity, etc.)
└── vercel.json # Cron job schedule
npm run dev # Start development server
npm run build # Production build (type-checks + optimizes)
npm run start # Start production server
npm run lint # ESLint
npm run seed # Seed Supabase databaseThe app defaults to Spanish and supports English. Language is toggled via the header. All translation strings live in i18n/es/common.json and i18n/en/common.json and are accessed with the useTranslation() hook using dot notation:
const { t } = useTranslation()
t('scenarios.democraticTransition.title')
t('ioda.status.outage')
t('ioda.events.detected', { count: 4 })Contributions are welcome. Please open an issue to discuss proposed changes before submitting a pull request.
- Fork the repository
- Create a feature branch (
git checkout -b feature/your-feature) - Commit your changes
- Push to the branch and open a Pull Request
