* feat(sentiment): add AAII investor sentiment survey
Weekly bull/bear/neutral sentiment from AAII (1987-present). Shows
current reading, bull-bear spread, and 52-week historical chart.
Seeder fetches from AAII CSV, stores last 52 weeks in Redis.
* fix(aaii): wire panel loading + mark fallback data explicitly
* fix(aaii): keep panel live across refreshes + surface in health monitoring
- fetchData now falls back to /api/bootstrap?keys=aaiiSentiment on
refresh (getHydratedData is one-shot and returns undefined after
the first read, causing a permanent spinner on hourly refresh)
- Shows an error state with auto-retry when both hydrated and
bootstrap-fetch miss, matching the WsbTickerScannerPanel pattern
- Registered aaiiSentiment in api/health.js BOOTSTRAP_KEYS and
api/seed-health.js SEED_DOMAINS so rollout failures and
fallback-only operation are observable in the monitoring dashboards
* fix(sentiment): handle BIFF8 SST trailing bytes and use UTC for AAII Thursday calc
Two P2 greptile fixes from PR #2930 review:
1. BIFF8 SST parser was reading the rich-text run count (cRun, flags & 0x08)
and extended-string size (cbExtRst, flags & 0x04) to advance past those
header fields, but never skipped the trailing bytes AFTER the char data:
4 * cRun formatting-run bytes and cbExtRst ext-rst bytes. If any string
before the column header was rich-text formatted, every subsequent SST
entry parsed from the wrong offset, silently breaking XLS extraction and
falling back to HTML scraping.
2. parseHtmlSentiment() computed last-Thursday via today.getDay() +
setDate(today.getDate() - daysToThursday), both local-TZ-dependent. On
Railway (non-UTC TZ) the inferred Thursday could drift by a day, causing
the HTML-derived row to mismatch the XLS historical rows. Switched to
getUTCDay() + Date.UTC() for TZ-stable arithmetic.