Data collectors - ram290476/investor-dashboard GitHub Wiki
How the investor-dashboard data jobs collect, store and publish data. Sources: services/data-jobs/, infra/terraform/variables.tf (var.jobs, var.api_keys), services/data-jobs/src/app/status_feed.py, docs/operations.md, and issue #41.
Credentials: see Data collectors API keys. This page lists SSM key names only and never their values.
-
Runtime: every job is a container-image Lambda named
invdash-<job>, built fromservices/data-jobs. Handlers live insrc/functions/<name>/. Terraformvar.jobsgives each job its handler, schedule, triggers, memory/timeout,read_prefixes/write_prefixes,api_keysandreads_prefs. The jobs module (infra/terraform/modules/jobs) turns these into Lambdas, IAM and EventBridge rules. -
Schedules: EventBridge Scheduler cron expressions in
America/New_York. Jobs check the NYSE / US federal calendars and skip holidays. -
Triggers: event-driven jobs subscribe to
job:<ID>patterns on the default bus.ticker-added(from prefs-api) starts the backfill. -
job_handler(job_id)(src/app/observability.py) wraps each handler. It:- sets correlation ids (
run_id = <JOB>-<UTC ts>-<hex>), X-Ray annotations and metric flushing; - records one run per source through the
source_run("DS-xx")bulkhead (a failure is recorded and the job moves on to the next source); - writes ingestion_runs records;
- publishes a
Job Finishedevent with{job, run_id, outcome, failed_sources}.
- sets correlation ids (
-
status-feed (trigger
job:*) merges each event intoserving/status.jsonwith conditional writes (If-Match ETag).JOBSinstatus_feed.pylists 16 job ids with display names, planned crons and calendars. A job that has never reported showsstatus: never_runandlast_run: null. -
Data freshness panel (
apps/web/app.js→renderStatus()) shows one row perstatus.jobs[]entry andformatTime(job.last_run). A nulllast_rundisplays as "not yet collected".
flowchart LR
S[EventBridge Scheduler<br/>ET crons] --> C[Collector Lambdas<br/>invdash-*]
P[prefs-api<br/>ticker-added] --> B[invdash-backfill]
C -->|api_key from SSM<br/>/invdash/api-keys/*| K[(SSM SecureString)]
C --> R[(raw/)]
C --> CU[(curated/ Parquet)]
B --> CU
C -- Job Finished --> EB{{default event bus}}
B -- Job Finished --> EB
EB -->|job:D4, M1, RECONCILE| T[trend-metrics]
T --> SV1[(serving/trend_metrics/)]
T -- Job Finished --> EB
EB -->|job:D4, TREND, Q1, SHORT,<br/>OPTIONS, BACKFILL, RECONCILE| D[dashboard-build]
EB -->|job:*| SF[status-feed]
SF --> ST[(serving/status.json)]
D --> DJ[(serving/dashboard.json)]
ST --> D
DJ --> W[Web app / Data freshness panel]
ST --> W
Bucket invdash-lake-308639168050 (us-west-1). SSE-KMS with the project data key is the bucket default. Helpers are in src/app/lake.py.
| Layer | Format | Contents |
|---|---|---|
raw/ |
Untouched provider JSON | Payloads as received, so curated data can be rebuilt. |
curated/ |
Typed Parquet (zstd), Hive-style partitions | e.g. curated/prices_daily/ticker=<T>/year=<YYYY>/prices.parquet, curated/short_interest/settlement_date=<D>/short_interest.parquet, curated/fundamentals_quarterly/, curated/options_daily/, curated/macro_daily/. Shared yearly price partitions use conditional read-merge-write (upsert_parquet / upsert_prices, If-Match retries). For the same ticker and date, D4/Alpaca DS-02 rows win over Yahoo backfill DS-05 rows. State files such as _backfill/state.json and _state.json sit next to the data. |
serving/ |
Compact JSON read by the site and API |
serving/dashboard.json (snapshot from dashboard-build, schema_version 1), serving/status.json (status-feed), serving/trend_metrics/latest/<T>.json, serving/fundamentals_quarterly.json. |
-
ingestion_runs: one row per source run (
job,run_id, source, outcome, rows, duration), written tocurated/ingestion_runs/year=<Y>/month=<M>/<run_id>.parquet. -
Least privilege: each job's IAM role can only write its
write_prefixesand read itsread_prefixes.bucketandbucket/*are granted separately. -
Per-job folder isolation: a job writes only under its own keys, so concurrent jobs never overwrite each other. The exceptions are the shared price partitions and
status.json, both of which use conditional writes. -
Legacy layouts:
date=<D>/daily.parquetandbatch_start=/batch_end=price objects are still read and deduped.scripts/compact_price_partitions.pyfolds them into the yearly objects.
All schedules are ET (America/New_York) unless noted. The SSM names are under /invdash/api-keys/.
| Job ID | Lambda | Schedule / trigger | Sources | SSM keys | Writes | Notes |
|---|---|---|---|---|---|---|
| BACKFILL | invdash-backfill |
rate(15 minutes) + ticker-added
|
DS-05 Yahoo chart (5y daily) | — | curated/prices_daily/ |
Resumable checkpoint curated/prices_daily/_backfill/state.json. |
| D4 | invdash-daily-prices |
cron(45 16 ? * MON-FRI *) |
DS-02 Alpaca daily bars (raw/split/all adj., last 7 days) |
alpaca-key-id, alpaca-secret-key
|
curated/prices_daily/ |
Skips NYSE holidays. |
| OPTIONS | invdash-options-daily |
cron(50 16 ? * MON-FRI *) |
Alpaca options snapshots, indicative feed (put/call, IV30) |
alpaca-key-id, alpaca-secret-key
|
curated/options_daily/ |
Does nothing until enable_options_daily = true. |
| SHORT | invdash-short-interest |
cron(30 18 ? * MON-FRI *) |
DS-91 FINRA consolidated short interest |
finra-client-id, finra-client-secret
|
curated/short_interest/ |
Writes only new settlement dates (twice a month). |
| Q1 | invdash-q1-fundamentals |
cron(30 8 ? * MON *) |
manual/fundamentals/ |
— |
curated/fundamentals_quarterly/, serving/fundamentals_quarterly.json
|
Weekly safety net. |
| TREND | invdash-trend-metrics |
job:D4, job:M1, job:RECONCILE
|
curated/prices_daily/, curated/macro_daily/
|
— | serving/trend_metrics/ |
Feeds the Macro drivers panel. |
| RECONCILE | invdash-price-reconcile |
cron(15 19 ? * MON-FRI *) |
Yahoo splits/dividends | — | curated/prices_daily/ |
Rewrites adjusted history when a corporate action appears. |
| — | invdash-dashboard-build |
job:D4, TREND, Q1, SHORT, OPTIONS, BACKFILL, RECONCILE |
prices, trend, fundamentals, status | — | serving/dashboard.json |
Builds the site snapshot. |
| — | invdash-status-feed |
job:* |
Job Finished events | — | serving/status.json |
Data freshness feed. |
| — | key-rotation checker (key_rotation_check) |
cron(0 8 * * ? *) America/Los_Angeles
|
SSM parameter metadata and RotationDays tags (never values) |
reads metadata of all keys | — | Emails the security topic at 14/7/3/1/0 days before due, every day when overdue, and on Mondays when a key still holds the placeholder. |
The prefs-api Lambda serves user preferences (DynamoDB) and emits ticker-added.
4. Planned collectors (issue #41)
Update, Oct 10, 2026: all five collectors below were built and merged on Oct 7 (PRs #42, #43 and #47), and D1 followed in #55. The table is kept as the original design record. See section 7 for what happened when they went live.
These ids are in status_feed.JOBS, but no Lambda or terraform job exists for them yet, so the panel shows "not yet collected". Full design: issue #41.
| ID | Display name | Planned cron (ET) | Sources (SSM keys) | Storage | Aggregation → serving |
|---|---|---|---|---|---|
| H1 | Market prices |
5 10-16 * * 1-5 (early close stops at 13:05) |
Alpaca 1-hour bars (alpaca-key-id, alpaca-secret-key); Finnhub (finnhub) and Alpha Vantage (alpha-vantage) as backups |
raw/alpaca_bars_1h/, curated/prices_hourly/ticker=/year=/
|
Dedupe on (ticker, ts); intraday last, change % and session high/low → tickers[T].intraday
|
| H2 | News & sentiment | 15 * * * 1-5 |
Finnhub company-news (finnhub), Alpha Vantage NEWS_SENTIMENT (alpha-vantage, ≤16 calls/day), Massive (massive), Google News and SNAPI RSS |
raw/news/, curated/news_articles/date=/, curated/news_daily/
|
URL-hash dedupe, −1…1 sentiment score, daily and 7-day rollups → tickers[T].news
|
| H3 | Regulatory & company feeds | 25 6-22 * * 1-5 |
SEC EDGAR submissions (no key; needs sec_user_agent), Federal Register, Fed RSS, White House, OFAC, Launch Library 2; FCC (api-data-gov) optional |
raw/edgar/, curated/filings/ticker=/year=/, curated/events/date=/
|
Accession dedupe; 8-K item and Form 4 classification; net insider flow over 30 days → tickers[T].filings, events
|
| D5 | Government contracts | 45 17 * * 1-5 |
USAspending (no key), SAM.gov awards and opportunities (sam-gov), DoD announcements, NASA RSS |
raw/contracts/, curated/contracts/fiscal_year=/, curated/contracts_rollup/
|
Upsert on award_id with mods summed; obligated $ by ticker × agency × quarter and TTM → tickers[T].contracts
|
| M1 | Release-day data | event-driven (08:35 on release days) | BLS (bls), BEA (bea), Census (census), FRED release dates (fred), Cleveland Fed nowcast |
raw/releases/, curated/releases/series=/year=/, curated/macro_daily/
|
Surprise = actual − nowcast; revisions tracked → triggers trend-metrics, plus releases in serving |
Each new collector uses @job_handler("<ID>"), a new var.jobs entry, its own write_prefixes, and a dashboard-build trigger and read prefix. Suggested build order: stopgap "not set up yet" label → H1 → H3 → D5 → H2 → M1. M1 also needs a release calendar, and D1 (Morning macro & calendars) is unbuilt too.
All commands assume export AWS_PROFILE=invdash-admin AWS_REGION=us-west-1. If the SSO session has expired, run aws sso login --profile invdash-admin first.
Manually invoke a job
aws lambda invoke --function-name invdash-daily-prices --payload '{}' /tmp/daily-prices.json && cat /tmp/daily-prices.json
aws logs tail /aws/lambda/invdash-daily-prices --since 1hA manual run emits Job Finished just like a scheduled one, so status-feed and the downstream jobs (trend-metrics, dashboard-build) also run.
Backfill (base tickers plus ETF proxies from universe.py):
aws lambda invoke --function-name invdash-backfill --cli-binary-format raw-in-base64-out \
--payload '{"tickers":["TSLA","SPCX","SPY","DIA","QQQ","IWM","XLY","ITA","SMH"]}' /tmp/backfill-result.json
aws s3 cp s3://invdash-lake-308639168050/curated/prices_daily/_backfill/state.json -
aws logs tail /aws/lambda/invdash-backfill --followThe backfill is safe to re-run because it resumes from the checkpoint. Changing backfill_years while a load is active is rejected.
Rotating a key
AWS_PROFILE=invdash-admin infra/terraform/scripts/rotate-key.sh sam-gov- The script reads the value silently and sends it on stdin, so it never appears in shell history or the process list. It writes
/invdash/api-keys/<name>as a SecureString with the data KMS key. An unknown name lists the valid names. - The parameter's last-modified date counts as "last rotated". The
api-key-changedalert emails the security topic. -
Caching: Lambdas cache keys for about 5 minutes (
app/api_keys.py). Wait about 5 minutes after rotating before re-invoking, or the old value may still be used. - Provider rules:
sam-govexpires every 90 days,blsmust be renewed yearly, and Alpaca keys should be rotated every 180 days. See Data collectors API keys.
Alerts and monitoring
- SNS ops and security topics in the primary region, plus the CloudFront alarm topic in us-east-1.
- CloudWatch alarms and dashboards from the Terraform outputs.
- After two retries, a failed Lambda goes to its dead-letter queue.
- The key-rotation checker sends reminder emails.
- Logs are structured Powertools JSON lines. Filter on
job,run_idandevent: job_run.
-
Short interest isn't in serving.
short-interestwritescurated/short_interest/, butdashboard-builddoesn't read that prefix, sodashboard.jsonhas no short interest. Fix: add the read prefix and atickers[T].short_interestfield. -
SPCX pre-listing backfill shows partial. Yahoo has no history for SPCX before it listed, so that ticker's backfill and reconcile sources report a failure and the run is marked
partial(amber dot) even though nothing is broken. -
Short-interest error masking.
source_runswallows exceptions as a bulkhead. If the FINRA token or query fails,new_datesis never assigned, and the followingif new_dates:raises anUnboundLocalErrorthat hides the real FINRA error in logs and status. Fix: initialisenew_dates = set()before thewithblock. - prefs-api returns 500 when a serving file is missing. It should treat a missing serving object as empty/unavailable. New serving fields must be optional.
-
sec_user_agentmust be set. It defaults to"", which makeshttp_clientfall back to a placeholder User-Agent. SEC requires a real contact (investor-dashboard <contact email>in tfvars and theTERRAFORM_TFVARSsecret) before H3 ships. -
SAM.gov limits. The basic
sam-govkey allows 10 calls a day and expires every 90 days. D5 must budget about 3 calls per run and reportpartial(not crash) when the key has expired. -
Planned jobs look broken. H1/H2/H3/D5/M1 (and D1, D2, D3, W1) appear as "not yet collected" with a
next_run. A stopgap "not set up yet" label is proposed in #41.
Added Sat Oct 10, 2026, about 1:30 PM PT. All times are Pacific (PT). The facts come from the repo's issues, pull requests, commits and GitHub Actions runs unless a row says otherwise.
This section is the history of what went wrong after the collectors were first deployed, why, and what fixed it. It runs roughly in date order. Each entry links to the issue or pull request where the work happened, so you can read the full detail there.
| # | Problem | When | Fix | Status |
|---|---|---|---|---|
| 7.1 | Dashboard loaded but showed no data | Oct 6–7 | Ran the Yahoo backfill; real API keys installed with rotate-key.sh; loading and error states in the UI (#40) |
Fixed |
| 7.2 | Five feeds showed "not yet collected" (#41) | Oct 7 | Built H1, H2, H3, D5 and M1 (#42, #43, #47) | Fixed |
| 7.3 | New collectors never ran on schedule; then a deploy failed with AccessDenied | Oct 7 | Scheduler KMS decrypt, PassRole for the release-day job, admin apply of the boundary (#50) | Fixed |
| 7.4 | Trend metrics had no daily macro inputs and lost monthly releases (#26) | Oct 7–8 | Daily FRED collector D1 with 5 years of history; release series and release links (#55–#57, #66, #68, #70) | Fixed |
| 7.5 | Individual providers failing in production (#69) | Oct 8–10 | Per-source failure handling (#120) | PR open, not merged |
| 7.6 | Daily volume dropped 40–90x when Alpaca rows replaced Yahoo rows (#97) | Oct 10 | Store consolidated Yahoo volume for the last 10 sessions (#111) | Fixed |
| 7.7 | Site API returned bare HTTP 500s (#99), and the fix's alarm patterns then broke two deploys | Oct 10 | Structured error boundary (#114); JSON metric filters (#121) | Fixed |
| 7.8 | API responses uncompressed and close to growing past the 6 MB Lambda limit (#98) | Oct 10 | Gzip plus a response-size guard (#116) | Fixed |
| 7.9 | Pull-request CI runs started pointless Deploy runs | Oct 10 | Deploy only follows CI on main (#124) |
Fixed |
| 7.10 | Company page showed no company metrics (#126) | Oct 10 | SEC EDGAR collector (#132) | Deployed; metrics await approval |
| 7.11 | First company-metrics runs: 8-K 404s, partial run reported as success, stale cash series | Oct 10 | #135 | Merged and deployed; not yet re-run |
| 7.12 | Status feed shows the wrong next run for company IR collect | Oct 10 | None yet | Open |
What happened. The site and sign-in worked, but the dashboard had no prices or charts. Two things caused it (recorded on Deployment issues):
-
No backfill had ever run. The schedules alone don't load history for the starting tickers. The Yahoo backfill (
invdash-backfill) has to be invoked once, and it hadn't been after the jobs stack was restored. Soserving/dashboard.jsondidn't exist. -
All 14 provider keys in SSM were still the Terraform placeholder
REPLACE_ME. Alpaca, FINRA and every key-based collector would fail until real values went in.
The frontend made this harder to see. When /dashboard returned something it couldn't read, it quietly showed "nothing published yet" instead of an error.
Fix.
- The backfill was invoked once with the ticker payload. It needs no API keys, because Yahoo is public. The command is in section 5.
- Real keys were installed with
rotate-key.sh. Values never go into Git, the wiki or chat. - PR #40 added proper loading, error and "Try again" states, so a failed or slow read no longer looks like missing data.
What happened. The Data freshness panel showed "not yet collected" for Government contracts, Market prices, News & sentiment, Regulatory & company feeds, and Release-day data. The panel was working correctly. Those five job ids (D5, H1, H2, H3, M1) were listed in status_feed.JOBS, but no Lambda or Terraform job existed for any of them. Re-running jobs or rotating keys could never have fixed this. Issue #41 has the full design for each feed (summarised in section 4).
Fix. Each feed was built on its own branch, all on Oct 7:
| Feed | Job | Pull request | Outcome |
|---|---|---|---|
| Market prices (hourly) | H1 | #42 | Merged 5:09 AM. Alpaca 1-hour IEX bars, with Finnhub and Alpha Vantage as backups. Also added short_interest to the dashboard and a live last price with a 1D chart. |
| News & sentiment | H2 | #43 | Merged 5:44 AM. Finnhub, Alpha Vantage (capped at 16 calls a day), Massive and RSS. |
| Regulatory & company feeds | H3 | #44 | Closed. Landed through #47. |
| Government contracts | D5 | #45 | Closed. Landed through #47. |
| Release-day data | M1 | #46 | Closed. Landed through #47. |
| H3 + D5 + M1 together | — | #47 | Merged 6:16 AM. |
Merge conflicts and CI fixes along the way.
-
PR #43. Once H1 had merged, all the feed branches touched the same files (
dashboard_build.py,variables.tf,apps/web/app.js, the Dockerfile,pyproject.toml). Mergingmaininto the H2 branch left a duplicatedatetimeimport in the middle ofdashboard_build.py, and the CI lint step (ruff) failed with 4 errors (run). A follow-up commit repaired the merge, CI went green, and the PR merged. -
PRs #44, #45 and #46. Updating these onto
mainone at a time concatenated test functions and Terraform job blocks, which produced invalid Python and failed CI on all three branches. Rather than untangle three conflicting branches, #47 started fresh frommainand merged the last clean (green) commit of each feed. The three original PRs were closed.
What happened. Even after the five collectors were deployed, H1, H2, H3, D5 and M1 never ran on their schedules. 103 of 104 EventBridge Scheduler target invocations failed in 24 hours. The scheduler role couldn't decrypt the schedules, because it lacked kms:Decrypt on the project data key that encrypts them. Two smaller problems turned up at the same time:
- The release-day job (M1) creates its own one-off schedules, which means passing the
invdash-jobs-schedulerrole to EventBridge Scheduler. The workload permissions boundary didn't allow that. - USAspending returned HTTP 422, because the request was missing the required
award_type_codes.
Fix. PR #50 (merged Oct 7, 11:17 AM):
- gave the scheduler role
kms:Decrypton the data key; - added a
PassJobsSchedulerRolestatement to the boundary, which allowsiam:PassRoleoninvdash-jobs-scheduleronly, and only toscheduler.amazonaws.com; - made an M1 scheduler error count as
partialinstead of failed; - fixed the USAspending request (
award_type_codesand paging), keeping the watermark when a request fails.
The deploy failure. The first deploy of PR #50, Deploy production run #80 (11:20 AM), failed with:
AccessDenied:
invdash-terraform-deployis not authorized to performiam:CreatePolicyVersiononinvdash-workload-boundarywith an explicit deny
This is by design. The CI deploy role is never allowed to change its own permissions boundary, so a boundary change needs an administrator apply (the PR description said so). After the admin apply, the next run, #81 (11:24 AM), succeeded, finding the boundary already up to date. Note that #81 was the successful run, not the failed one.
What happened. The trend-metrics job (#26) could compute z-scores, correlations and net pressure, but it was missing inputs:
- no job collected the daily FRED macro series it expects;
- M1 wrote series ids the trend job didn't recognise, kept only the latest value, and dated each value to the start of its period, which leaks future information into the past.
The full plan is on Implementation plan: trend metrics serving layer (#26).
Fix.
| PR | Merged | What it did |
|---|---|---|
| #55 | Oct 7, 5:55 PM |
Phase 1 PR A: daily FRED collector (job D1, invdash-macro-daily). 13 daily series (Treasury yields, curve, real yield, breakevens, fed funds, SOFR, VIX, dollar index, WTI, policy uncertainty). Runs at 07:00 ET on weekdays and skips federal holidays. The first run backfills 5 years; later runs refetch from the watermark minus 10 days to pick up revisions. Writes curated/macro_daily/source=fred/ and triggers trend-metrics. |
| #56 | Oct 7, 6:00 PM | Phase 1 PR B: release series. CPI, core CPI, PCE and core PCE stored as year-over-year series dated on the publication date, with full history. Each release gets its own trend (accelerating or decelerating, and for how many releases). Records stock moves around each release (T-5..T-1, T-2..T-1, T0) and their correlation with the surprise. |
| #57 | Oct 7, 6:18 PM | Release links for every watchlist ticker, not only TSLA and SPCX. A backfill that wrote rows triggers a links-only M1 run that makes no provider calls. Stats stay empty until a ticker has 12 releases of history. |
| #66 | Oct 8, 12:05 PM | Shared series history, publication-date alignment, refresh paths, dashboard schema v3, and removal of phantom D2/D3/W1 status rows. |
| #68 | Oct 8, 5:16 PM | FINRA short interest: checks the token response, and a failure is now reported as a FINRA failure instead of an UnboundLocalError. Watermarks don't advance on failure. |
| #70 | Oct 8, 5:24 PM | The dashboard was looking for the wrong row type and hid the latest release facts. Monthly macro history is now rebuilt from stored releases. |
The production check recorded on #26 (Oct 8, about 5:36 PM) found trend-metrics ok, schema v3 live, CPI/core CPI/PCE latest releases present, nine release-window summaries, and core PCE explicitly missing rather than invented. That check confirmed the serving layer, not the health of every provider (see 7.5).
What happened. The CloudWatch review after #26 (Oct 7–8, us-west-1) found that several individual sources fail in production even though their jobs as a whole run. FRED, daily and hourly prices, news, regulatory feeds, fundamentals and options were healthy.
| Source | Failure seen | How PR #120 handles it |
|---|---|---|
| Yahoo (backfill) |
APPL returns 404 (a user typo for AAPL); SPCX returns 400 before its listing date; 429 when throttled |
404 keeps the symbol as typed and marks it done; 400 is accepted only for a genuine before-listing window; 429 stops the run as rate_limited
|
| Cleveland Fed nowcast | Page layout changed | New parser that keeps MoM, YoY and quarterly values apart |
| Census foreign trade | HTTP 400 | Request uses the published variables; values stored in millions of USD |
| BLS release calendar | 404 from Lambda, 403 locally | Uses the documented bls.ics; on 403/404 no dates are added and FRED fills the gaps |
| SAM.gov opportunities | HTTP 400 | Date format fixed; still one call within the 3-per-run, 10-per-day budget |
| DoD contracts | HTTP 403 | One RSS request only; article pages not fetched |
| USAspending | HTTP 422 | Field allowlist; the watermark doesn't advance on 422 |
| NASA | HTTP 429 | Recorded as throttling after retries |
| FINRA | Credential health unclear | 401/403 is reported as an authentication failure and the watermark doesn't advance |
Status. PR #120 is open, not merged (CI green, 310 tests). Issue #69 is open. Merging it doesn't fix production on its own: the affected collectors then have to be re-run within each provider's limits, and row counts checked. A Lambda success doesn't prove the data arrived.
What happened. The 5-year history from the Yahoo backfill is consolidated volume (all US venues). The daily Alpaca bars use Alpaca's free IEX-only feed, about 2–3% of consolidated volume. When Alpaca rows replaced Yahoo rows, volume fell 40–90x overnight for every ticker. For TSLA, 39.5 million shares on Sep 28 became 524,882 on Sep 29. The Volume lane collapsed and any volume-based metric was wrong. Details: #97.
Fix. PR #111 (merged Oct 10, 1:02 AM) kept Alpaca for prices but uses Yahoo for volume:
-
price-reconcilenow replaces volume for the last 10 sessions with Yahoo's consolidated number, from the chart it already fetches, so there are no extra calls. - Each bar carries
volume_source(DS-05 Yahoo or DS-02 Alpaca), and the IEX print is kept asvolume_iex. - The dashboard serves
volume_source_changesso a mixed series is visible.
After the fix, every one of the 10 sessions matched Yahoo exactly for all 7 watchlist tickers. Hourly and intraday volume are still IEX-only.
What happened. Some failures in the site API Lambda (prefs_api) escaped as a bare HTTP 500 with no JSON body and no structured log line: an STS hiccup, an S3 or KMS read error, a truncated dashboard.json, or an EventBridge error after a successful save (#99). The last of these was the worst: a preference that had been saved was reported to the user as a failure.
Fix, part 1. PR #114 (merged Oct 10, 12:58 AM) wrapped the handler in one error boundary:
- Every failure returns JSON
{error, code}:STS_UNAVAILABLE,SERVING_READ_FAILED,PREFS_STORE_UNAVAILABLE,SERVING_DOCUMENT_INVALIDorINTERNAL, with 503 for transient errors and 502 otherwise. - A saved PUT whose EventBridge publish fails now returns 200 with
backfill_queued: false. - The web app shows a specific message for each code.
- One CloudWatch metric filter and alarm per code.
The deploy failure. The metric filters used the pattern "event": "<event>" "code": "<CODE>". That isn't valid CloudWatch filter syntax. Deploy run #165 (1:01 AM) and #167 (1:08 AM) both failed with InvalidParameterException: Invalid metric filter pattern. Terraform had already updated the Lambdas but exited with an error, so the static site wasn't published.
Fix, part 2. PR #121 (merged 5:53 AM) switched to JSON filters, { ($.event = "<event>") && ($.code = "<CODE>") }, which match the one-JSON-object-per-line logs. It also added a test so the pattern stays valid. Deploy run #173 (5:57 AM) succeeded. No IAM change was needed: the error was a 400, not AccessDenied.
What happened. The static site is compressed by CloudFront, but /dashboard, /chart/{ticker} and /status come from an API Gateway HTTP API, which doesn't compress. The dashboard sent about 1 MB of JSON on every load (#98). The document also grows with the number of tickers, and a synchronous Lambda response over 6 MB fails outright.
Fix. PR #116 (merged Oct 10, 7:32 AM; deployed by run #178, 7:36–7:39 AM):
-
/dashboard,/chart/{ticker}and/statusreturn gzip (Content-Encoding: gzip,Vary: Accept-Encoding) when the client accepts it and the body is over 1 KB. Other clients still get plain JSON. -
dashboard-buildwrites pre-compressedserving/dashboard.json.gzandserving/chart_data/<T>.json.gz, and the Lambda caches gzip bytes by S3 ETag. -
Response-size guard: if the response would exceed 6 MiB after base64, the API returns 503
RESPONSE_TOO_LARGEinstead of a payload API Gateway would reject. - New metrics
DashboardRawBytesandDashboardGzipBytes, and the alarminvdash-dashboard-payload-sizefires above 4 MiB raw.
Live check, Oct 10, 2026:
| Route | Before (raw) | After (gzip on the wire) |
|---|---|---|
/dashboard |
about 2.99 MB | about 515 KB |
/chart/TSLA |
about 1.47 MB | about 248 KB |
/status |
3.3 KB | under 1 KB |
All three responses carried Content-Encoding: gzip. These live numbers come from a manual check on Oct 10 and aren't recorded in the PR or issue. The PR's own test used a smaller synthetic document. Note that the live dashboard (about 3 MB raw) is already well above the 1 MB in the Oct 7 snapshot, which is why the size guard and alarm matter.
What happened. Deploy production runs on workflow_run after CI. CI also runs on pull-request branches, so every PR push started a Deploy run that immediately skipped (for example runs #168–#172 and #174). That cluttered the Actions history and made real deploy failures harder to spot.
Fix. PR #124 (merged Oct 10, 6:45 AM) added branches: [main] to the trigger. Deploys still happen for pushes to main and for manual runs.
What happened. The per-stock company page (/#stock/TSLA, from #87) always said "No company-specific metrics discovered". The page, API route and IAM grant were live. What was missing was the data (#126):
-
The
company_ircollector was a placeholder. #87 shipped the page and a helper library, but the three job handlers were stubs (collect only planned, extract returned 0, serve returned 0). -
Every TSLA catalog metric was
proposed, so nothing would have shown even with data. - The manual fundamentals CSV had no deliveries or FSD subscriber rows. Q1 treated a missing CSV as "no rows" without a warning. The repo's example command also used 497,099 as Q3 2026 deliveries; that's Q3 2025. Q3 2026 was 486,532.
Fix. PR #132 (merged Oct 10, 10:01 AM; deployed by run #182, which started at 10:05 AM) built the collector on SEC EDGAR:
- Sources: submissions JSON, the 8-K Item 2.02 EX-99.1 exhibits (the deliveries press release and the quarterly update deck), the latest 10-Q/10-K, and XBRL companyfacts.
-
Tesla's websites aren't scraped.
www.tesla.comand their.tesla.comHTML pages return 403 to automated requests, and the collector doesn't work around that. A deck PDF on ir.tesla.com is a fallback only after checkingrobots.txt, and a 403 or 429 stops that host. -
Discovery and approval registry: a new row label in a known table becomes a
proposedentry incatalog/<TICKER>.json. Nothing is auto-approved. Approval goes throughscripts/approve_metric.py. The TSLA catalog now has 47 entries, allproposed. -
Dedupe priority: key
(ticker, metric_id, fiscal_period). XBRL wins for GAAP items. For operating items, the latest deck beats the press release. News never wins. A changed later value is kept as a revision (supersedes). Values about 100x off their neighbours are flagged as possible scale errors. - Once approved, deliveries and FSD subscriptions replace the manual CSV rows in Q1.
More detail is on Company Metrics Collector.
Two on-demand runs were made on Saturday Oct 10, after the deploy. The run details come from the Company Metrics Collector page and the run review. PR #135 confirms the problems found.
Run 1, scheduler payload ({"source":"schedule"}), about 12:04 PM: skipped. This is expected. plan_collection collects on a schedule only on Mondays (UTC date) or inside an earnings window (3 days before to 5 days after an earnings date). The scheduler payload carries no earnings dates and Oct 10 is a Saturday, so collect skipped before contacting EDGAR. Extract and serve skipped with "upstream skipped". No errors.
Run 2, weekly payload ({"reason":"weekly"}), about 12:06 PM: partial.
| Finding | Detail |
|---|---|
| 8-K exhibits all failed |
45 of 45 8-K index fetches returned 404. The code requested .../{accession}-index.json, but SEC serves .../index.json. So no deliveries releases or decks, and no operating metrics (deliveries, FSD). |
| Partial reported as success | Collect logged and published outcome: success even though the run status was partial. The status feed looked clean. |
| Cash series stale |
cash_and_investments mapped to an XBRL tag (CashCashEquivalentsRestrictedCashAndRestrictedCashEquivalents) that Tesla stopped filing after 2018Q3, so the series stopped there. |
| Possible scale errors |
16 older TSLA values were flagged: revenue_gaap for 2010–2012, two eps_diluted points and one deferred_revenue point. They stay flagged and aren't served. |
| What did arrive | Documents for 12 tickers (10-Q, 10-K, XBRL). For TSLA, 10 of 47 catalog metrics have XBRL data (356 rows), all awaiting approval, so serving/stock/TSLA.json has metrics: []. |
Fix. PR #135 (CI green; merged Oct 10, 12:23 PM; deployed by run #183, 12:27–12:30 PM):
- The 8-K index URL is now
index.json(verified 200 against a real Tesla filing). - Collect publishes
partialorfailureto match the run. Extract and serve still run after a partial collect, but not after a failed one. - Cash is now
CashAndCashEquivalentsAtCarryingValueplusShortTermInvestments(orMarketableSecuritiesCurrent) per quarter. That gives $43.524B for 2026Q2.
The scale-error check was deliberately left unchanged.
The Data refresh panel shows the next Company IR collect run as Tue Oct 13. The status feed's entry for Q2C uses the federal holiday calendar, so it skips Mon Oct 12 (Columbus Day). EventBridge Scheduler doesn't know about holidays. The real schedule, cron(15 7 ? * MON-FRI *) in America/New_York, fires on Mon Oct 12 at 4:15 AM PT. That's a Monday in UTC too, so the weekly collect will run. The panel is wrong; the job isn't. This hasn't been fixed yet.
| Gap in section 6 | Current status |
|---|---|
| Short interest isn't in serving | Fixed in #42: dashboard-build reads curated/short_interest/ and serves tickers[T].short_interest. |
Short-interest error masking (UnboundLocalError) |
Fixed in #68. |
| prefs-api returns 500 when a serving file is missing | Addressed by #114. Errors return structured JSON codes, and an unpublished dashboard returns 503 DASHBOARD_NOT_READY. |
| Planned jobs look broken | The five collectors and D1 now exist (7.2, 7.4). #66 removed the phantom D2/D3/W1 rows. |
| SPCX pre-listing backfill shows partial | Still true on main. Handled by the open #120. |
sec_user_agent must be set |
Not re-verified in this review. |
| SAM.gov limits | The collectors keep within the budget (#45/#47, #120). The 90-day key expiry still applies. |
-
Approve TSLA company metrics. All 47 catalog entries are
proposed, so the company page shows nothing. Approve withservices/data-jobs/scripts/approve_metric.py. An approval UI is tracked in #137. -
Confirm the PR #135 fixes in a real run. Nothing has been collected since the 12:30 PM deploy. The first scheduled weekly collect is Mon Oct 12, 4:15 AM PT. Check that 8-K exhibits are found, that deliveries and FSD rows appear, that the outcome is
partial/failurewhen appropriate, and that cash runs through 2026Q2. - Fix the status feed's next-run date for Q2C so it matches the scheduler (7.12).
- Review and merge PR #120 for the #69 provider failures. Then re-run the affected collectors within provider limits and check row counts.
-
Review the 16 flagged scale-error values (TSLA
revenue_gaap2010–2012,eps_diluted,deferred_revenue). -
Watch the dashboard payload size. The live raw
/dashboardis about 3 MB. The alarm fires at 4 MiB, and the hard limit is 6 MiB. Splittingdashboard.jsonper ticker is the longer-term fix suggested in #98. - Company page follow-up work (operating-KPI panels, empty states, theme parity) is tracked in #137. The per-stock vision is in #87. How the page works today: Company-specific ticker page.
- Implementation plan: trend metrics serving layer (#26) — daily FRED macro (D1), M1 release-series fix, trend-metrics refactor, triggers and schema v3.
-
Company-specific ticker page — the per-stock page that
serving/stock/<T>.jsonfeeds: panels, proposed vs approved, and the metric approval how-to.