Company specific ticker page - ram290476/investor-dashboard GitHub Wiki

Company-specific ticker page

The company page is the per-stock view of the dashboard. It lives at /#stock/<TICKER> (for example investor.vellamsetti.com/#stock/TSLA). Ongoing work on it is tracked in #137, which stays open as a living work item.

Sources: issues #87, #125, #126, #133, #137, #140 and #141; PRs #131, #134, #139, #142 and #143; and the code on main at d084b67 (Oct 10, 2026, 2:10 PM PT): apps/web/stock-page.js, apps/web/app.js (renderStockPage), apps/web/routes.js.

Status, Oct 10, 2026 (about 2:15 PM PT): the page is live, but it shows no company-collector metrics yet. Every catalog metric is still proposed, and main hides proposed metrics. PR #143, which would show them with a Pending review label, is open and not merged.

1. Purpose

From the per-stock vision in #87: give each watchlist stock its own page with the metrics investors actually watch for that company. Many of those aren't in standardized XBRL financials. For Tesla, that means FSD subscriptions, Semi, Robotaxi, Superchargers, energy storage and Optimus. The page also shows IR fundamentals and quarterly-call material. Every value must be source-backed (source URL, document hash, confidence and approval state). Missing quarters show "not reported", never zero, and nothing is shown until Ram approves it.

