Summary of changes — March 16–17, 2026
This issue documents the batch of features and fixes shipped over the last two sessions.
✅ Journalist Investigation Features
GET /api/v1/stories/packages — 8 pre-packaged investigation narratives with live data
- Each package includes: Spanish lede, concrete vendor examples, expected government defense, FOIA next steps, difficulty badge (rapida / requiere_solicitud / investigacion_larga)
- Packages:
ghost_companies, top_suspicious, administration_comparison, efos_vendors, sector_overpricing, new_vendor_risk, monopoly_capture, direct_award_surge
GET /api/v1/vendors/{id}/similar-cases — cosine similarity vs GT case centroids
- Computes vendor z-score centroid from
contract_z_features, returns top 3 similar corruption cases with shared/divergent features
GET /api/v1/vendors/{id}/narrative — year-by-year arc detection
- Returns
arc_shape (explosive_entry, capture_pattern, single_burst, etc.), peak_year, active_years, per-year data
StoryFinderSection in /journalists — replaces static "IDEAS" section
- Expandable
StoryPackageCard components with difficulty badges, live vendor examples table, defense blockquote, numbered next steps
- Falls back to original static cards on error/loading
"Periodista" tab in VendorProfile (pre-existing, confirmed working)
- Narrative arc BarChart, similar GT case cards, auto-generated Spanish lede with copy button, evidence CSV download
🐛 Bug Fixes
stories/packages 30s timeout (fix(stories))
- Root cause: 3 of 8 queries hit full 3.1M row table scans —
sector_overpricing took 71.6s alone, direct_award_surge 23.8s, admin_comparison 20.6s
- Fix: module-level 1-hour TTL cache + background thread. First request returns
503 Retry-After: 30 while computation runs (~2 min). warm_stories_cache() called from _warmup_caches at startup so cache is hot before users arrive. Subsequent requests: <1ms
test_year_filter PHI test — 1 failing test fixed
- PHI year-filtered endpoint legitimately returns 503 on cold cache (background computation model by design). Test now accepts
200 | 503
Executive Summary stat bombs overflow (fix(ui))
- 6-column grid was clipping
361K, 130K, 0.849, 11.9% at narrower viewports
- Fix:
gap-4→3, p-5→p-3 sm:p-4, font size scaling >5chars:1.6rem / >3chars:2rem / ≤3chars:2.5rem, overflow-hidden→whitespace-nowrap
Journalists page dark mode (feat(journalists))
- Removed hardcoded
style={{ backgroundColor: '#faf9f6' }} inline style
- Added
dark: Tailwind classes throughout: cards, tables, text, borders, grade badges, difficulty badges, attribution blockquote, copy button states
🚀 Deploy
All changes deployed to production at http://37.60.232.109 (rubli.xyz) via Docker Compose.
Commits:
2a62dcf feat(journalists): dark mode support throughout
1f00482 fix(stories): add TTL cache + startup warmup to avoid 30s timeout
99ab5ef fix(model): v6.1 red-team hardening + frontend CSS theming (incl. stat bomb fix)
Test status: 590 passing (backend) · 0 TypeScript errors · build ✓
Summary of changes — March 16–17, 2026
This issue documents the batch of features and fixes shipped over the last two sessions.
✅ Journalist Investigation Features
GET /api/v1/stories/packages— 8 pre-packaged investigation narratives with live dataghost_companies,top_suspicious,administration_comparison,efos_vendors,sector_overpricing,new_vendor_risk,monopoly_capture,direct_award_surgeGET /api/v1/vendors/{id}/similar-cases— cosine similarity vs GT case centroidscontract_z_features, returns top 3 similar corruption cases with shared/divergent featuresGET /api/v1/vendors/{id}/narrative— year-by-year arc detectionarc_shape(explosive_entry, capture_pattern, single_burst, etc.),peak_year,active_years, per-year dataStoryFinderSectionin/journalists— replaces static "IDEAS" sectionStoryPackageCardcomponents with difficulty badges, live vendor examples table, defense blockquote, numbered next steps"Periodista" tab in VendorProfile (pre-existing, confirmed working)
🐛 Bug Fixes
stories/packages30s timeout (fix(stories))sector_overpricingtook 71.6s alone,direct_award_surge23.8s,admin_comparison20.6s503 Retry-After: 30while computation runs (~2 min).warm_stories_cache()called from_warmup_cachesat startup so cache is hot before users arrive. Subsequent requests: <1mstest_year_filterPHI test — 1 failing test fixed200 | 503Executive Summary stat bombs overflow (
fix(ui))361K,130K,0.849,11.9%at narrower viewportsgap-4→3,p-5→p-3 sm:p-4, font size scaling>5chars:1.6rem / >3chars:2rem / ≤3chars:2.5rem,overflow-hidden→whitespace-nowrapJournalists page dark mode (
feat(journalists))style={{ backgroundColor: '#faf9f6' }}inline styledark:Tailwind classes throughout: cards, tables, text, borders, grade badges, difficulty badges, attribution blockquote, copy button states🚀 Deploy
All changes deployed to production at http://37.60.232.109 (rubli.xyz) via Docker Compose.
Commits:
2a62dcffeat(journalists): dark mode support throughout1f00482fix(stories): add TTL cache + startup warmup to avoid 30s timeout99ab5effix(model): v6.1 red-team hardening + frontend CSS theming (incl. stat bomb fix)Test status: 590 passing (backend) · 0 TypeScript errors · build ✓