gloomberb 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
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type PaneFooterSegment,
|
|
9
9
|
type PaneHint,
|
|
10
10
|
} from "../../../components";
|
|
11
|
+
import { savedCopyFooterInfo } from "../../../components/layout/pane/footer";
|
|
11
12
|
import { useAsyncResource } from "../../../react/async-resource";
|
|
12
13
|
import { colors } from "../../../theme/colors";
|
|
13
14
|
import {
|
|
@@ -119,8 +120,9 @@ function RiskFactorsReader({
|
|
|
119
120
|
const reportLoader = useCallback((force: boolean) => guard(loadRiskReport(ticker!, year!, { force, listing })), [guard, listing, ticker, year]);
|
|
120
121
|
const detail = useAsyncResource(ticker && year !== null ? reportLoader : null, { clearOnError: isPermanentClientError });
|
|
121
122
|
const report = detail.data;
|
|
122
|
-
|
|
123
|
-
const
|
|
123
|
+
// A copy saved earlier answering a failed refresh says when it was saved, in the footer every pane uses, not as an error.
|
|
124
|
+
const listError = list.error ?? (list.data?.savedAt == null ? list.data?.refreshError : null) ?? null;
|
|
125
|
+
const reportError = detail.error ?? (report?.savedAt == null ? report?.refreshError : null) ?? null;
|
|
124
126
|
const refresh = useCallback(() => {
|
|
125
127
|
void list.reload();
|
|
126
128
|
if (year !== null) void detail.reload();
|
|
@@ -151,12 +153,7 @@ function RiskFactorsReader({
|
|
|
151
153
|
info.push({ id: "loading", parts: [{ text: "loading", tone: "muted" }] });
|
|
152
154
|
if (listError) info.push({ id: "list-error", parts: [{ text: `Report list: ${listError}`, tone: "warning" }] });
|
|
153
155
|
if (reportError) info.push({ id: "report-error", parts: [{ text: `${year} report: ${reportError}`, tone: "warning" }] });
|
|
154
|
-
|
|
155
|
-
info.push({ id: "list-stale", parts: [{ text: `List cached ${new Date(list.data.fetchedAt).toISOString()}`, tone: "muted" }] });
|
|
156
|
-
}
|
|
157
|
-
if ((reportError || report?.stale) && report) {
|
|
158
|
-
info.push({ id: "report-stale", parts: [{ text: `Report cached ${new Date(report.fetchedAt).toISOString()}`, tone: "muted" }] });
|
|
159
|
-
}
|
|
156
|
+
info.push(...savedCopyFooterInfo([list.data?.savedAt, report?.savedAt]));
|
|
160
157
|
const hints: PaneHint[] = report?.docUrl
|
|
161
158
|
? [{ id: "open", key: "o", label: "pen filing", onPress: openFiling }]
|
|
162
159
|
: [];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { PluginModule } from "../plugin-module";
|
|
2
|
+
import { RoadmapPane } from "./pane";
|
|
3
|
+
|
|
4
|
+
export const roadmapModule: PluginModule = {
|
|
5
|
+
panes: [
|
|
6
|
+
{
|
|
7
|
+
id: "roadmap",
|
|
8
|
+
name: "Roadmap",
|
|
9
|
+
icon: "R",
|
|
10
|
+
component: RoadmapPane,
|
|
11
|
+
reportFreshness: { status: "not-a-feed", basis: "curated list" },
|
|
12
|
+
defaultPosition: "right",
|
|
13
|
+
defaultMode: "floating",
|
|
14
|
+
defaultFloatingSize: { width: 100, height: 30 },
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
|
|
18
|
+
paneTemplates: [
|
|
19
|
+
{
|
|
20
|
+
id: "roadmap-pane",
|
|
21
|
+
paneId: "roadmap",
|
|
22
|
+
label: "Roadmap",
|
|
23
|
+
description: "What is being built, what is planned and what shipped. Pro members vote on what comes next.",
|
|
24
|
+
keywords: ["roadmap", "planned", "building", "shipped", "upcoming", "features", "vote", "votes", "request"],
|
|
25
|
+
shortcut: { prefix: "ROADMAP", aliases: ["RMAP"] },
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { RoadmapItem, RoadmapPayload, RoadmapStatus, RoadmapVote } from "../../../api-client/roadmap";
|
|
2
|
+
import type { TableSection } from "../../../components";
|
|
3
|
+
|
|
4
|
+
export const ROADMAP_STATUS_LABELS: Record<RoadmapStatus, string> = {
|
|
5
|
+
building: "Building",
|
|
6
|
+
planned: "Planned",
|
|
7
|
+
shipped: "Shipped",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const SECTION_ORDER: readonly RoadmapStatus[] = ["building", "planned", "shipped"];
|
|
11
|
+
|
|
12
|
+
/** Building, then Planned, then Shipped, each in the order the service sent; an empty group is left out. */
|
|
13
|
+
export function roadmapSections(items: readonly RoadmapItem[]): TableSection<RoadmapItem>[] {
|
|
14
|
+
return SECTION_ORDER.map((status) => ({
|
|
15
|
+
label: ROADMAP_STATUS_LABELS[status],
|
|
16
|
+
key: status,
|
|
17
|
+
items: items.filter((item) => item.status === status),
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Votes are taken on what is not shipped yet; a shipped item's count is final. */
|
|
22
|
+
export function isVotable(item: RoadmapItem): boolean {
|
|
23
|
+
return item.status !== "shipped";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The items with this session's own votes on top: the count and mark a vote
|
|
28
|
+
* changed, held until the next read of the list replaces them.
|
|
29
|
+
*/
|
|
30
|
+
export function withLocalVotes(
|
|
31
|
+
payload: RoadmapPayload | null,
|
|
32
|
+
local: Readonly<Record<string, RoadmapVote>>,
|
|
33
|
+
): RoadmapItem[] {
|
|
34
|
+
if (!payload) return [];
|
|
35
|
+
return payload.items.map((item) => {
|
|
36
|
+
const vote = local[item.id];
|
|
37
|
+
return vote ? { ...item, votes: vote.votes, voted: vote.voted } : item;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** What the row shows while a vote is on its way: the mark flipped and the count moved by one. */
|
|
42
|
+
export function optimisticVote(item: RoadmapItem, voted: boolean): RoadmapVote {
|
|
43
|
+
const step = voted === item.voted ? 0 : voted ? 1 : -1;
|
|
44
|
+
return { id: item.id, votes: Math.max(0, item.votes + step), voted };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The votes cell: the count, with a check before it when this account voted. */
|
|
48
|
+
export function votesText(item: RoadmapItem): string {
|
|
49
|
+
return item.voted ? `✓ ${item.votes}` : String(item.votes);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** What one vote that failed is called on its row, and the reason the detail gives. */
|
|
53
|
+
export interface VoteFailure {
|
|
54
|
+
status: string;
|
|
55
|
+
reason: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function voteFailure(error: unknown, voted: boolean): VoteFailure {
|
|
59
|
+
const status = typeof error === "object" && error && "status" in error ? (error as { status?: unknown }).status : undefined;
|
|
60
|
+
const action = voted ? "The vote" : "Taking the vote back";
|
|
61
|
+
if (status === 401) return { status: "sign in", reason: `${action} did not go through: sign in again to vote.` };
|
|
62
|
+
if (status === 402 || status === 403) return { status: "Pro only", reason: `${action} did not go through: voting is for verified Pro members.` };
|
|
63
|
+
if (status === 404) return { status: "removed", reason: "This item is no longer on the roadmap." };
|
|
64
|
+
if (status === 409) return { status: "closed", reason: "This item shipped, so its votes are final." };
|
|
65
|
+
const message = error instanceof Error && error.message.trim() ? error.message.trim() : "the request failed";
|
|
66
|
+
return { status: "failed", reason: `${action} did not go through: ${message.replace(/\.$/, "")}.` };
|
|
67
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type RefObject } from "react";
|
|
2
|
+
import { apiClient } from "../../../api-client";
|
|
3
|
+
import { usePlanAccess } from "../../../api-client/plan-access";
|
|
4
|
+
import type { RoadmapItem, RoadmapVote } from "../../../api-client/roadmap";
|
|
5
|
+
import {
|
|
6
|
+
buildSectionedRows,
|
|
7
|
+
DataTableStackView,
|
|
8
|
+
EMPTY_TABLE_CELL,
|
|
9
|
+
isSectionedItemRow,
|
|
10
|
+
renderSectionedRowHeader,
|
|
11
|
+
usePaneStatusFooter,
|
|
12
|
+
useRowNotes,
|
|
13
|
+
type DataTableCell,
|
|
14
|
+
type DataTableColumn,
|
|
15
|
+
type PaneFooterSegment,
|
|
16
|
+
type PaneHint,
|
|
17
|
+
type SectionedRow,
|
|
18
|
+
} from "../../../components";
|
|
19
|
+
import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
|
|
20
|
+
import { DetailReader, type DetailFact } from "../../../components/reader/detail-reader";
|
|
21
|
+
import { useAsyncResource, usePluginPaneState } from "../../../public/react";
|
|
22
|
+
import { useThemeColors } from "../../../theme/theme-context";
|
|
23
|
+
import type { PaneProps } from "../../../types/plugin";
|
|
24
|
+
import { Box, TextAttributes, type ScrollBoxRenderable } from "../../../ui";
|
|
25
|
+
import { formatShortDate } from "../../../utils/datetime-format";
|
|
26
|
+
import {
|
|
27
|
+
isVotable,
|
|
28
|
+
optimisticVote,
|
|
29
|
+
ROADMAP_STATUS_LABELS,
|
|
30
|
+
roadmapSections,
|
|
31
|
+
voteFailure,
|
|
32
|
+
votesText,
|
|
33
|
+
withLocalVotes,
|
|
34
|
+
type VoteFailure,
|
|
35
|
+
} from "./model";
|
|
36
|
+
|
|
37
|
+
/** The roadmap calls the pane makes; tests hand it their own. */
|
|
38
|
+
export type RoadmapClient = Pick<typeof apiClient, "getRoadmap" | "setRoadmapVote">;
|
|
39
|
+
export const RoadmapClientContext = createContext<RoadmapClient | null>(null);
|
|
40
|
+
|
|
41
|
+
type Row = SectionedRow<RoadmapItem>;
|
|
42
|
+
type ColumnId = "title" | "blurb" | "votes" | "shipped";
|
|
43
|
+
type Column = DataTableColumn & { id: ColumnId };
|
|
44
|
+
|
|
45
|
+
const rowKey = (row: Row) => row.key;
|
|
46
|
+
const itemKey = (item: RoadmapItem) => item.id;
|
|
47
|
+
const voteSignature = (item: RoadmapItem) => `${item.votes}:${item.voted}`;
|
|
48
|
+
|
|
49
|
+
/** Said once, quietly, to everyone who cannot vote: no upgrade pitch. */
|
|
50
|
+
const PRO_VOTES_SEGMENT: PaneFooterSegment = { id: "pro-votes", parts: [{ text: "Pro members vote", tone: "muted" }] };
|
|
51
|
+
const NO_SEGMENTS: PaneFooterSegment[] = [];
|
|
52
|
+
|
|
53
|
+
function shippedDate(item: RoadmapItem): string {
|
|
54
|
+
return item.shippedAt ? formatShortDate(item.shippedAt, { fallback: "" }) : "";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** `votesCell` is what the votes cell says: the count, or a vote's failure on that row. */
|
|
58
|
+
function buildColumns(items: readonly RoadmapItem[], width: number, votesCell: (item: RoadmapItem) => string): Column[] {
|
|
59
|
+
const longest = Math.max(8, ...items.map((item) => item.title.length));
|
|
60
|
+
const narrow = width < 64;
|
|
61
|
+
const columns: Column[] = narrow
|
|
62
|
+
? [{ id: "title", label: "Feature", width: Math.min(longest, 24), flexGrow: 1, align: "left" }]
|
|
63
|
+
: [
|
|
64
|
+
{ id: "title", label: "Feature", width: Math.min(longest, width >= 120 ? 36 : 26), align: "left" },
|
|
65
|
+
{ id: "blurb", label: "Summary", width: 16, flexGrow: 1, align: "left" },
|
|
66
|
+
];
|
|
67
|
+
columns.push({ id: "votes", label: "Votes", width: Math.max(5, ...items.map((item) => votesCell(item).length)), align: "right" });
|
|
68
|
+
if (!narrow && items.some((item) => item.shippedAt)) {
|
|
69
|
+
columns.push({ id: "shipped", label: "Shipped", width: 12, align: "left" });
|
|
70
|
+
}
|
|
71
|
+
return columns;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function RoadmapDetail({ item, failure, width, scrollRef }: {
|
|
75
|
+
item: RoadmapItem;
|
|
76
|
+
failure: VoteFailure | null;
|
|
77
|
+
width: number;
|
|
78
|
+
scrollRef: RefObject<ScrollBoxRenderable | null>;
|
|
79
|
+
}) {
|
|
80
|
+
const colors = useThemeColors();
|
|
81
|
+
// The stack title names the item; the facts and the blurb follow.
|
|
82
|
+
const facts: DetailFact[] = [
|
|
83
|
+
{ label: "Status", value: ROADMAP_STATUS_LABELS[item.status] },
|
|
84
|
+
{ label: "Votes", value: item.voted ? `${item.votes}, yours included` : String(item.votes) },
|
|
85
|
+
...(item.shippedAt ? [{ label: "Shipped", value: shippedDate(item) }] : []),
|
|
86
|
+
...(failure ? [{ label: "Vote", value: failure.reason, color: colors.negative }] : []),
|
|
87
|
+
];
|
|
88
|
+
return (
|
|
89
|
+
<DetailReader
|
|
90
|
+
ref={scrollRef}
|
|
91
|
+
resetScrollKey={item.id}
|
|
92
|
+
width={width}
|
|
93
|
+
facts={facts}
|
|
94
|
+
body={item.blurb || null}
|
|
95
|
+
bodyFormat="text"
|
|
96
|
+
/>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function RoadmapPane({ focused, width, height }: PaneProps) {
|
|
101
|
+
const colors = useThemeColors();
|
|
102
|
+
const client = useContext(RoadmapClientContext) ?? apiClient;
|
|
103
|
+
const access = usePlanAccess();
|
|
104
|
+
// A sign-in, a sign-out or a new plan asks again: the votes and the right to
|
|
105
|
+
// cast one are the account's. The new loader starts from no list, so the
|
|
106
|
+
// last account's marks and vote hint go at once, not when (or if) the new
|
|
107
|
+
// read lands. A refresh of the same session keeps its list.
|
|
108
|
+
const sessionKey = JSON.stringify([apiClient.getCurrentUser()?.id ?? null, access.emailVerified, access.hasProAccess]);
|
|
109
|
+
const loader = useCallback((_force: boolean) => client.getRoadmap(), [client, sessionKey]);
|
|
110
|
+
const resource = useAsyncResource(loader);
|
|
111
|
+
const sessionRef = useRef(sessionKey);
|
|
112
|
+
sessionRef.current = sessionKey;
|
|
113
|
+
const payload = resource.data;
|
|
114
|
+
const canVote = payload?.canVote === true;
|
|
115
|
+
|
|
116
|
+
// This session's votes, over the list until the next read replaces it.
|
|
117
|
+
const [localVotes, setLocalVotes] = useState<Record<string, RoadmapVote>>({});
|
|
118
|
+
useEffect(() => setLocalVotes({}), [resource.updatedAt]);
|
|
119
|
+
const items = useMemo(() => withLocalVotes(payload, localVotes), [payload, localVotes]);
|
|
120
|
+
const rows = useMemo(() => buildSectionedRows(roadmapSections(items), itemKey), [items]);
|
|
121
|
+
const { noteFor, setNote, clearNotes } = useRowNotes<RoadmapItem, VoteFailure>(items, { key: itemKey, signature: voteSignature });
|
|
122
|
+
// Another account's votes and refusals are not this one's.
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
setLocalVotes({});
|
|
125
|
+
clearNotes();
|
|
126
|
+
}, [sessionKey, clearNotes]);
|
|
127
|
+
|
|
128
|
+
const [selectedId, setSelectedId] = usePluginPaneState<string | null>("selectedItemId", null);
|
|
129
|
+
const [openId, setOpenId] = usePluginPaneState<string | null>("openItemId", null);
|
|
130
|
+
const selected = items.find((item) => item.id === selectedId) ?? rows.find(isSectionedItemRow)?.item ?? null;
|
|
131
|
+
const open = openId ? items.find((item) => item.id === openId) ?? null : null;
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
// An item that left the roadmap closes once the list that dropped it is in.
|
|
134
|
+
if (openId && payload && !open) setOpenId(null);
|
|
135
|
+
}, [open, openId, payload]);
|
|
136
|
+
const detailScrollRef = useRef<ScrollBoxRenderable>(null);
|
|
137
|
+
|
|
138
|
+
const inFlight = useRef(new Set<string>());
|
|
139
|
+
const reload = resource.reload;
|
|
140
|
+
const toggleVote = useCallback(async (item: RoadmapItem) => {
|
|
141
|
+
if (!canVote || !isVotable(item) || inFlight.current.has(item.id)) return;
|
|
142
|
+
const voted = !item.voted;
|
|
143
|
+
const session = sessionRef.current;
|
|
144
|
+
inFlight.current.add(item.id);
|
|
145
|
+
setNote(item.id, null);
|
|
146
|
+
setLocalVotes((current) => ({ ...current, [item.id]: optimisticVote(item, voted) }));
|
|
147
|
+
try {
|
|
148
|
+
const answer = await client.setRoadmapVote(item.id, voted);
|
|
149
|
+
// An answer for the account that just left changes nothing on screen.
|
|
150
|
+
if (sessionRef.current !== session) return;
|
|
151
|
+
setLocalVotes((current) => ({ ...current, [item.id]: answer }));
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (sessionRef.current !== session) return;
|
|
154
|
+
setLocalVotes((current) => ({ ...current, [item.id]: { id: item.id, votes: item.votes, voted: item.voted } }));
|
|
155
|
+
setNote(item.id, voteFailure(error, voted));
|
|
156
|
+
} finally {
|
|
157
|
+
inFlight.current.delete(item.id);
|
|
158
|
+
}
|
|
159
|
+
}, [canVote, client, setNote]);
|
|
160
|
+
|
|
161
|
+
usePaneRefreshKey(() => void reload(), { focused });
|
|
162
|
+
|
|
163
|
+
const target = open ?? selected;
|
|
164
|
+
const hints = useMemo<PaneHint[]>(() => (
|
|
165
|
+
canVote && target && isVotable(target)
|
|
166
|
+
? [{ id: "vote", key: "v", label: target.voted ? " unvote" : "ote", title: target.voted ? "Take vote back" : "Vote", onPress: () => void toggleVote(target) }]
|
|
167
|
+
: []
|
|
168
|
+
), [canVote, target, toggleVote]);
|
|
169
|
+
usePaneStatusFooter({
|
|
170
|
+
registrationId: "roadmap",
|
|
171
|
+
// A failed refresh keeps the list; the failure goes in the footer.
|
|
172
|
+
loading: resource.loading && !!payload,
|
|
173
|
+
error: payload ? resource.error : null,
|
|
174
|
+
info: !canVote && items.length > 0 ? [PRO_VOTES_SEGMENT] : NO_SEGMENTS,
|
|
175
|
+
hints,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const columns = useMemo(
|
|
179
|
+
() => buildColumns(items, width, (item) => noteFor(item)?.status ?? votesText(item)),
|
|
180
|
+
[items, noteFor, width],
|
|
181
|
+
);
|
|
182
|
+
const renderCell = useCallback((row: Row, column: Column): DataTableCell => {
|
|
183
|
+
if (!isSectionedItemRow(row)) return EMPTY_TABLE_CELL;
|
|
184
|
+
const item = row.item;
|
|
185
|
+
switch (column.id) {
|
|
186
|
+
case "title":
|
|
187
|
+
return { text: item.title, color: colors.textBright };
|
|
188
|
+
case "blurb":
|
|
189
|
+
return { text: item.blurb, color: colors.textDim };
|
|
190
|
+
case "votes": {
|
|
191
|
+
const failure = noteFor(item);
|
|
192
|
+
if (failure) return { text: failure.status, color: colors.negative };
|
|
193
|
+
return {
|
|
194
|
+
text: votesText(item),
|
|
195
|
+
value: item.votes,
|
|
196
|
+
color: item.voted ? colors.positive : isVotable(item) ? colors.text : colors.textDim,
|
|
197
|
+
...(item.voted ? { attributes: TextAttributes.BOLD } : {}),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
case "shipped":
|
|
201
|
+
return { text: shippedDate(item), color: colors.textDim };
|
|
202
|
+
}
|
|
203
|
+
}, [colors, noteFor]);
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<DataTableStackView<Row, Column>
|
|
207
|
+
focused={focused}
|
|
208
|
+
detailOpen={!!open}
|
|
209
|
+
onBack={() => setOpenId(null)}
|
|
210
|
+
detailTitle={open?.title}
|
|
211
|
+
detailContent={open ? (
|
|
212
|
+
<RoadmapDetail item={open} failure={noteFor(open)} width={width} scrollRef={detailScrollRef} />
|
|
213
|
+
) : <Box flexGrow={1} />}
|
|
214
|
+
detailScrollRef={detailScrollRef}
|
|
215
|
+
rootWidth={width}
|
|
216
|
+
rootHeight={height}
|
|
217
|
+
// A refresh keeps the list on screen; only a first read waits or fails in the body.
|
|
218
|
+
status={payload ? undefined : {
|
|
219
|
+
loading: resource.loading,
|
|
220
|
+
error: resource.error,
|
|
221
|
+
subject: "Roadmap",
|
|
222
|
+
retry: { onRetry: reload, pending: resource.loading },
|
|
223
|
+
}}
|
|
224
|
+
emptyStateTitle="Nothing on the roadmap yet."
|
|
225
|
+
columns={columns}
|
|
226
|
+
items={rows}
|
|
227
|
+
getItemKey={rowKey}
|
|
228
|
+
isNavigable={isSectionedItemRow}
|
|
229
|
+
renderSectionHeader={renderSectionedRowHeader}
|
|
230
|
+
selection={{
|
|
231
|
+
kind: "id",
|
|
232
|
+
selectedId: selected?.id ?? null,
|
|
233
|
+
getId: rowKey,
|
|
234
|
+
onChange: (id) => setSelectedId(String(id)),
|
|
235
|
+
}}
|
|
236
|
+
onActivate={(row) => { if (isSectionedItemRow(row)) setOpenId(row.item.id); }}
|
|
237
|
+
sortColumnId={null}
|
|
238
|
+
sortDirection="asc"
|
|
239
|
+
renderCell={renderCell}
|
|
240
|
+
selectedTextOverridesCellColor
|
|
241
|
+
showHorizontalScrollbar={false}
|
|
242
|
+
/>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type LiveScan,
|
|
18
18
|
} from "./live-model";
|
|
19
19
|
import { LIVE_SCAN_INDEXES } from "../../../api-client/live-scan";
|
|
20
|
+
import { formatHiloPrice } from "./hilo-model";
|
|
20
21
|
|
|
21
22
|
/** `all`, an index (`sp500`) or a sector (`Energy`), as `--universe` takes it. */
|
|
22
23
|
function parseHeadlessUniverse(value: string): LiveScan["universe"] {
|
|
@@ -61,7 +62,24 @@ function emptyText(payload: LiveScanPayload): string {
|
|
|
61
62
|
|
|
62
63
|
const number = (value: unknown) => (typeof value === "number" ? value : undefined);
|
|
63
64
|
|
|
65
|
+
/** `2026-10-27 19:59 UTC`: when the scan last ran, to the minute. */
|
|
66
|
+
const utcMinute = (at: number) => `${new Date(at).toISOString().slice(0, 16).replace("T", " ")} UTC`;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* What the report says above the matches: outside the session that the
|
|
70
|
+
* market is closed and when the scan last ran, since a delay means nothing
|
|
71
|
+
* then; in the session a delayed feed's delay.
|
|
72
|
+
*/
|
|
73
|
+
function headerNotice(payload: LiveScanPayload): string | null {
|
|
74
|
+
if (payload.status === "closed") {
|
|
75
|
+
const last = payload.asOf ?? payload.takenAt;
|
|
76
|
+
return last != null && Number.isFinite(last) ? `Market closed; last scan ${utcMinute(last)}` : "Market closed";
|
|
77
|
+
}
|
|
78
|
+
return payload.access === "delayed" ? `Delayed ${payload.delayMinutes} minutes` : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
64
81
|
function projectLiveScan(payload: LiveScanPayload, scan: LiveScan): HeadlessSnapshotResult {
|
|
82
|
+
const notice = headerNotice(payload);
|
|
65
83
|
return {
|
|
66
84
|
asOf: new Date(payload.asOf ?? payload.takenAt ?? Date.now()).toISOString(),
|
|
67
85
|
empty: emptyText(payload),
|
|
@@ -80,7 +98,7 @@ function projectLiveScan(payload: LiveScanPayload, scan: LiveScan): HeadlessSnap
|
|
|
80
98
|
...(payload.status === "degraded" ? { errors: ["Live scanner degraded"] } : {}),
|
|
81
99
|
freshness: freshness(payload),
|
|
82
100
|
metadata: {
|
|
83
|
-
...(
|
|
101
|
+
...(notice ? { notices: [notice] } : {}),
|
|
84
102
|
...(payload.warnings.length ? { warnings: payload.warnings } : {}),
|
|
85
103
|
status: payload.status,
|
|
86
104
|
access: payload.access,
|
|
@@ -117,7 +135,8 @@ export const liveScanHeadless: HeadlessPaneDefinition<"snapshot"> = {
|
|
|
117
135
|
columns: [
|
|
118
136
|
{ key: "symbol", header: "Symbol" },
|
|
119
137
|
{ key: "name", header: "Name" },
|
|
120
|
-
|
|
138
|
+
// At the pane's precision: cents, or four decimals under a dollar; the JSON keeps the feed's digits.
|
|
139
|
+
{ key: "price", header: "Price", align: "right", format: (value) => typeof value === "number" ? formatHiloPrice(value) : "--" },
|
|
121
140
|
{ key: "changePercent", header: "Chg %", align: "right", format: (value) => formatPercentRaw(number(value)) },
|
|
122
141
|
{ key: "relativeVolume", header: "RVol", align: "right", format: (value) => formatMultiple(number(value)) },
|
|
123
142
|
{ key: "volume", header: "Volume", align: "right" },
|
|
@@ -1,12 +1,34 @@
|
|
|
1
1
|
import type { PaneLock } from "../../../types/plugin";
|
|
2
|
-
import {
|
|
2
|
+
import { isOptionsUnavailableError } from "../../../market-data/options-alternatives";
|
|
3
|
+
import { resolveHeadlessInstrument } from "../shared/headless-market-data";
|
|
4
|
+
import { proLockNote, type FreeRoute } from "../shared/pro-lock-note";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The listing's option chain, which needs no account, where options are
|
|
8
|
+
* served for it: LHA:XETRA has none, and `options LHA:XETRA` says so.
|
|
9
|
+
*/
|
|
10
|
+
const OPTIONS_ROUTE: FreeRoute = {
|
|
11
|
+
text: (ticker, company) => `gloom options ${ticker} lists ${company}'s option chain, each contract with its volume and open interest.`,
|
|
12
|
+
answers: async (symbol, context) => {
|
|
13
|
+
const { symbol: ticker, exchange } = await resolveHeadlessInstrument(context, symbol);
|
|
14
|
+
if (!context.marketData.getOptionsChain) return false;
|
|
15
|
+
try {
|
|
16
|
+
const chain = await context.marketData.getOptionsChain(ticker, exchange ?? "");
|
|
17
|
+
return chain.expirationDates.length + chain.calls.length + chain.puts.length > 0;
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (isOptionsUnavailableError(error)) return false;
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
};
|
|
3
24
|
|
|
4
25
|
/**
|
|
5
26
|
* FLOW is Pro whole: the one scanner with no delayed tier to fall back to.
|
|
6
27
|
* The options chain needs no account and lists each contract's day volume and
|
|
7
28
|
* open interest, the raw counts behind a sweep or a block, without the prints.
|
|
8
29
|
* FLOW takes a ticker only to narrow the market to one name, so the note
|
|
9
|
-
* repeats it only when the reader gave one
|
|
30
|
+
* repeats it only when the reader gave one, and only when the listing has
|
|
31
|
+
* options served: FLOW's prints are of the same US options.
|
|
10
32
|
*/
|
|
11
33
|
export function flowLockNote(lock: PaneLock): Promise<string> {
|
|
12
34
|
return proLockNote(lock, {
|
|
@@ -14,6 +36,7 @@ export function flowLockNote(lock: PaneLock): Promise<string> {
|
|
|
14
36
|
argument: "optional",
|
|
15
37
|
does: "it streams real-time sweeps, blocks and large premium prints across US options exchanges",
|
|
16
38
|
shows: "it",
|
|
17
|
-
free: [
|
|
39
|
+
free: [OPTIONS_ROUTE],
|
|
40
|
+
noneFor: (ticker) => `Options are not available for ${ticker}, so FLOW has no prints of it.`,
|
|
18
41
|
});
|
|
19
42
|
}
|
|
@@ -20,6 +20,7 @@ import { fetchFundFacts, fundFactEntries, fundFactPublishers, fundFactsEmpty, fu
|
|
|
20
20
|
import { REPORT_ROWS } from "../shared/headless-options";
|
|
21
21
|
import { fundAbroadProfile, venueAbroad, type FundAbroadProfile } from "./fund-abroad";
|
|
22
22
|
import { listingAnnouncementsPage } from "../../../utils/exchange-announcements";
|
|
23
|
+
import { companyDataNotes } from "../../../types/company-from";
|
|
23
24
|
|
|
24
25
|
const SEC_COLUMNS: HeadlessPaneColumn[] = [
|
|
25
26
|
{
|
|
@@ -140,7 +141,7 @@ export function createSecHeadless(dependencies: SecHeadlessDependencies = defaul
|
|
|
140
141
|
return {
|
|
141
142
|
rows,
|
|
142
143
|
textColumns: secTextColumns(rows),
|
|
143
|
-
...(note ? { notes: [note] } : {}),
|
|
144
|
+
...(note || filings[0]?.companyFrom ? { notes: [...(note ? [note] : []), ...companyDataNotes(filings[0]?.companyFrom)] } : {}),
|
|
144
145
|
...(form ? { empty: `No ${form} filings found for ${symbol}.` }
|
|
145
146
|
: page ? { empty: `${symbol} files no SEC reports. ${page.name}: ${page.url}` } : {}),
|
|
146
147
|
metadata: {
|
|
@@ -12,6 +12,7 @@ import { Box, Text, useUiCapabilities, type ScrollBoxRenderable } from "../../..
|
|
|
12
12
|
import { FILL_REST } from "../../../components/ui/fill";
|
|
13
13
|
import { t, tf } from "../../../i18n";
|
|
14
14
|
import { EmptyState, ExternalLinkText, FeedDataTableStackView, PaneLinkMenu, QueryBar, Spinner, TickerBadgeList, usePaneNoticeFooter, useTableLoadMore, type FeedDataTableItem, type PaneHint, type QueryBarFilter } from "../../../components";
|
|
15
|
+
import { useCompanyDataFooter } from "../../../components/layout/pane/status-footer";
|
|
15
16
|
import { displayWidth } from "../../../utils/format";
|
|
16
17
|
import type { DetailFact } from "../../../components/reader/detail-reader";
|
|
17
18
|
import type { ThemeColors } from "../../../theme/colors";
|
|
@@ -466,6 +467,8 @@ function SecView({ width, height, focused, paneType }: { width: number; height:
|
|
|
466
467
|
showOpenHint: !!(openFiling ? openFiling.filingUrl : factsUrl ?? abroadPage ?? announcements),
|
|
467
468
|
});
|
|
468
469
|
|
|
470
|
+
// A foreign line's filings are its company's, read from the home line.
|
|
471
|
+
useCompanyDataFooter(issuerFilings[0]?.companyFrom, !openFiling);
|
|
469
472
|
usePaneNoticeFooter({
|
|
470
473
|
registrationId: "sec:form-filter",
|
|
471
474
|
notices: filterHistoryCapped
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { logFailure } from "../../../utils/log-failure";
|
|
2
|
-
import { ApiRequestError
|
|
3
|
-
import type
|
|
2
|
+
import { ApiRequestError } from "../../../api-client/errors";
|
|
3
|
+
import { savedCopyForTransientFailure, type PluginCacheResult } from "../../../data/plugin-cache";
|
|
4
4
|
|
|
5
5
|
interface CloudCache<T> {
|
|
6
6
|
get(key: string, options?: { allowExpired?: boolean }): PluginCacheResult<T> | null;
|
|
@@ -12,6 +12,8 @@ export interface CloudResource<T> {
|
|
|
12
12
|
payload: T;
|
|
13
13
|
stale: boolean;
|
|
14
14
|
refreshError: string | null;
|
|
15
|
+
/** When the copy shown was saved, when it is one because its refresh failed. */
|
|
16
|
+
savedAt?: number;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
/**
|
|
@@ -36,8 +38,11 @@ const isAbort = (error: unknown) => !!error && typeof error === "object" && (err
|
|
|
36
38
|
|
|
37
39
|
/**
|
|
38
40
|
* Loads through the cache, falling back to the cached copy when a refresh
|
|
39
|
-
* fails
|
|
40
|
-
*
|
|
41
|
+
* fails for a reason asking again may get past (`isTransientFailure`). A
|
|
42
|
+
* refusal still throws: a refused session, so the pane shows its sign-in wall
|
|
43
|
+
* instead of data the account can no longer see, any other permanent client
|
|
44
|
+
* error (an unknown series, a route not served), and a reply that fails
|
|
45
|
+
* validation.
|
|
41
46
|
*
|
|
42
47
|
* Callers of one key share the request in flight, which runs on the first
|
|
43
48
|
* caller's `signal`. When that caller goes away (a tab left while it loads,
|
|
@@ -57,14 +62,22 @@ export async function loadCloudResource<T>(
|
|
|
57
62
|
throw error;
|
|
58
63
|
});
|
|
59
64
|
if (!result || cancelledByOther(result.error)) result = await cache.load(key, fetch, { force });
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
const saved = savedCopyForTransientFailure(result).source === "stale-fallback";
|
|
66
|
+
// A copy served because its refresh failed is one by where it came from, whatever the failure said (or did not).
|
|
67
|
+
return {
|
|
68
|
+
payload: validate(result.data), stale: result.stale, refreshError: result.refreshError ?? null,
|
|
69
|
+
...(saved ? { savedAt: result.fetchedAt } : {}),
|
|
70
|
+
};
|
|
62
71
|
}
|
|
63
72
|
|
|
64
73
|
/**
|
|
65
74
|
* Turns a route the server does not serve yet (404, or the given statuses)
|
|
66
75
|
* into a message that says what is unavailable; other errors pass through.
|
|
76
|
+
* The status stays, so a 404 is still a refusal no saved copy answers for and
|
|
77
|
+
* a 503 still a failure one may.
|
|
67
78
|
*/
|
|
68
79
|
export function unavailableOnServer(error: unknown, message: string, statuses: readonly number[] = [404]): unknown {
|
|
69
|
-
return error instanceof ApiRequestError && statuses.includes(error.status ?? 0)
|
|
80
|
+
return error instanceof ApiRequestError && statuses.includes(error.status ?? 0)
|
|
81
|
+
? new ApiRequestError(message, error.status, error.retryAfterMs, error.code, error.details)
|
|
82
|
+
: error;
|
|
70
83
|
}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { apiClient } from "../../../api-client";
|
|
2
2
|
import { loadCachedFredSeries } from "../../../sources/gloomberb-cloud/fred-series";
|
|
3
3
|
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* A FRED series from `start` on, oldest first, through the shared FRED cache;
|
|
6
|
+
* `savedAt` when a failed refresh left the copy saved then answering.
|
|
7
|
+
*/
|
|
5
8
|
export async function loadFredPoints(seriesId: string, start?: Date): Promise<{
|
|
6
9
|
points: Array<{ date: string; value: number | null }>;
|
|
7
10
|
stale: boolean;
|
|
8
11
|
refreshError?: string;
|
|
12
|
+
savedAt?: number;
|
|
9
13
|
}> {
|
|
10
14
|
const startDate = start ? start.toISOString().slice(0, 10) : "1900-01-01";
|
|
11
15
|
const result = await loadCachedFredSeries(
|
|
@@ -16,5 +20,6 @@ export async function loadFredPoints(seriesId: string, start?: Date): Promise<{
|
|
|
16
20
|
points: (result.data.observations ?? []).map((row) => ({ date: row.date, value: row.value })),
|
|
17
21
|
stale: result.stale,
|
|
18
22
|
...(result.refreshError ? { refreshError: result.refreshError } : {}),
|
|
23
|
+
...(result.source === "stale-fallback" ? { savedAt: result.fetchedAt } : {}),
|
|
19
24
|
};
|
|
20
25
|
}
|
|
@@ -6,7 +6,7 @@ import { clipPriceHistoryToRange } from "../../../time-series/history-window";
|
|
|
6
6
|
import { canonicalExchange, parsePublicTickerKey } from "../../../utils/exchanges";
|
|
7
7
|
import { nonUsSecListingVenue } from "../../../utils/sec";
|
|
8
8
|
|
|
9
|
-
export async function resolveHeadlessInstrument(ctx: HeadlessPaneContext, symbol: string) {
|
|
9
|
+
export async function resolveHeadlessInstrument(ctx: Pick<HeadlessPaneContext, "resolveInstrument">, symbol: string) {
|
|
10
10
|
return ctx.resolveInstrument ? ctx.resolveInstrument(symbol) : parsePublicTickerKey(symbol);
|
|
11
11
|
}
|
|
12
12
|
|
|
@@ -39,7 +39,7 @@ export async function resolveHeadlessIssuerListing(ctx: HeadlessPaneContext, key
|
|
|
39
39
|
return resolveIssuerListing(key, exchange, ctx.marketData);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
export async function loadHeadlessFinancials(ctx: HeadlessPaneContext, key: string) {
|
|
42
|
+
export async function loadHeadlessFinancials(ctx: Pick<HeadlessPaneContext, "marketData" | "resolveInstrument">, key: string) {
|
|
43
43
|
const { symbol, exchange } = await resolveHeadlessInstrument(ctx, key);
|
|
44
44
|
return ctx.marketData.getTickerFinancials(symbol, exchange ?? "");
|
|
45
45
|
}
|