The company page (/#stock/<T>, any watchlist ticker) is separate from the older research page (/ticker/TSLA, /ticker/SPCX only), which still exists and has the Government contracts and About this data panels.

2. How to open the page

Way in Status on main
Enter on a focused ticker in the ticker bar Works (navigateToStock)
Long-press a ticker in the ticker bar (550 ms, LONG_PRESS_MS) Works on touch and mouse
URL /#stock/TICKER Works (parseStockHash)
URL /stock/TICKER Works. routeFromPath accepts it, and CloudFront serves index.html for unknown paths
Ticker-bar arrow Open TICKER page โ†’ Removed by PR #134 (#133 item 4)
Open company page link above the period selector Added by PR #134 (#133 item 5), then removed by PR #139

So there's no visible button on the main dashboard that opens the company page now. PR #139 left it to the owner to decide whether Enter, long-press and the URLs are enough. The โ†— arrow next to a ticker still exists, but it opens the research page (/ticker/<T>), not the company page.

Note: the #137 body still says the page opens from "Open company page" at the top right of the chart panel, and it lists the ticker-bar arrow as an owner observation. Both were overtaken by PR #134 and PR #139.

Signing in is required. The API (GET /stock/{ticker}) returns 401 without a token and 403 for a ticker that isn't in your watchlist.

3. Layout

Company page heading at 1440px, Industrial Dark: name and price on the left, Back to dashboard at the top right (before | after, #133 item 1)

Heading at 1440px, Industrial Dark, before and after #133 item 1 (PR #134 capture, fixture data, from docs/design-roadmap/issue-133/).

  • Heading (stock-heading): TICKER ยท Company name on the left, then the last price and day change, Updated <Mon D>, and a freshness line (see section 4). Company names come from the payload, or a built-in map (TSLA, SPCX), or else the ticker itself. Based on #125 and PR #131.
  • Back to dashboard: a native <a href="/">, keyboard-focusable, with a 44px target. It's pinned in the heading's top-right cell on desktop and at 390px (#133 item 1, PR #134). Back restores the dashboard's previously selected ticker.
  • Compact price chart (price-panel-compact) under the heading, using the same price panel as the dashboard.
  • Section panels: Operating KPIs, Fundamentals (titled "SEC filings" or "IR fundamentals"), and Quarterly call notes. Inside each section, metric cards sit two per row from 1024px and one per row below that. An odd last card stays on the left (#125, PR #131).
  • Phone tabs: below 640px the page shows tabs (Overview, KPIs, Fundamentals, Calls). They use arrow, Home and End keys and the compact --control-height button (PR #139).
  • Axes (#133 item 2, PR #134): the x-axis has one Q1โ€“Q4 label per quarter, with the year on its own line below. Narrow widths thin the quarter labels. The y-axis has rounded amount ticks with units ($1.2B, 45%, 400K vehicles).
  • No DS-11 line: the provenance/XBRL source line under each chart was removed from the UI (#133 item 3). The serving data is unchanged.
  • Theme (PR #139): the company-page panels use the Filings & events panel theme in all six themes: shared panel chrome, IBM Plex Sans, month-and-day dates (Oct 8), and QoQ/YoY shown as a colored dot plus Uptrend / Downtrend / Flat with the โ–ฒ/โ–ผ move. PR #139 also themed Government contracts and About this data, but those panels are on the dashboard and research page, not the company page.

Company metric card at 1440px, Industrial Dark: DS-11 line removed, Q1โ€“Q4 axis with years and y-axis units (before | after)

Metric card at 1440px, before and after #133 items 2 and 3. Fixture data: the 465,000 deliveries value is not a real figure.

Company page heading at 390px, Clean Light: Back to dashboard stays at the top right (before | after)

Heading at 390px, Clean Light. These captures are from before the PR #139 panel theming, so fonts and the freshness date format look slightly different now.

4. Panels

Today (on main)

Section What renders
Heading + price Price, day change, Updated date, freshness line
Operating KPIs Approved operating metrics from serving/stock/<T>.json, plus approved "unavailable" entries. Today: empty. It shows "No company-specific metrics discovered. Proposed metrics stay off this page until they are approved."
SEC filings (fallback for Fundamentals) With no approved IR fundamentals, the page falls back to the Q1 fundamentals series in the chart data: Revenue, Gross profit, Gross margin (SEC XBRL), Shares outstanding, Public float (SEC DEI), and Deliveries / FSD subscribers (reviewed manual CSV, DS-12, if any rows exist). Each series gets a card with QoQ. Once IR fundamentals are approved, the title changes to IR fundamentals
Quarterly call notes Approved guidance metrics (none yet), plus two notes: guidance appears after Ram approves it, and transcripts are collected only where the company hosts them or the license allows it

Each metric card shows the series (gaps filled as "not reported"), the latest value and period, QoQ/YoY, and, for collector metrics, the source link, published date, confidence and an XBRL reconciled or mismatch mark.

Freshness line states (freshnessBadge):

  • Loading company metrics
  • Add <T> to your watchlist to see company metrics (403)
  • No company-specific metrics discovered (404 or no payload)
  • Company metrics could not be loaded. Retry (5xx or network)
  • Partial run ยท previous approved values kept (stale, partial or failed run)
  • No approved company metrics (payload but nothing approved; the current state for TSLA)
  • IR data approved (when at least one metric is approved)

Planned

From #140 items 4โ€“5, implemented in open PR #143, and #137 / #87:

  • GAAP chart panels, one per collected metric with a time series (revenue, net income, operating income, R&D, SG&A, EPS, deferred revenue, operating cash flow, cash and investments, and more), with the #125/PR #139 layout and Q1โ€“Q4 axes. Panels come from each ticker's metric registry (catalog), not a hard-coded Tesla list. PR #143 adds chart, KPI, table and text panel kinds.
  • CapEx panel (#140 item 5): a quarterly chart for any company with a capex metric, with the latest QoQ and YoY marks, operating cash flow and free cash flow for context, and a revised-period mark where the definition changed (Tesla from 2025Q1). The #140 context gives TSLA 16 quarters through 2026Q1, at $2,493M.
  • Unique quarterly-report panels as KPI tiles, tables, status lists or short text cards, with quarter and source. For Tesla:
    • deliveries and production by model
    • active FSD subscriptions (1.48M in the Q2 2026 deck)
    • energy storage deployed (13.7 GWh in Q3 2026)
    • Supercharger stations and connectors
    • Robotaxi metros and status
    • Optimus production lines and milestones
    • Tesla Semi commissioning and production status
    • every factory being deployed, with its products, installed capacity (for example California Model 3/Y >550,000, Shanghai >950,000, Berlin Model Y >375,000, Texas Model Y >250,000) and status
    • AI compute (Cortex 1 >90 MW, Cortex 2 >115 MW)
    • 4680 and LFP cell capacity
    • outlook and product milestones
  • SpaceX (SPCX) (#141): segment revenue (Space, Connectivity, AI), segment and total Adjusted EBITDA, total capex, launches, mass to orbit, Starlink subscribers and ARPU, AI nameplate compute, backlog and Starship flights.
  • Nvidia (NVDA) (#141): Data Center revenue (Hyperscale vs AI Clouds, Industrial & Enterprise), Edge Computing, Compute & Networking vs Graphics, gross margin, next-quarter revenue and margin outlook, DSO, buybacks and dividends, and the China/export-control note. Nvidia's fiscal year ends in late January.
  • Other watchlist tickers: the #141 comment proposes lists for AAPL, AMZN, BYND, EVGO, GOOG, META, MRVL, PLTR and QS (for example AWS, Search/YouTube/Cloud, daily active people, EVgo stalls, Palantir US commercial and government revenue).
  • Sustainability (Impact) report and other company reports (#140): sustainability metrics, energy deployment trends and Supercharger network growth. The Impact Report is annual and on tesla.com, which returns 403 to automated requests. It needs an allowed source (a PDF link, SEC exhibit or manual upload), and the collector must not work around the block.
  • Fundamentals overlay on the main dashboard fed by the same collected data (#140 item 4).
  • From #87 (later phases): Guidance & Outlook, Quarterly Call Notes from licensed transcripts, and XBRL reconciliation flags in the IR fundamentals panel.

5. Data flow

flowchart LR
  E[SEC EDGAR<br/>submissions, 8-K EX-99.1 / EX-99.2,<br/>10-Q / 10-K, XBRL companyfacts] --> C[Q2C collect<br/>invdash-q2-company-ir-collect]
  C --> R[(raw/company_ir/TICKER/PERIOD/<br/>docs + manifest, SHA-256)]
  C -- Job Finished --> X[Q2X extract]
  R --> X
  K[catalog/TICKER.json<br/>metric registry] --> X
  X --> CU[(curated/company_metrics/ticker=TICKER/<br/>Parquet)]
  X -- Job Finished --> S[Q2S serve]
  CU --> S
  K --> S
  S --> SV[(serving/stock/TICKER.json)]
  SV --> API[prefs-api GET /stock/TICKER<br/>Cognito, watchlist-scoped]
  API --> P[Company page<br/>stock-page.js / renderStockPage]
Loading
  1. Collect (Q2C). Runs weekdays on cron(15 7 ? * MON-FRI *), but plan_collection only collects on Mondays (UTC) or inside an earnings window (T-3 to T+5), and on ticker-added. EDGAR is the primary source and uses the InvestorDashboardIR/1.0 (<SEC_USER_AGENT>) identity under 10 requests/second. www.tesla.com and the ir.tesla.com HTML pages aren't fetched (403). Since PR #142, exhibits are found by SEC document type from the filing -index.html or the <TYPE> in the full submission, and both EX-99.1 and EX-99.2 are kept.
  2. Extract (Q2X). Parses EX-99.1 tables and, since PR #142, flowing-text KPI rows (Tesla deck, SpaceX release). It reconciles GAAP overlaps against XBRL (0.5% tolerance), differences year-to-date cash-flow items into quarters, and derives the fiscal period from the release text and the fiscal-year end (Nvidia 0131). Rows land in curated/company_metrics/ keyed by (ticker, metric_id, fiscal_period). LLM extraction is off (COMPANY_IR_LLM_SECRET_ID unset).
  3. Serve (Q2S). Builds serving/stock/<T>.json. On main it includes approved metrics only. A partial or failed run keeps the previous approved values. Scale-warning flags are now scoped per ticker (PR #142).
  4. API and page. GET /stock/<T> reads the serving file. A missing file returns "No company-specific metrics discovered". The page renders what it gets.

More detail: Company Metrics Collector and Data collectors ยง7.10โ€“7.12.

Proposed vs approved, and Pending review

  • Metric registry: services/data-jobs/src/functions/company_ir/catalog/<T>.json. Each entry has metric_id, display name, unit, definition, category, aliases, status (proposed / approved / rejected), approved_by and approved_at. load_catalog merges a generic GAAP catalog into every ticker, so tickers without a file still keep their XBRL rows (PR #142).
  • Current state on main: TSLA has 47 entries, SPCX 15 and NVDA 11. All are proposed. Nothing is auto-approved. LLM-only values can never be approved.
  • What main shows: approvedMetrics() drops anything proposed or rejected, so the page shows "No approved company metrics", and the Fundamentals section falls back to the Q1 "SEC filings" series.
  • Pending review (#140 / PR #143, open): serve and the API would pass proposed metrics through with their status, confidence and source. Approved values come first. Proposed values get a Pending review badge, and an Approved only switch hides them. LLM and rejected rows stay out.

6. Current limitations and open items

Item Status Links
No company-collector metrics on the page, because every catalog entry is proposed Waiting on Ram's approvals or PR #143 #126, #141
Pending review panels, CapEx panel, unique report panels, Fundamentals feed, Data refresh pop-up, chart toolbar PR #143 open, not merged. CI validate passed, but GitHub reports merge conflicts with main and there are no reviews. Screenshots are "being added" #140, PR #143
Exhibit matching, release classification, fiscal quarters, flowing-text parsing, per-ticker scale flags, SPCX/NVDA catalogs, YTD differencing PR #142 merged Oct 10, 2:10 PM PT, with no recorded GitHub review. Its own body said "Does not merge". The production deploy of d084b67 started at 2:14 PM PT PR #142, #141
Tesla operating KPIs (deliveries, FSD, storage, Superchargers) not collected yet Expected from the first weekly collect after PR #142 (Mon Oct 12, 4:15 AM PT). Not confirmed #141
Narrative items (Optimus, Semi, guidance, SpaceX backlog, Starship) Catalogued as proposed, not extracted as numbers. Tesla capacity lower bounds in the real deck are prose PR #142 "Limits"
Chart-only items (FSD miles, Robotaxi miles) and the Impact report No source path. tesla.com returns 403 #140, #141
9 other watchlist tickers have no company-specific catalog Proposed lists in the #141 comment. Generic GAAP rows only #141 comment
No visible dashboard entry point Owner decision pending PR #139
Empty, loading and freshness states; theme parity in six themes; 1440/390 checks Open in #137 #137
Approval UI (admin-only Approve/Undo on the page, S3 catalog override) Shipped in PR #159 (Oct 11). Approve not changing state on the live site is fixed in PR #167 (merged Oct 11, 4:40 PM PT, deploy pending). See 8. Company metric approval #126, #147, #166
SPCX IPO date: the owner said July 12, but EDGAR shows June 12 (424B4) and the IPO closed June 15 Needs owner confirmation #141
/ticker/<T> research page still separate (TSLA and SPCX only) Not consolidated #87

7. How to approve a metric

Since PR #159 (Oct 11), an admin can approve or undo a metric from the company page. See 8. Company metric approval. The CLI below still works and is the only way to reject a metric.

services/data-jobs/scripts/approve_metric.py exists on main. It wraps set_catalog_status and validate_catalog. See services/data-jobs/src/functions/company_ir/README.md.

  1. Pick the metric from catalog/<T>.json. TSLA ids include total_deliveries, fsd_subscriptions, energy_storage_deployed, supercharger_stations, capex, revenue_gaap and operating_income. Approve only metrics you've checked against the source. No metric may be approved without Ram's explicit decision.
  2. On a branch, from the repo root:
    python services/data-jobs/scripts/approve_metric.py TSLA total_deliveries --by ram
    # reject instead:
    python services/data-jobs/scripts/approve_metric.py TSLA total_deliveries --by ram --status rejected
    # back to proposed:
    python services/data-jobs/scripts/approve_metric.py TSLA total_deliveries --status proposed
    --by is required to approve. approved_at defaults to now (UTC), or pass --at <ISO timestamp>. Rejecting clears the reviewer fields. The script fails if the ticker has no catalog file.
  3. Or edit the JSON by hand: set status to approved and fill approved_by and approved_at.
  4. Open a PR. CI fails if an approved entry has no approved_by. Merging to main deploys the catalog with the job image.
  5. The next Q2S serve writes the metric into serving/stock/<T>.json, and it appears on /#stock/<T>. Serve runs after extract, after collect.
  6. For a ticker without a catalog, run python services/data-jobs/scripts/propose_catalog.py <T> --fixtures <exhibit.html> --write, review the proposed file, and open a PR. That script never approves and doesn't open the PR itself.

8. Company metric approval

Sources: PR #159 (merged Oct 11, 11:41 AM PT), issues #147, #161 and #166, and the #166 UI PR PR #167. All code references are to main at a39b5a7 (Oct 11, 2026, 4:40 PM PT), which already includes PR #167.

Status, Oct 11, 2026 (about 4:45 PM PT): the in-app approval workflow from PR #159 is deployed (Deploy production #202, 11:45 AM PT). Clicking Approve on the live site has not been changing the state (#166 item 5). The fix is in PR #167. It was expected to stay open for owner review, but it was merged at 4:40 PM PT and is no longer open. Its CI run was still in progress at the time of writing. Production deploys only after CI passes on main, so the fix is not live yet. See 8.7 Known issue.

8.1 Purpose

The company-metrics collector (Q2C collect โ†’ Q2X extract โ†’ Q2S serve, see section 5) only proposes metrics. A collected metric stays proposed (shown as Pending review) until a person approves it. Approval is the human gate between "the collector found this number" and "the dashboard treats it as a fact":

  • Approved metrics are listed as approved in the Company metrics panel, and they can be added to the main dashboard's Fundamentals chart overlay group.
  • Pending review metrics still draw as charts on the company page, but they never show up in the main dashboard Fundamentals overlay.
  • A later collect can add new proposed metrics. It never clears an approval (see 8.6).

Before PR #159, approving meant editing catalog/<T>.json in a PR (the CLI in section 7). Now an admin can approve from the company page.

8.2 Statuses

Status in the catalog Shown as What it means How you get there
proposed Pending review badge in the Company metrics list The collector found it; nobody has checked it Default for every collected metric; Undo returns an approved metric here
approved Approved label in the list An admin checked it against the source. approved_by and approved_at are recorded Approve button (admin), or approve_metric.py
rejected Hidden everywhere Not a metric to show approve_metric.py --status rejected only. The API doesn't accept rejected
  • Undo sets an approved metric back to proposed and clears approved_by and approved_at.
  • Approval is per metric and per ticker (metric_id in catalog/<T>.json), not per quarter.
  • LLM-only values can never be approved (catalog validation, section 5).

8.3 Who can approve

  • Only members of the Cognito group admin in user pool us-west-1_6ZwMxHaH2. Terraform creates the group empty (site_auth/main.tf:70โ€“78, aws_cognito_user_group.admin). Nobody is added in Terraform.
  • The public demo account [email protected] is always refused, even if it's added to the group. This is checked both in the browser (canApproveMetrics) and in the API (_is_demo_account). Don't add it.
  • Everyone else sees the status (Pending review / Approved) but no buttons. If they call the API directly, they get 403.

Add the owner's account to the admin group (one time, with AWS credentials that can manage the pool):

aws cognito-idp admin-add-user-to-group \
  --region us-west-1 \
  --user-pool-id us-west-1_6ZwMxHaH2 \
  --username <your-email> \
  --group-name admin

# check it
aws cognito-idp admin-list-groups-for-user \
  --region us-west-1 \
  --user-pool-id us-west-1_6ZwMxHaH2 \
  --username <your-email>

Use your own sign-in email (or the Cognito username), not the demo account. Then sign out and sign in again. Group membership is copied into the access token's cognito:groups claim only when a token is issued, so a session that started before you were added won't show Approve.

8.4 Where it is in the UI

Company page /#stock/<TICKER>, from top to bottom (app.js:3676โ€“3687):

  1. Header (company name and Back to dashboard)
  2. Compact price chart
  3. Show pending review / Approved only toggle, then Operating KPIs
  4. Fundamentals (or SEC filings)
  5. Company metrics: the list with Approve and Undo (#161 item 1, PRs #160 and #162)
  6. Quarterly call notes (last)

On a phone (below 640px) the Company metrics panel is in the Overview tab, and the toggle is at the top of the KPIs tab.

Company metrics panel

  • Approved rows come first, each with an Approved label. Under a Pending review heading come the proposed rows, each with a Pending review badge. The row text shows the latest value and quarter (for example Q3 2026), with the period-end date on hover (#166 item 8).
  • An admin sees Approve on pending rows and Undo on approved rows. While a save is running, every button reads Saving and is disabled. Rows marked unavailable get no button.
  • Non-admins see only the labels.
  • If the save fails, a red message (role="alert") appears at the top of this panel and stays there (#166 item 5, PR #167).

Show pending review / Approved only (above Operating KPIs)

  • Show pending review (default): every metric panel on the page shows approved and pending-review metrics (Operating KPIs, Fundamentals, guidance, CapEx, and the Company metrics list).
  • Approved only: those panels and the list show approved metrics only. A section that ends up empty shows the themed empty state.
  • The selected button is highlighted (aria-pressed="true"). The choice is saved in this browser only (localStorage), not in your account preferences.
  • Chart cards don't carry a Pending review badge or an approved/proposed pill (#147 item 5). The list is where status is shown.

Main dashboard Fundamentals overlay

  • The Fundamentals group in the main chart's overlay picker lists approved metrics only (#147 item 6). The old <id>_pending "Pending review" chips are gone.
  • Approving a metric adds FUNDAMENTAL:<metric_id> to that group without waiting for a collect. Undo removes it.
  • A saved overlay that still points at a pending id is dropped quietly (on the chart and when preferences are read or saved). Saving still returns 200.

Screenshots: docs/ has no screenshots of the approval UI yet. The PR #159 and PR #167 captures are attached to those PRs as agent artifacts, not committed. The metric-card screenshot in section 3 shows the card style.

8.5 End-to-end data flow

flowchart TD
  U[Admin clicks Approve / Undo<br/>Company metrics panel] --> H[submitMetricReview<br/>apps/web/app.js]
  H -->|"PUT /stock/T/metrics/ID<br/>Authorization: Bearer access token<br/>body: status approved or proposed"| G[API Gateway HTTP API<br/>invdash-site-api<br/>JWT authorizer: Cognito]
  G -->|AWS_PROXY 2.0| L[prefs_api Lambda<br/>invdash-prefs-api]
  L --> A{"_claims_for_review + _is_metric_admin<br/>cognito:groups contains admin,<br/>not the demo account"}
  A -- no --> F[403 Only an admin can change<br/>metric review status]
  A -- yes --> W{"ticker in your watchlist?<br/>metric in serving/stock/T.json?"}
  W -- no --> E[403 / 404]
  W -- yes --> C[(S3 lake: catalog/T.json<br/>status, approved_by, approved_at)]
  C --> S[(S3 lake: serving/stock/T.json<br/>+ T.json.gz, no-store)]
  S --> R[200: public stock document]
  R --> UI[session.stockPage = response<br/>renderDashboard: row shows Approved,<br/>Fundamentals overlay gains the metric]
  subgraph Next collect
    Q2C[Q2C collect] --> Q2X[Q2X extract<br/>review_catalog] --> Q2S[Q2S serve<br/>review_catalog, build_serving]
  end
  C -. read, approval kept .-> Q2X
  C -. read, approval kept .-> Q2S
  Q2S -->|rewrites JSON + .gz| S
Loading
  1. Button. submitMetricReview(metricId, "approved" | "proposed") sends the PUT with the Cognito access token.
  2. API Gateway. Route PUT /stock/{ticker}/metrics/{metric_id} on the HTTP API, behind the Cognito JWT authorizer. A missing or invalid token gets 401 before the Lambda runs.
  3. prefs_api admin check. Reads cognito:groups and refuses non-admins and the demo account (403).
  4. Catalog write. Read-modify-write of the lake review record catalog/<T>.json: status, approved_by (your email) and approved_at (UTC, ISO 8601).
  5. Serving update. The same metric is patched in serving/stock/<T>.json (and its .gz copy), so GET /stock/<T> reflects it right away.
  6. UI refresh. The 200 body is the patched company document. The page stores it and redraws, without a separate GET.
  7. Collect / extract / serve. Each job reads the lake catalog with review_catalog, so a lake approved (or an Undo back to proposed) wins over the packaged catalog in the job image. preserve_approved_status and merge_proposals never downgrade an approved entry. Serve rewrites the JSON and the .gz together.

8.6 Implementation

Frontend (apps/web)

What Where
Session state: stockPage, stockPageState, metricReview (toggle), metricReviewError app.js:131โ€“136
apiGet / apiPut: fetch with Authorization: Bearer ${session.accessToken}, Content-Type/Accept: application/json, cache: "no-store" app.js:561โ€“595
Error decoding: readApiResponse throws ApiError with the server's error text, or a friendly message for 502/503 codes api-response.js:34โ€“58
Who sees the buttons: canApproveMetrics(accessToken, email) decodes the access token, refuses the demo email, and looks for admin in cognito:groups settings-model.js:149 (DEMO_METRIC_ADMIN), settings-model.js:170โ€“178
Toggle storage: readStoredMetricReview / storeMetricReview, localStorage key investor-dashboard.metric-review ("approved" or "all") app.js:3144โ€“3159
Toggle UI: renderMetricReview (group of two metric-review-option buttons with aria-pressed) app.js:3161โ€“3179
Toggle placement: added only to the kpis section, above its title app.js:3657
Approve/Undo handler: submitMetricReview(metricId, status) app.js:3504โ€“3531
Company metrics panel renderer: renderCompanyMetrics(panels, pageState) (rows, Approved / Pending review, Approve/Undo buttons, error alert) app.js:3533โ€“3588
Error focus: focusMetricReviewError scrolls the alert into view app.js:3498โ€“3502
Page renderer and panel order: renderStockPage passes review into stockPanels app.js:3600โ€“3617, app.js:3676โ€“3687
Status helpers: isPendingMetric, approvedMetrics, pendingMetrics, metricsForReview(payload, review) stock-page.js:162โ€“192
Panel builder: stockPanels(payload, chartData, {review, period}) filters every section by the toggle stock-page.js:496โ€“535
Dashboard Fundamentals overlay (approved only): selectableOverlay rejects *_pending; rememberFundamentals skips _pending and proposed rows; mergeCompanyFundamentals adds only approved company metrics as FUNDAMENTAL:<id> rows chart-overlays.js:57โ€“66, chart-overlays.js:73โ€“101, chart-overlays.js:103โ€“139
Call site in renderPricePanel feeding company metrics into the overlay app.js:2050โ€“2053
Server-side overlay rule (saved prefs): overlay_allowed, visible_overlays drop *_pending prefs_api.py:148โ€“157

UI state after the response: not optimistic. On click, submitMetricReview sets session.metricSave = true and redraws, so the buttons show Saving and are disabled. That flag also blocks a second click. It then awaits apiPut. On 200 it replaces session.stockPage[ticker] with the response body (the server's patched, filtered document) and redraws, so the row moves to Approved and the dashboard overlay group is rebuilt from the same document. There is no follow-up GET; a page reload reads the patched serving file. On error it keeps the old document and puts the error text in session.metricReviewError, which renderCompanyMetrics draws as a role="alert" .metric-review-toast that stays across redraws. If the browser decides you're not an admin, the handler shows "Only an admin can change metric review status." and doesn't call the API.

API contract

PUT {apiBaseUrl}/stock/{ticker}/metrics/{metric_id}
Authorization: Bearer <Cognito access token>      (an ID token is also accepted by the authorizer)
Content-Type: application/json

{"status": "approved"}     or     {"status": "proposed"}   (Undo)
Code Body When
200 Public company document, same shape as GET /stock/<T>: ticker, generated_at, run_status, stale, freshness_label, discovered, metrics[] (approved first; each with status, approval_state, approved, approved_by, approved_at), unavailable, mismatches (_public_stock, prefs_api.py:370โ€“391) Saved
400 {"error": "Metric id is not valid"}, "Body must be a JSON object", or "status must be approved or proposed" Bad id, body, or status
401 API Gateway {"message":"Unauthorized"}, or {"error":"Not signed in"} from the Lambda when there's no sub Missing/expired token
403 {"error": "Only an admin can change metric review status"} Not in admin, or the demo account
403 {"error": "Ticker is not in your watchlist"} Admin, but the ticker isn't in your watchlist
404 {"error": "Metric is not on this company page"} serving/stock/<T>.json is missing or doesn't contain that metric_id
405 {"error": "<METHOD> not allowed"} Any other method on the path
409 not used This route has no conflict check. 409 belongs to PUT /prefs only
502 / 503 {"error": "The metric review could not be saved. Try again in a minute.", "code": "CATALOG_WRITE_FAILED"} s3:PutObject failed. 503 for throttling/transient codes, else 502 (prefs_api.py:571โ€“585)
502 / 503 code: "SERVING_READ_FAILED" GetObject of the serving file or the catalog failed (including KMS)
503 code: "SERVING_DOCUMENT_INVALID" Serving file or catalog isn't a JSON object
500 code: "INTERNAL" Anything else (for example LAKE_BUCKET unset)

Every response carries Cache-Control: no-store. 5xx errors are logged as one JSON line {"event":"prefs_api_error","route":"PUT /stock/{ticker}/metrics/{metric_id}","method":"PUT","error_class":โ€ฆ,"code":โ€ฆ} with no body or token (prefs_api.py:534โ€“542, handler prefs_api.py:1025โ€“1044). 4xx responses are not logged by the Lambda.

prefs_api handler (services/data-jobs/src/functions/prefs_api/prefs_api.py)

  1. Admin group and demo account. ADMIN_GROUP (env METRIC_ADMIN_GROUP, default admin) and DEMO_ACCOUNT (prefs_api.py:63โ€“65).
    • _claims_for_review (prefs_api.py:708โ€“732) starts from the authorizer claims (_jwt_claims, prefs_api.py:664โ€“672). The HTTP API JWT authorizer passes claims as strings and drops array claims, so cognito:groups can be missing. In that case it decodes the bearer token's payload (_bearer_claims / _unsigned_jwt_payload, prefs_api.py:685โ€“705; the signature was already checked by API Gateway). It uses that token's cognito:groups (and email/username if missing) only if its sub equals the verified sub and token_use is access or id.
    • _claim_groups (prefs_api.py:735โ€“746) accepts a list or string forms such as admin, [admin], [admin users].
    • _is_demo_account / _is_metric_admin (prefs_api.py:758โ€“766): the demo email in email, username or cognito:username always fails; otherwise admin must be in the groups.
    • _reviewer (prefs_api.py:769โ€“774) picks the value written to approved_by: your email/username (never the demo account), else sub.
  2. Validation in _review_metric: admin โ†’ watchlist (get_prefs) โ†’ JSON object body โ†’ status in {approved, proposed} โ†’ metric present in the current serving document.
  3. Catalog read-modify-write. _read_json_object("catalog/<T>.json") (prefs_api.py:781โ€“793). If it's missing it starts from {"ticker", "catalog_version": 1, "metrics": []}. _set_catalog_metric (prefs_api.py:823โ€“841) sets status and, for approved, approved_by and approved_at (datetime.now(UTC), seconds); for proposed it clears both. The lake catalog/<T>.json is the review record. The metric definitions stay in the packaged catalog in the job image (catalog_object_key docstring).
  4. Serving patch. _patch_serving_metric (prefs_api.py:862โ€“888) sets approved, approval_state, status, approved_by, approved_at and provenance.approval_state on that metric, re-sorts approved first, and recomputes freshness_label when run_status is ok.
  5. Writes. _write_json_object (prefs_api.py:796โ€“820) writes catalog/<T>.json, then serving/stock/<T>.json and, with gzip_sibling=True, serving/stock/<T>.json.gz (gzip.compress, same level, mtime=0). All with CacheControl: no-store (#166 item 5). GET /stock/<T> reads the JSON (prefs_api.py:984โ€“993); the .gz is kept in step so no reader of the sibling gets the old pending document.
  6. Concurrency. There is no ETag / If-Match / conditional put on these objects. Two simultaneous reviews of the same ticker are last-writer-wins for the whole catalog/<T>.json and serving file. The only guard is the browser's one-save-at-a-time flag. The catalog is written before the serving file, so if the second write fails, the approval is still recorded and the next serve publishes it.

S3 keys (lake bucket, env LAKE_BUCKET)

Key Written by Read by
catalog/<T>.json prefs_api (review); not by any job prefs_api; Q2X extract and Q2S serve via _read_lake_catalog
serving/stock/<T>.json prefs_api (patch); Q2S serve prefs_api GET /stock/<T> and the PUT
serving/stock/<T>.json.gz prefs_api (patch); Q2S serve gzip sibling, kept in step
services/data-jobs/src/functions/company_ir/catalog/<T>.json (repo, packaged in the image) PRs / approve_metric.py the jobs' load_catalog, then overlaid by the lake record

Terraform (infra/terraform/modules/user_prefs/main.tf, site_auth/main.tf)

Resource What it does Where
aws_cognito_user_group.admin Empty admin group in the site pool site_auth/main.tf:70โ€“78
aws_apigatewayv2_api.site (invdash-site-api) HTTP API; CORS GET, PUT; headers authorization, content-type, accept user_prefs/main.tf:331โ€“341
aws_apigatewayv2_authorizer.jwt Cognito JWT authorizer on $request.header.Authorization user_prefs/main.tf:343โ€“353
aws_apigatewayv2_integration.prefs AWS_PROXY, payload 2.0, to the prefs Lambda user_prefs/main.tf:355โ€“360
aws_apigatewayv2_route.site["PUT /stock/{ticker}/metrics/{metric_id}"] The route, authorization_type = "JWT" user_prefs/main.tf:362โ€“369
aws_lambda_permission.api_route["stock_metric"] Invoke permission for stock/*/metrics/* user_prefs/main.tf:391โ€“405
aws_lambda_function.api (invdash-prefs-api) Handler prefs_api.handler, env LAKE_BUCKET user_prefs/main.tf:292โ€“325
aws_iam_role_policy.api / data.aws_iam_policy_document.api ReadDashboardServingObjects (s3:GetObject incl. serving/stock/*), ReadMetricCatalog (s3:GetObject catalog/*), WriteMetricReview (s3:PutObject on catalog/* and serving/stock/* only; no delete) user_prefs/main.tf:224โ€“247
aws_cloudwatch_log_metric_filter.prefs_api_error["CATALOG_WRITE_FAILED"] + alarm Alarm "Site API could not save a company-metric review" user_prefs/main.tf:450โ€“453, user_prefs/main.tf:461โ€“472

No new IAM role. The writes use the existing prefs API Lambda role. The jobs' own permissions are separate: extract and serve read catalog/; serve writes serving/stock/.

How collect / extract / serve keep the approval (company_ir.py)

Tests

Test Covers
services/data-jobs/tests/test_prefs_api.py::test_admin_approval_writes_the_lake_catalog_and_patches_serving (L243) Catalog + serving writes, audit fields
โ€ฆ::test_access_token_groups_authorize_when_the_authorizer_omits_the_array (L282) #166 item 5 root cause
โ€ฆ::test_bearer_groups_are_ignored_for_a_different_sub_or_the_demo_account (L315) Token-mismatch and demo refusal
โ€ฆ::test_metric_review_rejects_non_admins_the_demo_account_and_bad_requests (L345) 403 / 400 / 404
โ€ฆ::test_catalog_write_failure_is_a_named_api_error (L370) CATALOG_WRITE_FAILED, log line
services/data-jobs/tests/test_prefs_api_metric_filters.py::test_metric_review_route_cors_integration_and_iam_prefixes (L39) Terraform route, JWT, integration, CORS, IAM prefixes
services/data-jobs/tests/test_company_ir.py::test_a_collect_does_not_clear_an_approved_metric (L131) preserve_approved_status, merge_proposals
โ€ฆ::test_serve_handler_rewrites_the_stock_json_and_gzip_sibling (L160) JSON + .gz, cache_seconds=0
โ€ฆ::test_lake_catalog_approval_is_served_even_when_the_extracted_row_is_unapproved (L169) review_catalog
apps/web/settings-model.test.js "only a non-demo admin group can approve company metrics" (L136) canApproveMetrics
apps/web/panel-theme.test.js "chart cards omit review badges and the review toggle sits above Operating KPIs" (L98) Toggle placement, metric-review-toast, submitMetricReview
apps/web/stock-page.test.js "review mode shows or hides operating, guidance, and CapEx panels together" (L282) Toggle filtering
apps/web/chart-overlays.test.js "fundamentals overlays keep approved company metrics and drop pending series" (L52) Approved-only dashboard overlay

Run: node --test apps/web/*.test.js; cd services/data-jobs && pytest -q.

8.7 Known issue: Approve does not change the state on the live site

Symptom (#166 item 5): on the live company page, an admin clicks Approve, the row stays on Pending review, and no error is shown.

Fix: PR #167, item 5 of #166. It was meant to stay open for owner review, but it was merged Oct 11, 4:40 PM PT (a39b5a7). Its deploy runs only after CI passes on main. Until that deploy finishes, the live site still has the bug.

Root cause (from PR #167):

  1. The admin check missed the group. The page decides to show Approve by decoding the access token's cognito:groups array. But the API Gateway HTTP API JWT authorizer passes claims to the Lambda as strings and drops array claims. The old _is_metric_admin read only requestContext.authorizer.jwt.claims, so a real admin had no groups and got 403 "Only an admin can change metric review status". Fix: _claims_for_review reads cognito:groups from the same bearer token when the authorizer omitted it, only if sub matches and token_use is access/id. The demo account is still refused.
  2. The failure was invisible. Errors went through notify(), a line at the top of the page, which the next renderStockPage wiped. Fix: the error is drawn inside the Company metrics panel (role="alert") and kept across redraws.
  3. Stale gzip copy. The write patched serving/stock/<T>.json but not serving/stock/<T>.json.gz, and set no cache header. Fix: both are written with Cache-Control: no-store, and Q2S serve rewrites both (cache_seconds=0).
  4. CORS allow_headers gained accept, matching what the page sends.

The route, Lambda permission and IAM write permission were already correct after PR #159. PR #167 notes that its environment had no AWS credentials, so the live API Gateway stage wasn't inspected.

8.8 Troubleshooting

Symptom Check / fix
No Approve or Undo buttons You aren't in admin, or your token is older than the group change. Run admin-list-groups-for-user (8.3), then sign out and in. The demo account never gets buttons.
Red message "Only an admin can change metric review status" 403. Before PR #167 is deployed, this happens even for real admins (see 8.7). After it, check the group and the account as above.
"Ticker is not in your watchlist" 403. Approvals are watchlist-scoped. Add the ticker under Account โ†’ My tickers.
"Metric is not on this company page" 404. The metric isn't in serving/stock/<T>.json yet. Wait for the next Q2S serve.
"The metric review could not be saved. Try again in a minute." 502/503 CATALOG_WRITE_FAILED: an S3 write failed (IAM WriteMetricReview, KMS, or throttling). The invdash-prefs-api-catalog-write-failed alarm fires.
"Dashboard data is being rebuilt; try again in a minute." SERVING_READ_FAILED / SERVING_DOCUMENT_INVALID: the serving file or catalog couldn't be read.
Approved, then a reload shows Pending review again Check catalog/<T>.json in the lake for status: approved. If it's there, the next serve republishes it. If not, the write didn't happen; check the logs.
Approved, but it's not in the dashboard Fundamentals overlay Only numeric quarterly series with a period-end date become overlays (mergeCompanyFundamentals). Reload the dashboard.

prefs-api logs (region us-west-1):

# Lambda errors for the review route (5xx only; 4xx aren't logged)
aws logs tail /aws/lambda/invdash-prefs-api --region us-west-1 --since 1h \
  --filter-pattern '{ $.event = "prefs_api_error" }'

# API Gateway access log: one line per request, with route and status (shows 401/403/404 too)
aws logs tail /aws/apigateway/invdash-site-api --region us-west-1 --since 1h \
  --filter-pattern '{ $.route = "PUT /stock/{ticker}/metrics/{metric_id}" }'

# What the lake says now
aws s3 cp s3://<lake-bucket>/catalog/TSLA.json - | jq '.metrics[] | select(.status=="approved")'

8.9 Related issues and PRs

  • #147: the original request: move the Company metrics panel to the company page, approval workflow, period-synced panels, no badges on charts, approved-only dashboard overlay (closed by PR #159)
  • PR #159: the approval workflow, PUT /stock/{ticker}/metrics/{metric_id}, admin group, audit fields, serving patch, collector preservation (merged Oct 11, 11:41 AM PT)
  • #161, PRs #160, #162, #163, #164: Company metrics panel above Call notes, header, toggle spacing, ticker-bar arrow
  • #166 / PR #167: header and panel spacing, toggles filter panels, Approve fix (item 5), readable dates (merged Oct 11, 4:40 PM PT)
  • #126, #137, #140, #141, #145: earlier approval and Pending review work
  • Company Metrics Collector: the Q2C/Q2X/Q2S jobs

Related

โš ๏ธ **GitHub.com Fallback** โš ๏ธ