@gloom-sh/terminal 0.19.0 → 0.19.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +2 -1
- package/patches/electrobun@1.18.1.patch +23 -1
- package/src/api-client/app-attention.ts +3 -0
- package/src/api-client/beneficial-owners.ts +3 -0
- package/src/api-client/company-kpis.ts +3 -0
- package/src/api-client/data.ts +2 -1
- package/src/api-client/errors.ts +38 -0
- package/src/api-client/hiring.ts +3 -0
- package/src/api-client/index.ts +4 -0
- package/src/api-client/ipo.ts +24 -1
- package/src/api-client/mna.ts +3 -0
- package/src/api-client/paths.ts +9 -1
- package/src/api-client/roadmap.ts +85 -0
- package/src/api-client/types.ts +12 -0
- package/src/app/runtime/startup.ts +3 -1
- package/src/architecture/core-builtin-allowlist.json +2 -2
- package/src/capabilities/chart-series.ts +2 -1
- package/src/cli/commands/market.ts +44 -16
- package/src/cli/commands/news-search.ts +22 -0
- package/src/cli/commands/overview.ts +84 -19
- package/src/cli/commands/search.ts +13 -13
- package/src/cli/commands/ticker.ts +32 -2
- package/src/cli/context.ts +1 -0
- package/src/cli/errors.ts +17 -2
- package/src/cli/glossary.ts +8 -1
- package/src/cli/history-rows.ts +35 -7
- package/src/cli/index-roots.ts +0 -7
- package/src/cli/known-gaps.ts +11 -0
- package/src/cli/listing-arg.ts +74 -11
- package/src/cli/pane-functions/capabilities.ts +1 -0
- package/src/cli/pane-functions/discovery.ts +21 -1
- package/src/cli/pane-functions/freshness.ts +7 -1
- package/src/cli/pane-functions/function-help.ts +7 -0
- package/src/cli/pane-functions/index.ts +6 -3
- package/src/cli/quote-answer.ts +30 -7
- package/src/cli/saved-copy-persistence.ts +88 -0
- package/src/components/command-bar/list/model.ts +5 -0
- package/src/components/command-bar/routes/root/runtime.ts +29 -8
- package/src/components/command-bar/selection-runtime.ts +64 -32
- package/src/components/command-bar/surface/index.tsx +2 -0
- package/src/components/layout/detached-pane-shell.tsx +2 -2
- package/src/components/layout/header.tsx +2 -2
- package/src/components/layout/pane/capture-stamp.ts +2 -2
- package/src/components/layout/pane/footer/index.tsx +23 -6
- package/src/components/layout/pane/footer/model.ts +116 -16
- package/src/components/layout/pane/header.tsx +2 -2
- package/src/components/layout/pane/status-footer.ts +37 -3
- package/src/components/layout/status-widget-space.ts +9 -0
- package/src/components/onboarding/onboarding-frame.tsx +2 -1
- package/src/components/onboarding/onboarding-wizard.tsx +6 -3
- package/src/components/ticker/list-table-view.tsx +4 -20
- package/src/components/ui/display.tsx +29 -3
- package/src/components/ui/icon.tsx +12 -3
- package/src/core/app-services.ts +11 -2
- package/src/data/app-persistence.ts +9 -7
- package/src/data/plugin-cache.ts +20 -3
- package/src/data/plugin-state-store.ts +81 -24
- package/src/data/resource-store.ts +130 -58
- package/src/data/saved-copies.ts +8 -0
- package/src/data/session-store.ts +31 -8
- package/src/data/sqlite/access.ts +313 -0
- package/src/data/sqlite/checkpointer.ts +198 -0
- package/src/data/sqlite/database.ts +47 -4
- package/src/data/sqlite/retry.ts +50 -9
- package/src/data/ticker-store.ts +57 -23
- package/src/docs/snapshot.json +32 -5
- package/src/i18n/es.ts +2 -4
- package/src/i18n/ja.ts +2 -4
- package/src/i18n/ko.ts +2 -4
- package/src/i18n/zh-cn.ts +2 -4
- package/src/i18n/zh-tw.ts +2 -4
- package/src/market-data/depositary-pair.ts +25 -1
- package/src/market-data/quotes/metadata.ts +7 -0
- package/src/market-data/realized-volatility.ts +4 -2
- package/src/news/aggregator.ts +7 -1
- package/src/news/types.ts +6 -0
- package/src/plugins/auto-update-node.ts +10 -6
- package/src/plugins/builtin/alerts/calendar-events.ts +10 -4
- package/src/plugins/builtin/alerts/cli.ts +34 -11
- package/src/plugins/builtin/alerts/event-rows.ts +24 -3
- package/src/plugins/builtin/alerts/events-pane.tsx +15 -6
- package/src/plugins/builtin/alerts/headless.ts +10 -6
- package/src/plugins/builtin/cds/client.ts +142 -30
- package/src/plugins/builtin/cds/headless.ts +35 -8
- package/src/plugins/builtin/cds/model.ts +145 -18
- package/src/plugins/builtin/cds/pane.tsx +16 -5
- package/src/plugins/builtin/central-bank-rates/series.ts +2 -3
- package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
- package/src/plugins/builtin/chart-composer/follow-binding.ts +14 -2
- package/src/plugins/builtin/chart-composer/headless-schema.ts +5 -0
- package/src/plugins/builtin/chart-composer/index.tsx +4 -5
- package/src/plugins/builtin/chart-composer/pane.tsx +5 -1
- package/src/plugins/builtin/chart-composer/session-range.ts +5 -2
- package/src/plugins/builtin/chat/controller/index.ts +5 -1
- package/src/plugins/builtin/chat/controller/notifications.ts +33 -3
- package/src/plugins/builtin/chat/controller/state.ts +25 -2
- package/src/plugins/builtin/chat/controller/unread-people.ts +89 -23
- package/src/plugins/builtin/chat/controller/utils.ts +7 -2
- package/src/plugins/builtin/chat/status-preview.ts +79 -0
- package/src/plugins/builtin/chat/status-widget.tsx +147 -10
- package/src/plugins/builtin/chat/unread-badge.ts +51 -10
- package/src/plugins/builtin/clinical-trials/client.ts +49 -4
- package/src/plugins/builtin/clinical-trials/model.ts +86 -22
- package/src/plugins/builtin/clinical-trials/pane.tsx +46 -27
- package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
- package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +87 -9
- package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
- package/src/plugins/builtin/commodity-balances/client.ts +4 -1
- package/src/plugins/builtin/company-attention/headless.ts +2 -1
- package/src/plugins/builtin/company-attention/pane.tsx +3 -0
- package/src/plugins/builtin/company-kpis/headless.ts +8 -3
- package/src/plugins/builtin/company-kpis/model.ts +73 -5
- package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
- package/src/plugins/builtin/composite-plugins.ts +2 -1
- package/src/plugins/builtin/cot/client.ts +28 -2
- package/src/plugins/builtin/cot/headless.ts +4 -1
- package/src/plugins/builtin/cot/index.tsx +4 -2
- package/src/plugins/builtin/cot/model.ts +58 -7
- package/src/plugins/builtin/cot/pane.tsx +6 -2
- package/src/plugins/builtin/cpi/client.ts +6 -5
- package/src/plugins/builtin/credit-boards/series.ts +3 -4
- package/src/plugins/builtin/credit-conditions/client.ts +22 -14
- package/src/plugins/builtin/credit-documents/headless.ts +3 -3
- package/src/plugins/builtin/crypto-board/client.ts +5 -5
- package/src/plugins/builtin/crypto-board/series.ts +4 -4
- package/src/plugins/builtin/debt-maturities/series.ts +3 -3
- package/src/plugins/builtin/doe/client.ts +6 -5
- package/src/plugins/builtin/doe/headless.ts +3 -2
- package/src/plugins/builtin/doe/pane.tsx +13 -3
- package/src/plugins/builtin/doe/prices.ts +2 -3
- package/src/plugins/builtin/doe/series.ts +4 -4
- package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
- package/src/plugins/builtin/earnings-calls/data.ts +37 -17
- package/src/plugins/builtin/earnings-calls/headless.ts +13 -3
- package/src/plugins/builtin/earnings-calls/pane.tsx +34 -14
- package/src/plugins/builtin/econ/calendar-model.ts +48 -1
- package/src/plugins/builtin/econ/calendar-source.ts +4 -2
- package/src/plugins/builtin/econ/detail-view.tsx +18 -4
- package/src/plugins/builtin/econ/figures.ts +48 -0
- package/src/plugins/builtin/econ/index.tsx +14 -5
- package/src/plugins/builtin/econ/print-watch.ts +3 -1
- package/src/plugins/builtin/econ-statistics/client.ts +11 -0
- package/src/plugins/builtin/econ-statistics/headless.ts +10 -2
- package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
- package/src/plugins/builtin/econ-statistics/stats.ts +3 -0
- package/src/plugins/builtin/executives/data.ts +6 -1
- package/src/plugins/builtin/executives/pane.tsx +9 -21
- package/src/plugins/builtin/fear-greed/cache.ts +4 -0
- package/src/plugins/builtin/fear-greed/series.ts +6 -4
- package/src/plugins/builtin/gpu/client.ts +4 -1
- package/src/plugins/builtin/gpu/series.ts +2 -3
- package/src/plugins/builtin/help/index.tsx +61 -59
- package/src/plugins/builtin/help/shortcut-table.tsx +5 -6
- package/src/plugins/builtin/help/table-cells.tsx +13 -2
- package/src/plugins/builtin/holders/beneficial-client.ts +5 -1
- package/src/plugins/builtin/holders/beneficial-view.tsx +4 -2
- package/src/plugins/builtin/holders/headless.ts +4 -2
- package/src/plugins/builtin/holders/pane.tsx +3 -0
- package/src/plugins/builtin/holders/report-rows.ts +4 -1
- package/src/plugins/builtin/insider/headless.ts +2 -2
- package/src/plugins/builtin/ipo-calendar/client.ts +18 -2
- package/src/plugins/builtin/ipo-calendar/headless.ts +75 -11
- package/src/plugins/builtin/ipo-calendar/listing-ipo.ts +160 -0
- package/src/plugins/builtin/ipo-calendar/model.ts +98 -2
- package/src/plugins/builtin/jodi/client.ts +4 -1
- package/src/plugins/builtin/macro-day/headless.ts +12 -5
- package/src/plugins/builtin/macro-day/index.tsx +1 -1
- package/src/plugins/builtin/macro-day/model.ts +39 -5
- package/src/plugins/builtin/macro-day/pane.tsx +27 -19
- package/src/plugins/builtin/market-movers/client.ts +3 -0
- package/src/plugins/builtin/market-movers/headless.ts +3 -1
- package/src/plugins/builtin/market-movers/index.tsx +10 -5
- package/src/plugins/builtin/market-movers/screener.ts +27 -6
- package/src/plugins/builtin/market-movers/session-body.tsx +2 -1
- package/src/plugins/builtin/mna/headless.ts +2 -0
- package/src/plugins/builtin/mna/pane.tsx +3 -1
- package/src/plugins/builtin/money-markets/series.ts +3 -5
- package/src/plugins/builtin/news/headless.ts +3 -1
- package/src/plugins/builtin/news/index.tsx +3 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -6
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +0 -14
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +2 -1
- package/src/plugins/builtin/portfolio-list/table.tsx +4 -6
- package/src/plugins/builtin/power/gas-client.ts +2 -1
- package/src/plugins/builtin/power/prices-client.ts +3 -2
- package/src/plugins/builtin/realized-vol/client.ts +13 -5
- package/src/plugins/builtin/realized-vol/evidence.ts +29 -4
- package/src/plugins/builtin/realized-vol/headless.ts +8 -4
- package/src/plugins/builtin/realized-vol/model.ts +37 -6
- package/src/plugins/builtin/realized-vol/pane.tsx +9 -7
- package/src/plugins/builtin/research/event-model.ts +30 -4
- package/src/plugins/builtin/research/index.tsx +7 -3
- package/src/plugins/builtin/research/relative-valuation-headless.ts +8 -25
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +52 -15
- package/src/plugins/builtin/research/relative-valuation-peers.ts +217 -20
- package/src/plugins/builtin/research/relative-valuation-settings.ts +44 -0
- package/src/plugins/builtin/risk-factors/data.ts +12 -6
- package/src/plugins/builtin/risk-factors/headless.ts +5 -2
- package/src/plugins/builtin/risk-factors/index.tsx +3 -1
- package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
- package/src/plugins/builtin/roadmap/index.tsx +28 -0
- package/src/plugins/builtin/roadmap/model.ts +67 -0
- package/src/plugins/builtin/roadmap/pane.tsx +244 -0
- package/src/plugins/builtin/scanner/live-headless.ts +21 -2
- package/src/plugins/builtin/scanner/lock-note.ts +26 -3
- package/src/plugins/builtin/sec/headless.ts +2 -1
- package/src/plugins/builtin/sec/index.tsx +3 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +20 -7
- package/src/plugins/builtin/shared/fred-points.ts +6 -1
- package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
- package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
- package/src/plugins/builtin/shared/report-freshness.ts +8 -0
- package/src/plugins/builtin/shared/research-cells.tsx +1 -1
- package/src/plugins/builtin/shared/series-cache.ts +4 -4
- package/src/plugins/builtin/shared/series-warning.ts +13 -4
- package/src/plugins/builtin/short-volume/series.ts +2 -3
- package/src/plugins/builtin/social-mentions/client.ts +15 -10
- package/src/plugins/builtin/social-mentions/series.ts +2 -3
- package/src/plugins/builtin/thirteenf/api.ts +15 -9
- package/src/plugins/builtin/thirteenf/data.ts +39 -11
- package/src/plugins/builtin/thirteenf/headless.ts +31 -4
- package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
- package/src/plugins/builtin/thirteenf/pane.tsx +21 -5
- package/src/plugins/builtin/thirteenf/types.ts +2 -0
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +5 -2
- package/src/plugins/builtin/ticker-detail/financials/model.ts +35 -2
- package/src/plugins/builtin/ticker-detail/financials/ratios.ts +53 -18
- package/src/plugins/builtin/ticker-detail/headless.ts +59 -8
- package/src/plugins/builtin/ticker-detail/overview/model.ts +10 -2
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +11 -6
- package/src/plugins/builtin/treasury-auctions/cache.ts +20 -8
- package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
- package/src/plugins/builtin/treasury-auctions/headless.ts +24 -13
- package/src/plugins/builtin/treasury-auctions/pane.tsx +18 -6
- package/src/plugins/builtin/volatility/client.ts +43 -16
- package/src/plugins/builtin/volatility/headless.ts +5 -2
- package/src/plugins/builtin/volatility/model.ts +17 -6
- package/src/plugins/builtin/volatility/pane.tsx +11 -4
- package/src/plugins/builtin/volatility/series.ts +2 -1
- package/src/plugins/builtin/yield-curve/client.ts +59 -1
- package/src/plugins/builtin/yield-curve/headless.ts +9 -6
- package/src/plugins/builtin/yield-curve/index.tsx +34 -10
- package/src/plugins/builtin/yield-curve/swaps.tsx +9 -5
- package/src/plugins/catalog-browser.ts +2 -1
- package/src/plugins/host-link.ts +1 -1
- package/src/plugins/installer.ts +50 -39
- package/src/portfolio/column-values.ts +11 -7
- package/src/renderers/dom/desktop/icons.tsx +10 -2
- package/src/renderers/electrobun/bun/desktop/backend.ts +9 -10
- package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +8 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +9 -1
- package/src/renderers/electrobun/bun/desktop/installer-folder.ts +731 -0
- package/src/renderers/electrobun/bun/desktop/relaunch.ts +102 -10
- package/src/renderers/electrobun/bun/desktop/windows-script.ts +92 -0
- package/src/renderers/electrobun/bun/index.ts +26 -2
- package/src/renderers/electrobun/bun/window/attention.ts +54 -0
- package/src/renderers/electrobun/bun/window/create.ts +45 -4
- package/src/renderers/electrobun/bun/window/events.ts +20 -0
- package/src/renderers/electrobun/bun/window/windows-custom-chrome.ts +9 -2
- package/src/renderers/electrobun/bun/window/windows-native.ts +78 -0
- package/src/renderers/electrobun/bun/window/windows-quit.ts +57 -0
- package/src/renderers/electrobun/launcher/preload.ts +13 -0
- package/src/renderers/electrobun/launcher/windows-emulation.ts +50 -0
- package/src/renderers/electrobun/shared/protocol.ts +2 -0
- package/src/renderers/electrobun/view/news-source.ts +2 -0
- package/src/renderers/electrobun/view/ui-host.tsx +16 -1
- package/src/series/resolve.ts +2 -1
- package/src/series/types.ts +5 -0
- package/src/sources/gloomberb-cloud/fred-series.ts +8 -4
- package/src/sources/gloomberb-cloud/index.ts +42 -8
- package/src/telemetry/stall-reports-node.ts +13 -2
- package/src/telemetry/stall-reports.ts +189 -13
- package/src/test-support/outage.ts +8 -0
- package/src/tickers/company-name-listing.ts +110 -40
- package/src/tickers/company-name.ts +71 -2
- package/src/tickers/index-roots.ts +7 -0
- package/src/tickers/listing-fate.ts +2 -2
- package/src/tickers/search/home-listing.ts +93 -8
- package/src/tickers/search/index.ts +7 -3
- package/src/tickers/search/ranking.ts +5 -1
- package/src/tickers/search/upsert.ts +13 -0
- package/src/tickers/ticker-changes.ts +8 -4
- package/src/time-series/market.ts +5 -3
- package/src/time-series/price-comparison.ts +20 -2
- package/src/time-series/resolve.ts +20 -1
- package/src/time-series/types.ts +7 -0
- package/src/types/company-from.ts +40 -0
- package/src/types/data-provider.ts +7 -0
- package/src/types/financials.ts +45 -3
- package/src/types/headless.ts +6 -0
- package/src/ui/host.tsx +6 -1
- package/src/utils/crypto-pair.ts +121 -7
- package/src/utils/declared-dividends.ts +1 -1
- package/src/utils/financial-statements.ts +73 -16
- package/src/utils/fundamentals.ts +53 -10
- package/src/utils/lease-liabilities.ts +54 -0
- package/src/utils/main-thread-stall.ts +47 -2
- package/src/utils/perf-marks.ts +23 -11
- package/src/utils/price-history-integrity.ts +47 -1
- package/src/version.ts +2 -2
|
@@ -5,12 +5,14 @@ import {
|
|
|
5
5
|
type CloudEarningsCallPayload,
|
|
6
6
|
type CloudEarningsTranscriptPayload,
|
|
7
7
|
} from "../../../api-client";
|
|
8
|
-
import { ApiRequestError } from "../../../api-client/errors";
|
|
8
|
+
import { ApiRequestError, isTransientFailure } from "../../../api-client/errors";
|
|
9
9
|
import { listingAbroad, type IssuerListingParams, type ListingAbroad } from "../../../api-client/paths";
|
|
10
|
+
import { servedFromSavedCopy } from "../../../data/saved-copies";
|
|
10
11
|
import { areDifferentCompanies } from "../../../sources/sec-registrant";
|
|
11
12
|
import type { HeadlessPaneApiClient, PluginPersistence } from "../../../types/plugin";
|
|
12
13
|
import { shortDate } from "../earnings/format";
|
|
13
14
|
import { callCountdown, callDay, callTimeKnown, isUpcomingCall } from "./upcoming";
|
|
15
|
+
import { type CompanyFrom, readCompanyFrom } from "../../../types/company-from";
|
|
14
16
|
|
|
15
17
|
type EarningsCallsApiClient = Pick<
|
|
16
18
|
HeadlessPaneApiClient,
|
|
@@ -43,6 +45,8 @@ export interface EarningsCallsResult {
|
|
|
43
45
|
calls: CloudEarningsCallPayload[];
|
|
44
46
|
fetchedAt: number;
|
|
45
47
|
stale: boolean;
|
|
48
|
+
/** Its refresh failed and the copy saved at this time (ms) answered instead. */
|
|
49
|
+
savedAt?: number;
|
|
46
50
|
refreshError?: string;
|
|
47
51
|
/** HTTP status when the request failed, used to pick the right gate. */
|
|
48
52
|
errorStatus?: number;
|
|
@@ -50,6 +54,8 @@ export interface EarningsCallsResult {
|
|
|
50
54
|
pending?: boolean;
|
|
51
55
|
/** The symbol is not one the SEC knows, so there is nothing to look for. */
|
|
52
56
|
unknownTicker?: boolean;
|
|
57
|
+
/** The company's home line the calls are read from, when not the listing asked. */
|
|
58
|
+
companyFrom?: CompanyFrom | null;
|
|
53
59
|
/** Maximum rows requested from the server, which supplies no total/hasMore. */
|
|
54
60
|
sourceLimit?: number;
|
|
55
61
|
/** The response filled its request; additional calls may exist. */
|
|
@@ -118,6 +124,8 @@ export interface UpcomingCallsResult {
|
|
|
118
124
|
stale: boolean;
|
|
119
125
|
/** The server answered its limit's worth: calls later on these days may be missing. */
|
|
120
126
|
truncated?: boolean;
|
|
127
|
+
/** Its refresh failed and the copy saved at this time (ms) answered instead. */
|
|
128
|
+
savedAt?: number;
|
|
121
129
|
refreshError?: string;
|
|
122
130
|
errorStatus?: number;
|
|
123
131
|
}
|
|
@@ -130,7 +138,7 @@ export interface UpcomingDays {
|
|
|
130
138
|
let persistence: PluginPersistence | null = null;
|
|
131
139
|
const activeUpcomingFetches = new Map<string, Promise<UpcomingCallsResult>>();
|
|
132
140
|
const activeListFetches = new Map<string, Promise<EarningsCallsResult>>();
|
|
133
|
-
const activeTranscriptFetches = new Map<string, Promise<
|
|
141
|
+
const activeTranscriptFetches = new Map<string, Promise<EarningsTranscriptResult>>();
|
|
134
142
|
|
|
135
143
|
export function attachEarningsCallsPersistence(value: PluginPersistence): void {
|
|
136
144
|
persistence = value;
|
|
@@ -216,8 +224,15 @@ export async function loadEarningsCallsWithClient(
|
|
|
216
224
|
...(offset > 0 ? { offset } : {}),
|
|
217
225
|
})
|
|
218
226
|
.then((payload) => {
|
|
219
|
-
|
|
220
|
-
const
|
|
227
|
+
// Calls the service read from the company's home line are the company's, whatever the line's own name.
|
|
228
|
+
const companyFrom = readCompanyFrom(payload.companyFrom);
|
|
229
|
+
const calls = companyFrom ? payload.calls ?? [] : listingCalls(payload.calls ?? [], abroad);
|
|
230
|
+
const value = {
|
|
231
|
+
calls,
|
|
232
|
+
pending: payload.pending === true,
|
|
233
|
+
unknownTicker: payload.unknownTicker === true,
|
|
234
|
+
...(companyFrom ? { companyFrom } : {}),
|
|
235
|
+
};
|
|
221
236
|
// A list with calls still being produced changes by the minute, so it
|
|
222
237
|
// is not worth keeping; a list of finished transcripts is.
|
|
223
238
|
const settled = !value.pending && calls.every((call) => call.hasTranscript);
|
|
@@ -242,22 +257,24 @@ export async function loadEarningsCallsWithClient(
|
|
|
242
257
|
.catch((error: unknown) => {
|
|
243
258
|
const refreshError = error instanceof Error ? error.message : String(error);
|
|
244
259
|
const errorStatus = statusOf(error);
|
|
245
|
-
//
|
|
246
|
-
//
|
|
260
|
+
// A refusal (signed out, no Pro, a route the server stopped serving) or a reply that fails
|
|
261
|
+
// validation is said rather than covered by a cached list.
|
|
247
262
|
const expired = store?.getResource<CloudEarningsCallListPayload>(LIST_KIND, key, {
|
|
248
263
|
sourceKey: CACHE_SOURCE,
|
|
249
264
|
schemaVersion: LIST_CACHE_SCHEMA_VERSION,
|
|
250
265
|
allowExpired: true,
|
|
251
266
|
});
|
|
252
|
-
if (expired &&
|
|
253
|
-
|
|
267
|
+
if (expired && isTransientFailure(error)) {
|
|
268
|
+
// A command that shows it ends with when the copy was saved, not with the failure.
|
|
269
|
+
return servedFromSavedCopy({
|
|
254
270
|
...expired.value,
|
|
255
271
|
...sourceScope(expired.value.calls),
|
|
256
272
|
fetchedAt: expired.fetchedAt,
|
|
257
273
|
stale: true,
|
|
274
|
+
savedAt: expired.fetchedAt,
|
|
258
275
|
refreshError,
|
|
259
276
|
errorStatus,
|
|
260
|
-
};
|
|
277
|
+
}, expired.fetchedAt);
|
|
261
278
|
}
|
|
262
279
|
throw error;
|
|
263
280
|
})
|
|
@@ -355,12 +372,13 @@ export async function loadUpcomingCallsWithClient(
|
|
|
355
372
|
const errorStatus = statusOf(error);
|
|
356
373
|
// Losing Pro access re-gates the calendar, as it does the list.
|
|
357
374
|
const expired = read(true);
|
|
358
|
-
if (expired &&
|
|
359
|
-
return {
|
|
375
|
+
if (expired && isTransientFailure(error)) {
|
|
376
|
+
return servedFromSavedCopy({
|
|
360
377
|
...upcomingFromCache(expired.value, expired.fetchedAt, true),
|
|
378
|
+
savedAt: expired.fetchedAt,
|
|
361
379
|
refreshError: error instanceof Error ? error.message : String(error),
|
|
362
380
|
errorStatus,
|
|
363
|
-
};
|
|
381
|
+
}, expired.fetchedAt);
|
|
364
382
|
}
|
|
365
383
|
throw error;
|
|
366
384
|
})
|
|
@@ -397,11 +415,14 @@ export function isPendingTranscript(
|
|
|
397
415
|
return record.status === "pending" || !Array.isArray(record.turns);
|
|
398
416
|
}
|
|
399
417
|
|
|
418
|
+
/** A transcript, and when it was saved when its refresh failed and the saved copy answered. */
|
|
419
|
+
export type EarningsTranscriptResult = CloudEarningsTranscriptPayload & { savedAt?: number };
|
|
420
|
+
|
|
400
421
|
export async function loadTranscriptWithClient(
|
|
401
422
|
client: EarningsCallsApiClient,
|
|
402
423
|
callId: string,
|
|
403
424
|
options?: { force?: boolean },
|
|
404
|
-
): Promise<
|
|
425
|
+
): Promise<EarningsTranscriptResult> {
|
|
405
426
|
const force = options?.force ?? false;
|
|
406
427
|
const store = persistence;
|
|
407
428
|
|
|
@@ -432,7 +453,6 @@ export async function loadTranscriptWithClient(
|
|
|
432
453
|
return transcript;
|
|
433
454
|
})
|
|
434
455
|
.catch((error: unknown) => {
|
|
435
|
-
const status = statusOf(error);
|
|
436
456
|
const expired = store?.getResource<CloudEarningsTranscriptPayload>(
|
|
437
457
|
TRANSCRIPT_KIND,
|
|
438
458
|
callId,
|
|
@@ -443,8 +463,8 @@ export async function loadTranscriptWithClient(
|
|
|
443
463
|
},
|
|
444
464
|
);
|
|
445
465
|
// Losing Pro access must re-gate the transcript, not serve it from cache.
|
|
446
|
-
if (expired &&
|
|
447
|
-
return expired.value;
|
|
466
|
+
if (expired && isTransientFailure(error)) {
|
|
467
|
+
return servedFromSavedCopy({ ...expired.value, savedAt: expired.fetchedAt }, expired.fetchedAt);
|
|
448
468
|
}
|
|
449
469
|
throw error;
|
|
450
470
|
})
|
|
@@ -461,7 +481,7 @@ export async function loadTranscriptWithClient(
|
|
|
461
481
|
export function loadTranscript(
|
|
462
482
|
callId: string,
|
|
463
483
|
options?: { force?: boolean },
|
|
464
|
-
): Promise<
|
|
484
|
+
): Promise<EarningsTranscriptResult> {
|
|
465
485
|
return loadTranscriptWithClient(apiClient, callId, options);
|
|
466
486
|
}
|
|
467
487
|
|
|
@@ -46,6 +46,10 @@ import {
|
|
|
46
46
|
type TranscriptSection,
|
|
47
47
|
} from "./model";
|
|
48
48
|
import { REPORT_FILTER, REPORT_PAGE, REPORT_ROWS } from "../shared/headless-options";
|
|
49
|
+
import { companyDataNotes } from "../../../types/company-from";
|
|
50
|
+
import { savedCopyMark, unansweredRefreshError } from "../../../data/saved-copies";
|
|
51
|
+
|
|
52
|
+
const optionalErrors = (error: string | null) => error ? [error] : undefined;
|
|
49
53
|
|
|
50
54
|
const CALL_COLUMNS = [
|
|
51
55
|
{ key: "ticker", header: "Ticker" },
|
|
@@ -170,9 +174,12 @@ function projectEarningsCallsHeadless(
|
|
|
170
174
|
columns: CALL_COLUMNS,
|
|
171
175
|
rows,
|
|
172
176
|
...(next ? { textRows: rows.filter((row) => row.id !== next.id) } : {}),
|
|
173
|
-
errors: result
|
|
177
|
+
errors: optionalErrors(unansweredRefreshError(result)),
|
|
174
178
|
...(sourceLimitReached || result.pending ? { complete: false } : {}),
|
|
179
|
+
...(result.companyFrom ? { notes: companyDataNotes(result.companyFrom) } : {}),
|
|
175
180
|
metadata: {
|
|
181
|
+
// A list served from a saved copy ends with when it was saved, instead of its refresh failure.
|
|
182
|
+
...savedCopyMark(result),
|
|
176
183
|
...(next ? { notices: [nextCallNotice(next, now)], nextCall: { id: next.id, callAt: next.callAt, timeKnown: callTimeKnown(next), in: callCountdown(next, now) } } : {}),
|
|
177
184
|
fetchedAt: result.fetchedAt,
|
|
178
185
|
stale: result.stale,
|
|
@@ -211,9 +218,11 @@ function transcriptResult(
|
|
|
211
218
|
return {
|
|
212
219
|
columns: TRANSCRIPT_COLUMNS,
|
|
213
220
|
rows,
|
|
214
|
-
errors: calls
|
|
221
|
+
errors: optionalErrors(unansweredRefreshError(calls)),
|
|
215
222
|
...(calls.stale || calls.pending ? { complete: false } : {}),
|
|
223
|
+
...(calls.companyFrom ? { notes: companyDataNotes(calls.companyFrom) } : {}),
|
|
216
224
|
metadata: {
|
|
225
|
+
...savedCopyMark(calls),
|
|
217
226
|
stale: calls.stale,
|
|
218
227
|
callListFetchedAt: calls.fetchedAt,
|
|
219
228
|
callListPending: calls.pending ?? false,
|
|
@@ -283,10 +292,11 @@ function projectCalendar(
|
|
|
283
292
|
columns: CALENDAR_COLUMNS,
|
|
284
293
|
textColumns: sameClock ? CALENDAR_TEXT_COLUMNS.filter((column) => column.key !== "local") : CALENDAR_TEXT_COLUMNS,
|
|
285
294
|
rows,
|
|
286
|
-
errors: result
|
|
295
|
+
errors: optionalErrors(unansweredRefreshError(result)),
|
|
287
296
|
...(result.stale || result.truncated ? { complete: false } : {}),
|
|
288
297
|
empty: "No calls on the calendar these days.",
|
|
289
298
|
metadata: {
|
|
299
|
+
...savedCopyMark(result),
|
|
290
300
|
view: "upcoming",
|
|
291
301
|
fetchedAt: result.fetchedAt,
|
|
292
302
|
stale: result.stale,
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import type {
|
|
3
|
-
CloudEarningsCallPayload,
|
|
4
|
-
CloudEarningsTranscriptPayload,
|
|
5
|
-
} from "../../../api-client";
|
|
2
|
+
import type { CloudEarningsCallPayload } from "../../../api-client";
|
|
6
3
|
import { recordResearchActivity } from "../../../api-client/research-activity";
|
|
7
4
|
import {
|
|
8
5
|
DataTableStackView,
|
|
@@ -19,6 +16,7 @@ import {
|
|
|
19
16
|
type QueryBarView,
|
|
20
17
|
type StatItem,
|
|
21
18
|
} from "../../../components";
|
|
19
|
+
import { useCompanyDataFooter } from "../../../components/layout/pane/status-footer";
|
|
22
20
|
import { handleRefreshKey } from "../../../components/data-table/table-pane";
|
|
23
21
|
import { AudioPlayer, useCanPlayAudio } from "../../../components/ui";
|
|
24
22
|
import { useShortcut } from "../../../react/input";
|
|
@@ -48,7 +46,11 @@ import {
|
|
|
48
46
|
loadEarningsCalls,
|
|
49
47
|
loadTranscript,
|
|
50
48
|
statusOf,
|
|
49
|
+
type EarningsTranscriptResult,
|
|
51
50
|
} from "./data";
|
|
51
|
+
import { isPermanentClientError } from "../../../api-client/errors";
|
|
52
|
+
import { savedCopyFooterInfo } from "../../../components/layout/pane/footer";
|
|
53
|
+
import { STALE_SEGMENT } from "../../../components/layout/pane/status-footer";
|
|
52
54
|
import { callPeriod, callTitle, eventLabelOf, formatCallDate, formatDuration, formatSentiment } from "./format";
|
|
53
55
|
import { findCallForQuarter, findInLines, MIN_FIND_LENGTH, turnParagraphs } from "./model";
|
|
54
56
|
import { READER_TABS, TranscriptView, type ReaderTab, type TranscriptPlayback } from "./transcript-view";
|
|
@@ -147,6 +149,8 @@ interface TickerLookup {
|
|
|
147
149
|
calls: CloudEarningsCallPayload[];
|
|
148
150
|
fetchedAt?: number;
|
|
149
151
|
stale?: boolean;
|
|
152
|
+
/** Its refresh failed and the copy saved at this time (ms) answered instead. */
|
|
153
|
+
savedAt?: number;
|
|
150
154
|
refreshError?: string;
|
|
151
155
|
refreshRequest?: number;
|
|
152
156
|
}
|
|
@@ -277,6 +281,10 @@ function CallsListView({
|
|
|
277
281
|
const [listStatus, setListStatus] = useState<"idle" | "loading" | "loaded" | "error">(initialShelf ? "loaded" : "idle");
|
|
278
282
|
const [listError, setListError] = useState<{ message: string; status?: number } | null>(null);
|
|
279
283
|
const [stale, setStale] = useState(initialShelf?.stale === true);
|
|
284
|
+
// The company's home line the shelf is read from, for a receipt or a foreign line.
|
|
285
|
+
const [companyFrom, setCompanyFrom] = useState(initialShelf?.companyFrom ?? null);
|
|
286
|
+
// The list's refresh failed and a copy saved at this time (ms) answered instead.
|
|
287
|
+
const [listSavedAt, setListSavedAt] = useState<number | null>(null);
|
|
280
288
|
const [listFetchedAt, setListFetchedAt] = useState(initialShelf?.fetchedAt ?? 0);
|
|
281
289
|
const listRequestVersion = useRef(0);
|
|
282
290
|
const [listCompletedRequest, setListCompletedRequest] = useState(0);
|
|
@@ -289,7 +297,7 @@ function CallsListView({
|
|
|
289
297
|
|
|
290
298
|
const [selectedId, setSelectedId] = usePluginPaneState<string | null>(`${stateKey}selectedId`, null);
|
|
291
299
|
const [detailOpen, setDetailOpen] = usePluginPaneState<boolean>(`${stateKey}detailOpen`, false);
|
|
292
|
-
const [transcript, setTranscript] = useState<
|
|
300
|
+
const [transcript, setTranscript] = useState<EarningsTranscriptResult | null>(null);
|
|
293
301
|
useEffect(() => {
|
|
294
302
|
if (focused && transcript && detailOpen) recordResearchActivity("pro_feature_used", "transcripts");
|
|
295
303
|
}, [focused, transcript, detailOpen]);
|
|
@@ -340,10 +348,13 @@ function CallsListView({
|
|
|
340
348
|
setNextOffset(result.calls.length);
|
|
341
349
|
setHasMore(result.sourceLimitReached === true);
|
|
342
350
|
setStale(result.stale);
|
|
351
|
+
setCompanyFrom(result.companyFrom ?? null);
|
|
352
|
+
setListSavedAt(result.savedAt ?? null);
|
|
343
353
|
setListFetchedAt(result.fetchedAt);
|
|
344
354
|
setListPending(result.pending === true && result.calls.length === 0);
|
|
355
|
+
// A saved copy says when it was saved, in the footer, rather than why the refresh failed.
|
|
345
356
|
setListError(
|
|
346
|
-
result.refreshError ? { message: result.refreshError, status: result.errorStatus } : null,
|
|
357
|
+
result.refreshError && result.savedAt == null ? { message: result.refreshError, status: result.errorStatus } : null,
|
|
347
358
|
);
|
|
348
359
|
setListStatus("loaded");
|
|
349
360
|
setListCompletedRequest(request);
|
|
@@ -353,6 +364,7 @@ function CallsListView({
|
|
|
353
364
|
setListPending(false);
|
|
354
365
|
setHasMore(false);
|
|
355
366
|
setNextOffset(0);
|
|
367
|
+
setListSavedAt(null);
|
|
356
368
|
setListError({
|
|
357
369
|
message: error instanceof Error ? error.message : String(error),
|
|
358
370
|
status: statusOf(error),
|
|
@@ -410,6 +422,8 @@ function CallsListView({
|
|
|
410
422
|
setListStatus(held ? "loaded" : "idle");
|
|
411
423
|
setListPending(held?.pending === true && held.calls.length === 0);
|
|
412
424
|
setStale(held?.stale === true);
|
|
425
|
+
setCompanyFrom(held?.companyFrom ?? null);
|
|
426
|
+
setListSavedAt(null);
|
|
413
427
|
setListFetchedAt(held?.fetchedAt ?? 0);
|
|
414
428
|
setListError(null);
|
|
415
429
|
setLookup(null);
|
|
@@ -477,19 +491,22 @@ function CallsListView({
|
|
|
477
491
|
calls: result.calls,
|
|
478
492
|
fetchedAt: result.fetchedAt,
|
|
479
493
|
stale: result.stale,
|
|
480
|
-
|
|
494
|
+
savedAt: result.savedAt,
|
|
495
|
+
refreshError: result.savedAt == null ? result.refreshError : undefined,
|
|
481
496
|
refreshRequest,
|
|
482
497
|
});
|
|
483
498
|
})
|
|
484
499
|
.catch((error: unknown) => {
|
|
485
500
|
if (cancelled) return;
|
|
486
|
-
|
|
501
|
+
// A refusal asking again will not change clears the rows; a passing failure keeps them.
|
|
502
|
+
const retainRows = !isPermanentClientError(error);
|
|
487
503
|
setLookup((current) => ({
|
|
488
504
|
ticker: lookupTicker,
|
|
489
505
|
status: "error",
|
|
490
506
|
calls: retainRows && current?.ticker === lookupTicker ? current.calls : [],
|
|
491
507
|
fetchedAt: retainRows && current?.ticker === lookupTicker ? current.fetchedAt : undefined,
|
|
492
508
|
stale: retainRows && current?.ticker === lookupTicker && current.calls.length > 0,
|
|
509
|
+
savedAt: retainRows && current?.ticker === lookupTicker ? current.savedAt : undefined,
|
|
493
510
|
refreshError: error instanceof Error ? error.message : String(error),
|
|
494
511
|
refreshRequest,
|
|
495
512
|
}));
|
|
@@ -817,6 +834,8 @@ function CallsListView({
|
|
|
817
834
|
[focusSearch, scrollTranscriptBy, openSource, playerShown, playback.toggle, playback.seekBy],
|
|
818
835
|
);
|
|
819
836
|
|
|
837
|
+
// A receipt's or foreign line's calls are its company's, read from the home line.
|
|
838
|
+
useCompanyDataFooter(companyFrom, !readerOpen);
|
|
820
839
|
usePaneFooter(
|
|
821
840
|
footerId,
|
|
822
841
|
() => {
|
|
@@ -857,15 +876,14 @@ function CallsListView({
|
|
|
857
876
|
}
|
|
858
877
|
}
|
|
859
878
|
const visibleLookup = lookup && (lookup.ticker === lookupTicker || (detailOpen && selected?.ticker === lookup.ticker)) ? lookup : null;
|
|
860
|
-
if (visibleLookup?.stale) {
|
|
861
|
-
info.push({ id: "lookup-stale", parts: [{ text: `${visibleLookup.ticker} stale cache`, tone: "warning" }] });
|
|
862
|
-
}
|
|
863
879
|
if (visibleLookup?.refreshError && lookupState !== "error") {
|
|
864
880
|
info.push({ id: "lookup-error", parts: [{ text: `${visibleLookup.ticker}: ${visibleLookup.refreshError}`, tone: "warning" }] });
|
|
865
881
|
}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
882
|
+
// What a failed refresh left on screen: a saved copy says when it was saved; a list
|
|
883
|
+
// held from an earlier session, still refreshing, is only stale.
|
|
884
|
+
const savedCopy = savedCopyFooterInfo([listSavedAt, visibleLookup?.savedAt, readerOpen ? selectedTranscript?.savedAt : null]);
|
|
885
|
+
info.push(...savedCopy);
|
|
886
|
+
if (stale && listSavedAt == null) info.push(STALE_SEGMENT);
|
|
869
887
|
if (listError) {
|
|
870
888
|
info.push({ id: "error", parts: [{ text: listError.message, tone: "warning" }] });
|
|
871
889
|
}
|
|
@@ -904,6 +922,8 @@ function CallsListView({
|
|
|
904
922
|
lookupTicker,
|
|
905
923
|
lookup,
|
|
906
924
|
stale,
|
|
925
|
+
listSavedAt,
|
|
926
|
+
selectedTranscript,
|
|
907
927
|
listError,
|
|
908
928
|
proRequired,
|
|
909
929
|
transcriptProRequired,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createPluginCache } from "../../../data/plugin-cache";
|
|
2
2
|
import type { DataTableColumn } from "../../../components";
|
|
3
3
|
import { colors } from "../../../theme/colors";
|
|
4
|
-
import { displayWidth } from "../../../utils/format";
|
|
4
|
+
import { displayWidth, MISSING_VALUE } from "../../../utils/format";
|
|
5
5
|
import { fetchEconCalendar } from "./calendar-source";
|
|
6
6
|
import { parseEconFigure } from "./figures";
|
|
7
7
|
import { isAllDayEvent, type EconEvent, type EconImpact } from "./types";
|
|
@@ -235,6 +235,51 @@ export function eventCountry(event: Pick<EconEvent, "country" | "event">): strin
|
|
|
235
235
|
return euroAreaMember(event) ?? event.country;
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
/** What the feeds call the Fed's rate decision. */
|
|
239
|
+
const FED_RATE_DECISION_NAMES = new Set(["federal funds rate", "fed interest rate decision"]);
|
|
240
|
+
/** A figure that is a range (3.75-4.00%) rather than one level. */
|
|
241
|
+
const RANGE_FIGURE = /\d\s*[-\u2013]\s*\d/;
|
|
242
|
+
const FED_FUNDS_UPPER_BOUND = "Federal Funds Rate (upper bound)";
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* A row's name as the calendar shows it. The Fed's decision is figured as one
|
|
246
|
+
* level, the top of the target range (4.00% for 3.75-4.00%), so it is named
|
|
247
|
+
* for that bound rather than read as the range or the effective rate; a row
|
|
248
|
+
* whose figures are the range keeps the feed's name. Whatever matches rows by
|
|
249
|
+
* name (alerts, the FRED history) reads the feed's own, `event.event`.
|
|
250
|
+
*/
|
|
251
|
+
export function econEventName(event: Pick<EconEvent, "country" | "event" | "actual" | "forecast" | "prior">): string {
|
|
252
|
+
if ((event.country !== "US" && event.country !== "USD") || !FED_RATE_DECISION_NAMES.has(event.event.trim().toLowerCase())) return event.event;
|
|
253
|
+
return [event.actual, event.forecast, event.prior].some((figure) => !!figure && RANGE_FIGURE.test(figure)) ? event.event : FED_FUNDS_UPPER_BOUND;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Letters and digits only, lower case: "Non-Farm" and "nonfarm" read alike. */
|
|
257
|
+
const compactName = (text: string) => text.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Whether a row's name carries every word of `query`, case, spaces and
|
|
261
|
+
* punctuation aside: `nonfarm` finds Non Farm Payrolls and Non-Farm
|
|
262
|
+
* Employment Change, `fed funds` the Federal Funds Rate. The name shown and the
|
|
263
|
+
* feed's own are both read.
|
|
264
|
+
*/
|
|
265
|
+
export function matchesEventSearch(event: Pick<EconEvent, "country" | "event" | "actual" | "forecast" | "prior">, query: string): boolean {
|
|
266
|
+
const words = query.split(/\s+/).map(compactName).filter(Boolean);
|
|
267
|
+
if (words.length === 0) return true;
|
|
268
|
+
const names = [event.event, econEventName(event)].map(compactName);
|
|
269
|
+
return names.some((name) => words.every((word) => name.includes(word)));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* A release still to come that has a prior and no forecast: the consensus is
|
|
274
|
+
* not in yet, which the calendar draws as a dash.
|
|
275
|
+
*/
|
|
276
|
+
export function awaitsConsensus(event: Pick<EconEvent, "actual" | "forecast" | "prior">): boolean {
|
|
277
|
+
return !event.actual?.trim() && !event.forecast?.trim() && !!event.prior?.trim();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** What a dash under Forecast means on a row still to come. */
|
|
281
|
+
export const NO_CONSENSUS_NOTE = `${MISSING_VALUE} forecast = no consensus yet`;
|
|
282
|
+
|
|
238
283
|
/** A national euro-area release matches EU (and G7, as EU) and its own member's code. */
|
|
239
284
|
export function matchesCountry(event: Pick<EconEvent, "country" | "event">, filter: CountryFilter): boolean {
|
|
240
285
|
const member = euroAreaMember(event);
|
|
@@ -376,6 +421,8 @@ export function subscribeCalendar(listener: () => void): () => void {
|
|
|
376
421
|
}
|
|
377
422
|
|
|
378
423
|
export interface EconCalendarLoadResult extends EconCalendarCacheEntry {
|
|
424
|
+
/** `stale-fallback`: the copy an earlier read saved, answering because its refresh failed. */
|
|
425
|
+
source?: "cache" | "network" | "stale-fallback";
|
|
379
426
|
refreshError?: string;
|
|
380
427
|
}
|
|
381
428
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { apiClient, type CloudEconEventPayload } from "../../../api-client";
|
|
2
2
|
import { ApiRequestError, isPermanentClientError } from "../../../api-client/errors";
|
|
3
|
+
import { withPercentUnits } from "./figures";
|
|
3
4
|
import type { EconEvent } from "./types";
|
|
4
5
|
|
|
5
6
|
function toEconEvent(event: CloudEconEventPayload): EconEvent {
|
|
@@ -9,9 +10,10 @@ function toEconEvent(event: CloudEconEventPayload): EconEvent {
|
|
|
9
10
|
};
|
|
10
11
|
}
|
|
11
12
|
|
|
13
|
+
/** The calendar, a percent indicator's figures in percent on every row (`withPercentUnits`). */
|
|
12
14
|
export async function fetchEconCalendar(): Promise<EconEvent[]> {
|
|
13
15
|
const events = await apiClient.getCloudEconomicCalendar();
|
|
14
|
-
return events.map(toEconEvent);
|
|
16
|
+
return withPercentUnits(events.map(toEconEvent));
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
/** The stored calendar from a day, or why it was not read so the caller reads the current calendar instead. */
|
|
@@ -40,7 +42,7 @@ export async function fetchEconCalendarFrom(from: string): Promise<EconCalendarF
|
|
|
40
42
|
if (!payload || !Array.isArray(payload.events) || !(payload.storedFrom === null || /^\d{4}-\d{2}-\d{2}$/.test(String(payload.storedFrom)))) {
|
|
41
43
|
return { events: null, failure: "the server returned an unreadable calendar" };
|
|
42
44
|
}
|
|
43
|
-
return { events: payload.events.map(toEconEvent), storedFrom: payload.storedFrom };
|
|
45
|
+
return { events: withPercentUnits(payload.events.map(toEconEvent)), storedFrom: payload.storedFrom };
|
|
44
46
|
} catch (error) {
|
|
45
47
|
if (error instanceof ApiRequestError && error.status === 404) return { events: null, failure: null };
|
|
46
48
|
if (isPermanentClientError(error)) throw error;
|
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
type StatItem,
|
|
19
19
|
} from "../../../components";
|
|
20
20
|
import { spanDigits } from "../../../components/chart-table";
|
|
21
|
+
import { savedCopyFooterInfo } from "../../../components/layout/pane/footer";
|
|
22
|
+
import { isPermanentClientError } from "../../../api-client/errors";
|
|
21
23
|
import { getTableWidth, hasMeaningfulTableHorizontalOverflow } from "../../../components/ui/table-layout";
|
|
22
24
|
import type { CompositeAxisDomain } from "../../../components/chart/composite/types";
|
|
23
25
|
import {
|
|
@@ -31,7 +33,7 @@ import { Box, Text } from "../../../ui";
|
|
|
31
33
|
import { displayWidth, truncateToDisplayWidth } from "../../../utils/format";
|
|
32
34
|
import { useOpenTickerChoice } from "../shared/ticker-choice";
|
|
33
35
|
import { resolveFredMapping, projectFredHistory, fredHistoryUnits } from "./fred-series-map";
|
|
34
|
-
import { actualColor, CALENDAR_TIME_ZONE_LABEL, timeLabel } from "./calendar-model";
|
|
36
|
+
import { actualColor, CALENDAR_TIME_ZONE_LABEL, econEventName, timeLabel } from "./calendar-model";
|
|
35
37
|
import { hasNoStatedTime, isAllDayEvent, type EconEvent } from "./types";
|
|
36
38
|
|
|
37
39
|
interface EconDetailViewProps {
|
|
@@ -123,6 +125,8 @@ export function EconDetailView({ event, width, height, focused }: EconDetailView
|
|
|
123
125
|
const [loading, setLoading] = useState(false);
|
|
124
126
|
const [error, setError] = useState<string | null>(null);
|
|
125
127
|
const [freshnessWarning, setFreshnessWarning] = useState<string | null>(null);
|
|
128
|
+
// The history's refresh failed and the copy saved at this time (ms) answered instead.
|
|
129
|
+
const [savedAt, setSavedAt] = useState<number | null>(null);
|
|
126
130
|
const [data, setData] = useState<FredSeriesData | null>(null);
|
|
127
131
|
// Null follows the latest period.
|
|
128
132
|
const [selectedDate, setSelectedDate] = useState<string | null>(null);
|
|
@@ -146,6 +150,7 @@ export function EconDetailView({ event, width, height, focused }: EconDetailView
|
|
|
146
150
|
setLoading(false);
|
|
147
151
|
setError(null);
|
|
148
152
|
setFreshnessWarning(null);
|
|
153
|
+
setSavedAt(null);
|
|
149
154
|
return;
|
|
150
155
|
}
|
|
151
156
|
|
|
@@ -156,6 +161,7 @@ export function EconDetailView({ event, width, height, focused }: EconDetailView
|
|
|
156
161
|
setLoading(false);
|
|
157
162
|
setError(null);
|
|
158
163
|
setFreshnessWarning(null);
|
|
164
|
+
setSavedAt(null);
|
|
159
165
|
return;
|
|
160
166
|
}
|
|
161
167
|
} else {
|
|
@@ -165,6 +171,7 @@ export function EconDetailView({ event, width, height, focused }: EconDetailView
|
|
|
165
171
|
setLoading(true);
|
|
166
172
|
setError(null);
|
|
167
173
|
setFreshnessWarning(null);
|
|
174
|
+
setSavedAt(null);
|
|
168
175
|
|
|
169
176
|
loadCachedFredSeries(
|
|
170
177
|
request,
|
|
@@ -174,12 +181,18 @@ export function EconDetailView({ event, width, height, focused }: EconDetailView
|
|
|
174
181
|
}),
|
|
175
182
|
)
|
|
176
183
|
.then((entry) => {
|
|
184
|
+
// A refusal or a reply that fails validation is thrown by the load, never covered by a saved copy.
|
|
185
|
+
const saved = entry.source === "stale-fallback";
|
|
177
186
|
setData(entry.data);
|
|
178
|
-
|
|
179
|
-
|
|
187
|
+
// A copy a failed refresh left says when it was saved, in the footer as every pane does;
|
|
188
|
+
// a series its source has not updated since is a note.
|
|
189
|
+
setSavedAt(saved ? entry.fetchedAt : null);
|
|
190
|
+
setFreshnessWarning(entry.stale && !saved
|
|
191
|
+
? `Source not updated since ${new Date(entry.fetchedAt).toISOString().slice(0, 10)}.`
|
|
180
192
|
: null);
|
|
181
193
|
})
|
|
182
194
|
.catch((err) => {
|
|
195
|
+
if (isPermanentClientError(err)) setData(null);
|
|
183
196
|
setError(err instanceof Error ? err.message : String(err));
|
|
184
197
|
})
|
|
185
198
|
.finally(() => {
|
|
@@ -199,6 +212,7 @@ export function EconDetailView({ event, width, height, focused }: EconDetailView
|
|
|
199
212
|
// which asks which one when there are several.
|
|
200
213
|
const openTickerChoice = useOpenTickerChoice();
|
|
201
214
|
const relatedTickers = useMemo(() => mapping?.relatedTickers ?? [], [mapping]);
|
|
215
|
+
usePaneFooter("econ-detail:saved", () => ({ info: savedCopyFooterInfo([savedAt]) }), [savedAt]);
|
|
202
216
|
usePaneFooter("econ-detail:related", () => relatedTickers.length > 0 ? {
|
|
203
217
|
hints: [{
|
|
204
218
|
id: "related",
|
|
@@ -238,7 +252,7 @@ export function EconDetailView({ event, width, height, focused }: EconDetailView
|
|
|
238
252
|
const rows = useMemo<HistoryRow[]>(() => projected
|
|
239
253
|
.map((obs, index) => ({ date: obs.date, value: obs.value, change: index > 0 ? obs.value - projected[index - 1]!.value : null }))
|
|
240
254
|
.reverse(), [projected]);
|
|
241
|
-
const label = event
|
|
255
|
+
const label = econEventName(event);
|
|
242
256
|
const series = useMemo(() => [staticSeries(
|
|
243
257
|
projected.map((obs) => scalarPoint(new Date(`${obs.date}T00:00:00Z`), obs.value)),
|
|
244
258
|
{ id: "fred-history", label, color: colors.positive, calendarSpaced: true },
|
|
@@ -70,3 +70,51 @@ export function econRowFigures(row: { actual?: string | null; forecast?: string
|
|
|
70
70
|
scale: first?.scale ?? null,
|
|
71
71
|
};
|
|
72
72
|
}
|
|
73
|
+
|
|
74
|
+
/** A figure printed as a bare number, with no unit and no scale letter: `0.6`, `-3`, `1,5`. */
|
|
75
|
+
const BARE_NUMBER = /^[+-]?(?:\d+(?:[.,]\d+)?|\.\d+)$/;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Release names that are a percent whatever one row's figures say: a change
|
|
79
|
+
* month on month, quarter on quarter or year on year, and the rates published
|
|
80
|
+
* in percent. "Consumer Inflation Expectations" is a balance, not a rate, and
|
|
81
|
+
* is not one of them.
|
|
82
|
+
*/
|
|
83
|
+
const PERCENT_NAME = /\b(?:mom|qoq|yoy)\b|(?:^|\s)[mqy]\/[mqy](?=\s|$)|\b(?:inflation|unemployment|jobless|interest|growth|mortgage|funds|participation|savings) rate\b/i;
|
|
84
|
+
|
|
85
|
+
type EconRowText = { country: string; event: string; indicatorId?: string | null; actual: string | null; forecast: string | null; prior: string | null };
|
|
86
|
+
|
|
87
|
+
const FIGURE_KEYS = ["actual", "forecast", "prior"] as const;
|
|
88
|
+
|
|
89
|
+
/** Every release of one indicator: its id, else its country and name. */
|
|
90
|
+
const indicatorKey = (event: Pick<EconRowText, "country" | "event" | "indicatorId">) => event.indicatorId || `${event.country}|${event.event}`;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The calendar with every figure of a percent indicator in percent. The feeds
|
|
94
|
+
* drop the sign on some rows: US Inflation Rate MoM's prior came as `0.6`, and
|
|
95
|
+
* the 30-year mortgage rate's as `7.32` beside its actual `7.41%`. An
|
|
96
|
+
* indicator is in percent when any of its figures is, or its name says so
|
|
97
|
+
* (`PERCENT_NAME`), and none of its figures is in another unit or scale; its
|
|
98
|
+
* bare figures then carry `%`. Anything else is left as the feed wrote it.
|
|
99
|
+
*/
|
|
100
|
+
export function withPercentUnits<T extends EconRowText>(events: readonly T[]): T[] {
|
|
101
|
+
const indicators = new Map<string, { percent: boolean; other: boolean }>();
|
|
102
|
+
for (const event of events) {
|
|
103
|
+
const key = indicatorKey(event);
|
|
104
|
+
const seen = indicators.get(key) ?? { percent: false, other: false };
|
|
105
|
+
if (PERCENT_NAME.test(event.event)) seen.percent = true;
|
|
106
|
+
for (const field of FIGURE_KEYS) {
|
|
107
|
+
const figure = parseEconFigure(event[field]);
|
|
108
|
+
if (!figure) continue;
|
|
109
|
+
if (figure.unit === "%") seen.percent = true;
|
|
110
|
+
else if (figure.unit || figure.scale !== 1) seen.other = true;
|
|
111
|
+
}
|
|
112
|
+
indicators.set(key, seen);
|
|
113
|
+
}
|
|
114
|
+
return events.map((event) => {
|
|
115
|
+
const seen = indicators.get(indicatorKey(event))!;
|
|
116
|
+
if (!seen.percent || seen.other) return event;
|
|
117
|
+
const bare = FIGURE_KEYS.filter((field) => BARE_NUMBER.test(event[field]?.trim() ?? ""));
|
|
118
|
+
return bare.length ? { ...event, ...Object.fromEntries(bare.map((field) => [field, `${event[field]!.trim()}%`])) } : event;
|
|
119
|
+
});
|
|
120
|
+
}
|