gloomberb 0.18.2 → 0.18.3
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/package.json +1 -1
- package/patches/electrobun@1.18.1.patch +106 -5
- package/src/api-client/data.ts +15 -0
- package/src/api-client/index.ts +2 -0
- package/src/api-client/request.ts +5 -3
- package/src/api-client/telemetry.ts +17 -0
- package/src/app/global-shortcuts.ts +1 -16
- package/src/app/runtime/update.ts +28 -8
- package/src/architecture/pane-tabs-allowlist.json +65 -0
- package/src/cli/commands/market.ts +73 -33
- package/src/cli/commands/ticker.ts +82 -25
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +8 -0
- package/src/cli/issuer-membership.ts +103 -0
- package/src/cli/listing-arg.ts +15 -2
- package/src/cli/name-entries.ts +51 -12
- package/src/cli/pane-functions/capabilities.ts +2 -0
- package/src/cli/pane-functions/catalog.ts +16 -0
- package/src/cli/pane-functions/dom.ts +2 -1
- package/src/cli/pane-functions/headless.ts +25 -6
- package/src/cli/pane-functions/index.ts +64 -5
- package/src/cli/pane-functions/resolver.ts +19 -5
- package/src/cli/report-tables.ts +136 -18
- package/src/cli/result.ts +12 -4
- package/src/cli/symbol-list.ts +47 -11
- package/src/components/chart/composite/composite-chart.tsx +14 -5
- package/src/components/chart/composite/drawing-store.ts +26 -6
- package/src/components/chart/composite/interactions.ts +2 -1
- package/src/components/chart/composite/panel-surface.tsx +11 -0
- package/src/components/chart/composite/types.ts +6 -0
- package/src/components/chart/composite/use-composite-chart-keyboard.ts +1 -1
- package/src/components/command-bar/routes/root/command-items.ts +32 -6
- package/src/components/data-table/view.tsx +23 -3
- package/src/components/index.ts +1 -0
- package/src/components/layout/detached-pane-shell.tsx +1 -0
- package/src/components/layout/floating-pane.tsx +1 -0
- package/src/components/layout/header.tsx +228 -55
- package/src/components/layout/pane/header.tsx +10 -3
- package/src/components/layout/pane/index.tsx +1 -0
- package/src/components/layout/pane/pane-tabs.tsx +9 -1
- package/src/components/ui/stat-grid.tsx +28 -3
- package/src/components/ui/status.tsx +3 -1
- package/src/core/app-services.ts +4 -0
- package/src/i18n/es.ts +22 -0
- package/src/i18n/ja.ts +22 -0
- package/src/i18n/ko.ts +22 -0
- package/src/i18n/zh-cn.ts +22 -0
- package/src/i18n/zh-tw.ts +22 -0
- package/src/plugins/builtin/alerts/alert-engine.ts +3 -1
- package/src/plugins/builtin/alerts/events.ts +1 -0
- package/src/plugins/builtin/alerts/research-rules.ts +1 -0
- package/src/plugins/builtin/analytics/risk-pane.tsx +10 -7
- package/src/plugins/builtin/attention/pane.tsx +16 -14
- package/src/plugins/builtin/awards/pane.tsx +6 -6
- package/src/plugins/builtin/backtest/export.ts +1 -1
- package/src/plugins/builtin/broker-trading/dock.tsx +4 -2
- package/src/plugins/builtin/broker-trading/pane.tsx +6 -4
- package/src/plugins/builtin/broker-trading/ticket.tsx +10 -5
- package/src/plugins/builtin/buildout/pane/index.tsx +9 -6
- package/src/plugins/builtin/catalysts/client.ts +2 -2
- package/src/plugins/builtin/catalysts/pane.tsx +3 -3
- package/src/plugins/builtin/chart-composer/pane.tsx +1 -0
- package/src/plugins/builtin/commodity-balances/pane.tsx +3 -4
- package/src/plugins/builtin/company-attention/app-rank.tsx +4 -5
- package/src/plugins/builtin/company-attention/pane.tsx +13 -11
- package/src/plugins/builtin/company-kpis/pane.tsx +4 -3
- package/src/plugins/builtin/correlation/headless.ts +2 -1
- package/src/plugins/builtin/correlation/relationship/model.ts +1 -0
- package/src/plugins/builtin/correlation/settings.ts +1 -0
- package/src/plugins/builtin/cot/pane.tsx +4 -3
- package/src/plugins/builtin/credit-documents/pane.tsx +8 -7
- package/src/plugins/builtin/debt-maturities/pane.tsx +4 -3
- package/src/plugins/builtin/doe/pane.tsx +1 -1
- package/src/plugins/builtin/equity-screener/headless.ts +10 -0
- package/src/plugins/builtin/equity-screener/model.ts +28 -0
- package/src/plugins/builtin/equity-screener/pane.tsx +105 -98
- package/src/plugins/builtin/exposure/pane.tsx +10 -9
- package/src/plugins/builtin/gpu/board.tsx +4 -1
- package/src/plugins/builtin/gpu/changes.tsx +10 -10
- package/src/plugins/builtin/gpu/equities.tsx +4 -1
- package/src/plugins/builtin/gpu/history.tsx +4 -2
- package/src/plugins/builtin/gpu/pane.tsx +5 -3
- package/src/plugins/builtin/holders/client.ts +2 -1
- package/src/plugins/builtin/holders/headless.ts +2 -1
- package/src/plugins/builtin/holders/thirteenf-match.ts +3 -1
- package/src/plugins/builtin/insider/base-values.ts +11 -7
- package/src/plugins/builtin/insider/client.ts +3 -0
- package/src/plugins/builtin/insider/headless.ts +19 -6
- package/src/plugins/builtin/insider/index.tsx +17 -8
- package/src/plugins/builtin/insider/issuer.ts +34 -4
- package/src/plugins/builtin/insider/signals-client.ts +2 -2
- package/src/plugins/builtin/insider/table-model.ts +35 -13
- package/src/plugins/builtin/insider/table.tsx +5 -2
- package/src/plugins/builtin/iv-history/events.ts +145 -0
- package/src/plugins/builtin/iv-history/format.ts +3 -0
- package/src/plugins/builtin/iv-history/headless.ts +91 -7
- package/src/plugins/builtin/jodi/pane.tsx +4 -4
- package/src/plugins/builtin/market-movers/client.ts +4 -3
- package/src/plugins/builtin/market-movers/screener.ts +4 -3
- package/src/plugins/builtin/notes/cli.ts +1 -1
- package/src/plugins/builtin/notes/export.ts +17 -6
- package/src/plugins/builtin/notes/files.ts +16 -5
- package/src/plugins/builtin/options/enrichment-model.ts +10 -9
- package/src/plugins/builtin/options/model-spot.ts +70 -4
- package/src/plugins/builtin/options/view.tsx +41 -13
- package/src/plugins/builtin/options-calculator/pane.tsx +3 -2
- package/src/plugins/builtin/options-positioning/headless.ts +11 -3
- package/src/plugins/builtin/options-positioning/model.ts +24 -0
- package/src/plugins/builtin/options-positioning/pane.tsx +10 -12
- package/src/plugins/builtin/options-scenario/client.ts +19 -1
- package/src/plugins/builtin/options-scenario/headless.ts +13 -6
- package/src/plugins/builtin/options-scenario/pane.tsx +13 -5
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +5 -4
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
- package/src/plugins/builtin/power/client.ts +2 -2
- package/src/plugins/builtin/power/pane.tsx +36 -31
- package/src/plugins/builtin/reverse-dcf/client.ts +2 -1
- package/src/plugins/builtin/reverse-dcf/model.ts +4 -3
- package/src/plugins/builtin/seasonality/client.ts +2 -1
- package/src/plugins/builtin/sectors/client.ts +4 -3
- package/src/plugins/builtin/shared/cloud-resource.ts +15 -2
- package/src/plugins/builtin/shared/research-trading.tsx +2 -0
- package/src/plugins/builtin/shared/series-list-detail.tsx +3 -1
- package/src/plugins/builtin/shared/volatility/options.ts +2 -1
- package/src/plugins/builtin/social-mentions/client.ts +2 -1
- package/src/plugins/builtin/vol-surface/client.ts +23 -3
- package/src/plugins/builtin/vol-surface/headless.ts +33 -14
- package/src/plugins/builtin/vol-surface/model.ts +30 -10
- package/src/plugins/builtin/vol-surface/pane.tsx +33 -13
- package/src/plugins/builtin/vol-surface/stored.ts +34 -3
- package/src/plugins/builtin/vol-surface/tables.ts +14 -0
- package/src/renderers/dom/desktop/controls.tsx +1 -1
- package/src/renderers/dom/desktop/query-bar.tsx +2 -0
- package/src/renderers/dom/host/tabs.tsx +16 -5
- package/src/renderers/dom/host/top-surface.ts +127 -0
- package/src/renderers/dom/styles.css +39 -0
- package/src/renderers/electrobun/bun/core-capabilities.ts +9 -1
- package/src/renderers/electrobun/bun/desktop/backend.ts +10 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +210 -30
- package/src/renderers/electrobun/view/news-source.ts +4 -2
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +8 -1
- package/src/renderers/opentui/start.tsx +7 -0
- package/src/sources/gloomberb-cloud/index.ts +7 -0
- package/src/sources/provider-router/index.ts +16 -0
- package/src/telemetry/crash-reports.ts +4 -0
- package/src/telemetry/update-failures-node.ts +59 -0
- package/src/telemetry/update-failures.ts +307 -0
- package/src/tickers/company-name-listing.ts +9 -7
- package/src/tickers/company-name.ts +43 -0
- package/src/tickers/listing-fate.ts +30 -0
- package/src/types/data-provider.ts +6 -0
- package/src/types/headless.ts +7 -0
- package/src/types/issuer-listings.ts +28 -0
- package/src/updater/failure.ts +163 -0
- package/src/updater/index.ts +87 -23
- package/src/utils/currency-pair.ts +26 -0
- package/src/utils/sec.ts +68 -0
package/src/i18n/zh-cn.ts
CHANGED
|
@@ -1020,6 +1020,28 @@ export const zhCN: Record<string, string> = {
|
|
|
1020
1020
|
"Checking for updates...": "正在检查更新...",
|
|
1021
1021
|
"Downloading v{version}: {percent}%": "正在下载 v{version}:{percent}%",
|
|
1022
1022
|
"Update failed: {error}": "更新失败:{error}",
|
|
1023
|
+
"Update failed": "更新失败",
|
|
1024
|
+
"Update check failed: {reason}": "检查更新失败:{reason}",
|
|
1025
|
+
"no connection": "无网络连接",
|
|
1026
|
+
"connection timed out": "连接超时",
|
|
1027
|
+
"GitHub rate limit": "GitHub 请求限制",
|
|
1028
|
+
"release server error": "发布服务器错误",
|
|
1029
|
+
"unreadable release data": "无法读取发布信息",
|
|
1030
|
+
"no build for this system": "没有适用于此系统的版本",
|
|
1031
|
+
"download interrupted": "下载中断",
|
|
1032
|
+
"download corrupted": "下载已损坏",
|
|
1033
|
+
"no permission to replace the app": "没有替换应用的权限",
|
|
1034
|
+
"move the app to Applications": "请将应用移到“应用程序”文件夹",
|
|
1035
|
+
"app and update on different disks": "应用与更新位于不同的磁盘",
|
|
1036
|
+
"install folder is read-only": "安装文件夹为只读",
|
|
1037
|
+
"disk full": "磁盘已满",
|
|
1038
|
+
"app files in use": "应用文件正在使用",
|
|
1039
|
+
"downloaded update missing": "找不到已下载的更新",
|
|
1040
|
+
"check failed": "检查失败",
|
|
1041
|
+
"download failed": "下载失败",
|
|
1042
|
+
"could not verify the download": "无法验证下载",
|
|
1043
|
+
"could not install": "无法安装",
|
|
1044
|
+
"could not restart": "无法重新启动",
|
|
1023
1045
|
"v{version} available — run {command}": "v{version} 可用 — 执行 {command}",
|
|
1024
1046
|
"v{version} available, update through the package manager that installed Gloomberb": "v{version} 可用,请通过安装 Gloomberb 的包管理器更新",
|
|
1025
1047
|
"v{version} available — starting download...": "v{version} 可用 — 开始下载...",
|
package/src/i18n/zh-tw.ts
CHANGED
|
@@ -1020,6 +1020,28 @@ export const zhTW: Record<string, string> = {
|
|
|
1020
1020
|
"Checking for updates...": "正在檢查更新...",
|
|
1021
1021
|
"Downloading v{version}: {percent}%": "正在下載 v{version}:{percent}%",
|
|
1022
1022
|
"Update failed: {error}": "更新失敗:{error}",
|
|
1023
|
+
"Update failed": "更新失敗",
|
|
1024
|
+
"Update check failed: {reason}": "檢查更新失敗:{reason}",
|
|
1025
|
+
"no connection": "無網路連線",
|
|
1026
|
+
"connection timed out": "連線逾時",
|
|
1027
|
+
"GitHub rate limit": "GitHub 請求限制",
|
|
1028
|
+
"release server error": "發布伺服器錯誤",
|
|
1029
|
+
"unreadable release data": "無法讀取發布資訊",
|
|
1030
|
+
"no build for this system": "沒有適用於此系統的版本",
|
|
1031
|
+
"download interrupted": "下載中斷",
|
|
1032
|
+
"download corrupted": "下載已損毀",
|
|
1033
|
+
"no permission to replace the app": "沒有取代應用程式的權限",
|
|
1034
|
+
"move the app to Applications": "請將應用程式移到「應用程式」資料夾",
|
|
1035
|
+
"app and update on different disks": "應用程式與更新位於不同的磁碟",
|
|
1036
|
+
"install folder is read-only": "安裝資料夾為唯讀",
|
|
1037
|
+
"disk full": "磁碟已滿",
|
|
1038
|
+
"app files in use": "應用程式檔案正在使用中",
|
|
1039
|
+
"downloaded update missing": "找不到已下載的更新",
|
|
1040
|
+
"check failed": "檢查失敗",
|
|
1041
|
+
"download failed": "下載失敗",
|
|
1042
|
+
"could not verify the download": "無法驗證下載",
|
|
1043
|
+
"could not install": "無法安裝",
|
|
1044
|
+
"could not restart": "無法重新啟動",
|
|
1023
1045
|
"v{version} available — run {command}": "v{version} 可用 — 執行 {command}",
|
|
1024
1046
|
"v{version} available, update through the package manager that installed Gloomberb": "v{version} 可用,請透過安裝 Gloomberb 的套件管理器更新",
|
|
1025
1047
|
"v{version} available — starting download...": "v{version} 可用 — 開始下載...",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AlertCondition, AlertRule } from "./types";
|
|
2
|
+
import { logFailure } from "../../../utils/log-failure";
|
|
2
3
|
|
|
3
4
|
export type { AlertRule };
|
|
4
5
|
|
|
@@ -97,7 +98,8 @@ export function deserializeAlerts(json: string): AlertRule[] {
|
|
|
97
98
|
return parsed.filter((a: any) =>
|
|
98
99
|
a?.id && a?.symbol && a?.condition && typeof a?.targetPrice === "number"
|
|
99
100
|
);
|
|
100
|
-
} catch {
|
|
101
|
+
} catch (error) {
|
|
102
|
+
logFailure("alerts.alert-engine", "Read saved price alerts failed")(error);
|
|
101
103
|
return [];
|
|
102
104
|
}
|
|
103
105
|
}
|
|
@@ -135,6 +135,7 @@ export function readEventAlerts(json: string): { rules: EventAlertRule[]; error:
|
|
|
135
135
|
}
|
|
136
136
|
return { rules, error: null };
|
|
137
137
|
} catch {
|
|
138
|
+
// Intentional ignore: invalid saved rules return the error displayed by callers.
|
|
138
139
|
return { rules: [], error: "Saved event alerts could not be read." };
|
|
139
140
|
}
|
|
140
141
|
}
|
|
@@ -632,7 +632,7 @@ export function PortfolioRiskPane({ focused, width, height }: PaneProps) {
|
|
|
632
632
|
};
|
|
633
633
|
// The view strip takes a row when it is not in the title bar; the portfolio
|
|
634
634
|
// bar, the holdings summary and the chart sit on the root view only.
|
|
635
|
-
const { strip: tabStrip, rows: tabRows } = usePaneTabs(portfolio
|
|
635
|
+
const { strip: tabStrip, rows: tabRows } = usePaneTabs(portfolio ? {
|
|
636
636
|
tabs,
|
|
637
637
|
activeValue: view,
|
|
638
638
|
onSelect: selectView,
|
|
@@ -650,12 +650,15 @@ export function PortfolioRiskPane({ focused, width, height }: PaneProps) {
|
|
|
650
650
|
);
|
|
651
651
|
if (!model)
|
|
652
652
|
return (
|
|
653
|
-
<
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
653
|
+
<Box width={width} height={height} flexDirection="column">
|
|
654
|
+
{tabStrip}
|
|
655
|
+
<PaneStatusBody
|
|
656
|
+
loading={resource.loading}
|
|
657
|
+
error={resource.error}
|
|
658
|
+
loadingLabel="Loading portfolio risk"
|
|
659
|
+
emptyTitle="Risk data unavailable"
|
|
660
|
+
/>
|
|
661
|
+
</Box>
|
|
659
662
|
);
|
|
660
663
|
return (
|
|
661
664
|
<Box width={width} height={height} flexDirection="column">
|
|
@@ -123,7 +123,8 @@ export function AttentionPane({ width, height, focused }: PaneProps) {
|
|
|
123
123
|
priceChangePct: null, relativeVolume: null, marketAsOf: null, relativeVolumeAsOf: null, news: [],
|
|
124
124
|
} : undefined);
|
|
125
125
|
useAttentionEvidence((detailMode ? detailData : data) ?? null, tab, selectedSymbol, tab === "history" ? selected?.history.length ?? 0 : rows.length);
|
|
126
|
-
const
|
|
126
|
+
const signInWall = !data && isCloudSessionRequired(resource.error);
|
|
127
|
+
const { strip, rows: tabRows } = usePaneTabs(!signInWall ? { tabs: [...ATTENTION_TABS], activeValue: tab, onSelect: setTab, focused, dense: true } : null);
|
|
127
128
|
useAutoRefresh(resource.updatedAt, resource.load);
|
|
128
129
|
usePaneRefreshKey(() => { void resource.reload(); if (detailMode) void detail.reload(); }, { focused });
|
|
129
130
|
const hints: PaneHint[] = [
|
|
@@ -142,7 +143,7 @@ export function AttentionPane({ width, height, focused }: PaneProps) {
|
|
|
142
143
|
usePaneNoticeFooter({ registrationId: "attention:notices", focused, notices: [resource.data?.refreshError, detailMode ? detail.data?.refreshError : null,
|
|
143
144
|
data?.status === "ready" && selected?.zScore === null ? "The selected ticker has insufficient qualified history for an abnormal-attention score." : null,
|
|
144
145
|
].filter((value): value is string => !!value) });
|
|
145
|
-
if (
|
|
146
|
+
if (signInWall) return <SignInWall placement="attention-signin" action="view research attention" needsVerification={session.needsVerification} />;
|
|
146
147
|
const bodyHeight = Math.max(3, height - tabRows);
|
|
147
148
|
const queryBar = <QueryBar width={width} filters={[{ id: "window", label: "Period", inline: true, value: window, options: WINDOWS,
|
|
148
149
|
onChange: (value: string) => setWindow(value) }, ...(detailMode && data?.rows.length ? [{ id: "ticker", label: "Ticker", value: selectedSymbol,
|
|
@@ -190,24 +191,25 @@ export function AttentionPane({ width, height, focused }: PaneProps) {
|
|
|
190
191
|
...(topSector ? [{ id: "sector", label: "Top sector", ...topSector }] : []),
|
|
191
192
|
...(topCountry ? [{ id: "country", label: "Top country", ...topCountry }] : []),
|
|
192
193
|
] : [];
|
|
194
|
+
// The strip and the period filter stay while a new period loads; only the body waits.
|
|
193
195
|
return <Box width={width} height={height} flexDirection="column">{strip}
|
|
194
|
-
<
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
196
|
+
{detailMode ? <Box flexGrow={1} flexBasis={0} flexDirection="column">{queryBar}
|
|
197
|
+
<PaneStatusBody loading={!data ? resource.loading : !detailData && detail.loading} error={!data ? resource.error : !detailData ? detail.error : null}
|
|
198
|
+
empty={!!data && !!detailData && !selected} subject={!data ? "research attention" : "ticker attention"}
|
|
199
|
+
emptyTitle={data?.entitlement === "preview" ? "This ticker is outside the current preview." : "No privacy-qualified hours for this ticker."}>
|
|
200
|
+
{data && selected ? tab === "history" ? <History row={selected} width={width} height={bodyHeight - 1} focused={focused} preview={data.entitlement === "preview"} />
|
|
201
|
+
: <Evidence row={selected} data={detailData ?? data} width={width} height={bodyHeight - 1} /> : null}
|
|
202
|
+
</PaneStatusBody>
|
|
203
|
+
</Box> : <DataTableView columns={columns} items={data ? items : []} rootWidth={width} rootHeight={bodyHeight} focused={focused && !search.active}
|
|
204
|
+
status={!data ? { loading: resource.loading, error: resource.error, subject: "research attention" }
|
|
205
|
+
: data.status !== "ready" && !data.rows.length ? { empty: true, emptyTitle: emptyAttention(data), subject: "research attention" } : undefined}
|
|
206
|
+
rootBefore={<ChartTableHeader width={width} height={bodyHeight} tableRows={items.length} tableColumns={columns} query={queryBar} figures={figures} />}
|
|
204
207
|
getItemKey={rowId} selection={{ kind: "id", selectedId: groupMode ? groupSelectedId : selected?.symbol ?? null, getId: rowId, onChange: (id) => { if (groupMode) setGroupSelected(id); else if (!id.startsWith("locked:")) setSelected(id); } }}
|
|
205
208
|
onActivate={(row) => { if (isLocked(row)) openUpgrade(); else if ("symbol" in row) { setSelected(row.symbol); setTab("history"); } else { setGroupFilter({ field: tab === "countries" ? "country" : "sector", value: row.name }); setQuery(""); setSelected(null); setTab("ranking"); } }}
|
|
206
209
|
renderCell={cell} selectedTextOverridesCellColor showHorizontalScrollbar
|
|
207
210
|
sortColumnId={!groupMode ? tab === "abnormal" ? "zScore" : sort.column : null} sortDirection={tab === "abnormal" ? "desc" : sort.direction}
|
|
208
211
|
onHeaderClick={!groupMode && tab !== "abnormal" ? (column) => setSort((current) => ({ column, direction: current.column === column && current.direction === "desc" ? "asc" : "desc" })) : undefined}
|
|
209
212
|
emptyStateTitle={tab === "abnormal" ? "Collecting enough qualified history for abnormal attention." : "No published research matches."}
|
|
210
|
-
bodyAfter={hidden && desktop ? <LockedOverlay rows={Math.min(3, hidden)} text="Pro unlocks the full attention graph" onPress={openUpgrade} /> : undefined} />
|
|
211
|
-
</PaneStatusBody>
|
|
213
|
+
bodyAfter={hidden && desktop ? <LockedOverlay rows={Math.min(3, hidden)} text="Pro unlocks the full attention graph" onPress={openUpgrade} /> : undefined} />}
|
|
212
214
|
</Box>;
|
|
213
215
|
}
|
|
@@ -221,15 +221,16 @@ function AwardsView({ width, height, focused, symbol }: PaneProps & { symbol: st
|
|
|
221
221
|
if (denied || !data && isCloudSessionRequired(failed?.message)) return <SignInWall placement="awards-signin" action="view government awards" needsVerification={session.needsVerification} />;
|
|
222
222
|
return <Box width={width} height={height} flexDirection="column">
|
|
223
223
|
{strip}
|
|
224
|
-
<
|
|
225
|
-
empty={!data && failed?.message === AWARDS_UNAVAILABLE} emptyTitle={AWARDS_UNAVAILABLE} subject="government awards">
|
|
226
|
-
{data ? <PageStackView focused={focused} detailOpen={!!openId} onBack={() => setOpen(null)} detailTitle={openRow ? truncateToDisplayWidth(openRow.title, Math.max(20, width - 16)) : "Award evidence"}
|
|
224
|
+
<PageStackView focused={focused} detailOpen={!!openId} onBack={() => setOpen(null)} detailTitle={openRow ? truncateToDisplayWidth(openRow.title, Math.max(20, width - 16)) : "Award evidence"}
|
|
227
225
|
detailContent={openRow ? <AwardDetail key={openRow.id} row={openRow} accessKey={accessKey} width={width} height={bodyHeight - 1} focused={focused} openAward={setOpen} openUpgrade={openUpgrade}
|
|
228
226
|
openChildren={(type) => { setParentId(openRow.id); setTicker(""); setSource(""); setQuery(""); setAgency(""); setSector(""); setCurrency(""); setJurisdiction(""); setRange("all"); setType(type); setOpen(null); setTab("feed"); }} />
|
|
229
227
|
: <PaneStatusBody loading={detail.loading} error={detail.error} empty={!detail.loading && !detail.error} emptyTitle="Award no longer available." subject="award evidence" />}
|
|
230
228
|
rootContent={<DataTableView<Item> columns={columns} items={tab === "company" && !ticker ? [] : items} rootWidth={width} rootHeight={bodyHeight}
|
|
229
|
+
// A filter change loads under the query bar, which stays.
|
|
230
|
+
status={data ? undefined : { loading: paged.loading, error: failed?.message !== AWARDS_UNAVAILABLE ? failed?.message : null,
|
|
231
|
+
empty: failed?.message === AWARDS_UNAVAILABLE, emptyTitle: AWARDS_UNAVAILABLE, subject: "government awards" }}
|
|
231
232
|
focused={focused && !openId && !search.active && !tickerSearch.active} scrollRef={tableScrollRef} onBodyScrollActivity={loadMore}
|
|
232
|
-
rootBefore={<ChartTableHeader width={width} height={bodyHeight} tableRows={items.length} tableColumns={columns} query={queryBar} chart={chart}
|
|
233
|
+
rootBefore={<ChartTableHeader width={width} height={bodyHeight} tableRows={items.length} tableColumns={columns} query={queryBar} chart={data ? chart : null}
|
|
233
234
|
figures={tab === "company" && ticker && latest ? [{ id: "total", label: `Obligated ${chartCurrency}`, value: money(latest.cumulativeObligatedAmount) },
|
|
234
235
|
{ id: "period", label: "Latest cohort", value: latest.month.slice(0, 7), detail: awardScope(chartSource) }] : undefined} />}
|
|
235
236
|
selection={{ kind: "id", selectedId: selected ? itemId(selected) : null, getId: itemId, onChange: setSelected }} getItemKey={itemId}
|
|
@@ -277,7 +278,6 @@ function AwardsView({ width, height, focused, symbol }: PaneProps & { symbol: st
|
|
|
277
278
|
return { text: award.revenueComparison?.periodEnd ?? "--", color: colors.textDim };
|
|
278
279
|
}}
|
|
279
280
|
emptyStateTitle={tab === "company" && !ticker ? "Enter a company ticker." : tab === "events" ? "No revenue-comparable awards in this scope." : "No awards match these filters."}
|
|
280
|
-
bodyAfter={data
|
|
281
|
-
</PaneStatusBody>
|
|
281
|
+
bodyAfter={data?.locked && desktop ? <LockedOverlay rows={3} text="Upgrade for complete awards and history" onPress={openUpgrade} /> : undefined} />} />
|
|
282
282
|
</Box>;
|
|
283
283
|
}
|
|
@@ -15,7 +15,7 @@ export function backtestEquityCsv(report: HeadlessBundleResult, symbol: string,
|
|
|
15
15
|
const tables = report.sections.flatMap((section): CliReportTable[] => {
|
|
16
16
|
if (!section.rows) return [];
|
|
17
17
|
// The curve's one section, identified as `headlessReportTables` identifies it (no per-row units).
|
|
18
|
-
const table = exportRowsTable(section.title, section.columns ?? [], section.rows);
|
|
18
|
+
const table = exportRowsTable(section.title, section.columns ?? [], section.rows, { status: true });
|
|
19
19
|
return [{
|
|
20
20
|
...table,
|
|
21
21
|
identity: { ...(section.unit ? { currencyOrUnit: section.unit } : {}), ...(section.interval ? { interval: section.interval } : {}) },
|
|
@@ -89,8 +89,10 @@ export function ResearchTradeWorkspace({ width, height, focused, symbol, tabId,
|
|
|
89
89
|
}, [captureTicket, selectTab, tabId]);
|
|
90
90
|
const split = visible && docked;
|
|
91
91
|
const ticketFocused = split && panelFocused;
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
// The chart consumes its own presses, so the box below never hears them: it says so itself.
|
|
93
|
+
const focusChart = useCallback(() => setPanelFocused(false), []);
|
|
94
|
+
const context = useMemo(() => ({ ...projection, symbol, available, docked, visible, ticketFocused, open: openTicket, close, focusChart, publish, ...(available ? { orders } : {}) }),
|
|
95
|
+
[projection, symbol, available, docked, visible, ticketFocused, openTicket, close, focusChart, orders]);
|
|
94
96
|
useShortcut((event) => {
|
|
95
97
|
if (event.name !== "f6" || event.repeat) return;
|
|
96
98
|
event.preventDefault(); event.stopPropagation(); captureTicket(false); setPanelFocused((value) => !value);
|
|
@@ -174,13 +174,15 @@ function ProfileTicket({ broker, instance, contract, width, height, focused, ord
|
|
|
174
174
|
const timer = setInterval(refresh, 30_000);
|
|
175
175
|
return () => { active = false; clearInterval(timer); if (refreshResources.current === refresh) refreshResources.current = () => {}; };
|
|
176
176
|
}, [broker, instance, connected, visible, selectAccount, allowAutoAccount, statusVersion]);
|
|
177
|
-
// A new ticket's limit defaults to its side of the quote, from the first quote that has a price,
|
|
178
|
-
// or streamed, until the trader sets one. A stop is the trader's: at the touch it would trigger at
|
|
177
|
+
// A new ticket's limit defaults to its side of the quote, from the first current quote that has a price,
|
|
178
|
+
// asked for or streamed, until the trader sets one. A stop is the trader's: at the touch it would trigger at
|
|
179
|
+
// once. A stale quote (before the open, the last session's book) is no price to trade at: the ticket shows
|
|
180
|
+
// its age and waits for a current one, which a broker's stream brings within seconds.
|
|
179
181
|
const pricePending = useRef(false);
|
|
180
182
|
const prefillPrice = useCallback(() => {
|
|
181
183
|
const latest = controller.getSnapshot();
|
|
182
184
|
const currentDraft = latest.draft, q = latest.quote?.quote;
|
|
183
|
-
if (!pricePending.current || !currentDraft || !q || latest.phase !== "editing" || latest.modifying) return;
|
|
185
|
+
if (!pricePending.current || !currentDraft || !q || latest.quote?.stale !== false || latest.phase !== "editing" || latest.modifying) return;
|
|
184
186
|
const source = currentDraft.action === "BUY" && q.ask !== undefined ? "ask" : currentDraft.action === "SELL" && q.bid !== undefined ? "bid" : "last";
|
|
185
187
|
try {
|
|
186
188
|
if ((currentDraft.orderType === "LMT" || currentDraft.orderType === "STP LMT") && currentDraft.limitPrice === undefined) {
|
|
@@ -392,7 +394,7 @@ function ProfileTicket({ broker, instance, contract, width, height, focused, ord
|
|
|
392
394
|
brokerName: broker.name, symbol: orderInstrumentLabel(receipt?.request ?? draft ?? { contract: ticketContract }), accounts, accountId,
|
|
393
395
|
accountChoices, accountChoiceValue: accountChoices && accountId ? ticketAccountChoiceKey(instance.id, accountId) : undefined,
|
|
394
396
|
accountType: accounts.find((account) => account.accountId === accountId)?.accountType,
|
|
395
|
-
quote: state.quote?.quote, legQuotes: state.quote?.legs, quoteData: connection?.quoteData,
|
|
397
|
+
quote: state.quote?.quote, quoteStale: state.quote?.stale, legQuotes: state.quote?.legs, quoteData: connection?.quoteData,
|
|
396
398
|
phase: page === "enable" ? "enable" : state.phase, draft: receipt?.request ?? draft, preview: state.review?.preview ?? receipt?.preview, warnings: state.review?.warnings ?? [], result: state.result, resultQuantities: state.resultQuantities,
|
|
397
399
|
error: connection?.state === "error" && connection.message ? connection.message : error ?? state.error, reconnecting,
|
|
398
400
|
position: position?.quantity, avgCost: position?.avgCost, positionPnl: position?.pnl, tradingEnabled: caps?.enabled === true,
|
|
@@ -20,6 +20,8 @@ export interface TicketViewModel {
|
|
|
20
20
|
brokerName: string; symbol: string; accounts: BrokerAccount[]; accountId?: string;
|
|
21
21
|
accountChoices?: readonly BrokerAccountChoice[]; accountChoiceValue?: string;
|
|
22
22
|
quote?: Quote; quoteData?: "realtime" | "delayed";
|
|
23
|
+
/** The quote is not current (older than the ticket accepts, or of unknown age): it shows its age beside it. */
|
|
24
|
+
quoteStale?: boolean;
|
|
23
25
|
/** A spread's legs' quotes, in leg order. `quote` is then the spread's own. */
|
|
24
26
|
legQuotes?: Quote[];
|
|
25
27
|
phase: "enable" | "editing" | "previewing" | "review" | "submitting" | "result" | "cancel-review" | "cancelling";
|
|
@@ -119,10 +121,11 @@ function TicketNumberInput({ value, focused, onChange, ...props }: {
|
|
|
119
121
|
/**
|
|
120
122
|
* The docked quote: last price and change first, then bid in green and ask in
|
|
121
123
|
* red with their sizes, as a broker's quote panel reads. A delayed quote shows
|
|
122
|
-
* a clock whose tooltip names the source and its age
|
|
124
|
+
* a clock whose tooltip names the source and its age; a stale one says its age
|
|
125
|
+
* beside the price, so the last session's book never reads as the market.
|
|
123
126
|
*/
|
|
124
|
-
function DockedQuote({ quote, delayed, source, updated, price, change, width }: {
|
|
125
|
-
quote?: Quote; delayed: boolean; source: string; updated: string; price(value: number | undefined): string; change: string; width: number;
|
|
127
|
+
function DockedQuote({ quote, delayed, stale, source, updated, price, change, width }: {
|
|
128
|
+
quote?: Quote; delayed: boolean; stale?: string; source: string; updated: string; price(value: number | undefined): string; change: string; width: number;
|
|
126
129
|
}) {
|
|
127
130
|
const c = useThemeColors();
|
|
128
131
|
const native = useUiCapabilities().nativePaneChrome;
|
|
@@ -139,6 +142,7 @@ function DockedQuote({ quote, delayed, source, updated, price, change, width }:
|
|
|
139
142
|
<FigureText>{quote?.price === undefined ? "--" : price(quote.price)}</FigureText>
|
|
140
143
|
<Box flexGrow={1} minWidth={0}><FigureText part="sub" change={quote?.change}>{change}</FigureText></Box>
|
|
141
144
|
{delayed ? <Box flexShrink={0} title={`${source} · ${updated}`}><Icon name="clock" color={c.warning} /></Box> : null}
|
|
145
|
+
{stale ? <Text fg={c.warning} flexShrink={0}>{stale}</Text> : null}
|
|
142
146
|
</Box>
|
|
143
147
|
<Box flexDirection="row" gap={2}>
|
|
144
148
|
{side("Bid", quote?.bid, quote?.bidSize, c.positive)}
|
|
@@ -377,6 +381,7 @@ export function BrokerTicketView({ model: m, width, height, focused, onEdit, onA
|
|
|
377
381
|
const updated = sourceAt && Number.isFinite(sourceAt) ? `${new Date(sourceAt).toISOString().slice(11, 19)} UTC` : "unknown";
|
|
378
382
|
const delayed = m.quoteData !== "realtime" || m.quote?.dataSource !== "live";
|
|
379
383
|
const age = ticketQuoteAge(m.quote, m.now).replace(" old", "");
|
|
384
|
+
const stale = m.quote && m.quoteStale ? ticketQuoteAge(m.quote, m.now) : undefined;
|
|
380
385
|
const source = `${m.brokerName}, ${delayed ? "delayed" : "real-time"} ${age}`;
|
|
381
386
|
const accountPicker = (pickerWidth: number) => <BrokerAccountPicker
|
|
382
387
|
accounts={m.accountChoices ?? m.accounts.map((a) => ({ value: a.accountId, label: a.name, tradingMode: a.tradingMode }))}
|
|
@@ -402,7 +407,7 @@ export function BrokerTicketView({ model: m, width, height, focused, onEdit, onA
|
|
|
402
407
|
<Box ref={nodeRef("account")} flexShrink={1} minWidth={0}>{accountPicker(width - (onClose ? 6 : 2))}</Box>
|
|
403
408
|
{onClose ? <IconButton icon="close" label="Close trade panel" shortcut="Esc" onPress={onClose} /> : null}
|
|
404
409
|
</Box>
|
|
405
|
-
<DockedQuote quote={m.quote} delayed={delayed} source={source} updated={updated} price={quotePrice} change={change} width={width - 2} />
|
|
410
|
+
<DockedQuote quote={m.quote} delayed={delayed} stale={stale} source={source} updated={updated} price={quotePrice} change={change} width={width - 2} />
|
|
406
411
|
</Box> : <Box flexShrink={0}>
|
|
407
412
|
<Box flexDirection="column" data-gloom-selectable="true" paddingX={native ? 3 : 1} paddingY={native ? wide ? 1 : 0.5 : 0} gap={native ? wide ? 0.65 : 0.2 : 0} >
|
|
408
413
|
<Box flexDirection={narrow ? "column" : "row"} justifyContent="space-between" alignItems={narrow ? "flex-start" : "center"} gap={narrow ? 0.4 : 1}>
|
|
@@ -429,7 +434,7 @@ export function BrokerTicketView({ model: m, width, height, focused, onEdit, onA
|
|
|
429
434
|
<Box flexDirection="row" flexWrap={native ? "wrap" : "nowrap"} alignItems="center" gap={1}>
|
|
430
435
|
{/* A delayed quote is the yellow clock, as in pane footers, with the detail on hover. */}
|
|
431
436
|
{delayed ? <Box flexShrink={0} title={source}><Icon name="clock" color={c.warning} /></Box> : null}
|
|
432
|
-
<Text fg={c.textDim}>{delayed ? `${m.brokerName} ${age}` : source}</Text>
|
|
437
|
+
<Text fg={stale ? c.warning : c.textDim}>{stale ? `${m.brokerName}, ${stale}` : delayed ? `${m.brokerName} ${age}` : source}</Text>
|
|
433
438
|
<Text fg={c.textDim}>{`${updated}${m.synthetic && !wide && !narrow ? " · SYNTHETIC" : ""}`}</Text>
|
|
434
439
|
</Box>
|
|
435
440
|
</Box>
|
|
@@ -379,12 +379,15 @@ export function BuildoutPane({ focused, width, height }: PaneProps) {
|
|
|
379
379
|
variant: "bare",
|
|
380
380
|
});
|
|
381
381
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
382
|
+
// The strip stays over the first load and a failed one, as over every tab's own.
|
|
383
|
+
if (state.status === "loading" || state.status === "error") {
|
|
384
|
+
return (
|
|
385
|
+
<Box flexDirection="column" width={width} height={height} overflow="hidden">
|
|
386
|
+
{tabStrip && <Box height={1}>{tabStrip}</Box>}
|
|
387
|
+
{state.status === "loading" ? <PaneStatusBody loading subject={BUILDOUT_NAME} />
|
|
388
|
+
: <PaneStatusBody error={state.message} subject={BUILDOUT_NAME} retry={{ onRetry: refresh }} />}
|
|
389
|
+
</Box>
|
|
390
|
+
);
|
|
388
391
|
}
|
|
389
392
|
|
|
390
393
|
const listOpen = activeTab === "companies" && !!selectedList;
|
|
@@ -23,6 +23,6 @@ export function validateCatalystDetail(payload: CatalystDetail, id?: string): Ca
|
|
|
23
23
|
export const fetchCatalysts = async (query: CatalystFilters, client: Pick<typeof apiClient, "getCloudCatalysts"> = apiClient, signal?: AbortSignal) => validateCatalysts(await client.getCloudCatalysts(query, signal));
|
|
24
24
|
export const fetchCatalystDetail = async (id: string, client: Pick<typeof apiClient, "getCloudCatalystEvent"> = apiClient, signal?: AbortSignal, query: { offset?: number; limit?: number } = {}) => validateCatalystDetail(await client.getCloudCatalystEvent(id, signal, query), id);
|
|
25
25
|
export const loadCatalysts = (query: CatalystFilters, accessKey: string, force = false, signal?: AbortSignal) =>
|
|
26
|
-
loadCloudResource(catalystCache, `${accessKey}:${JSON.stringify(query)}`, () => fetchCatalysts(query, apiClient, signal), { force, validate: validateCatalysts });
|
|
26
|
+
loadCloudResource(catalystCache, `${accessKey}:${JSON.stringify(query)}`, () => fetchCatalysts(query, apiClient, signal), { force, validate: validateCatalysts, signal });
|
|
27
27
|
export const loadCatalystDetail = (id: string, accessKey: string, force = false, offset = 0, signal?: AbortSignal) =>
|
|
28
|
-
loadCloudResource(catalystDetailCache, `${accessKey}:${id}:${offset}`, () => fetchCatalystDetail(id, apiClient, signal, { offset, limit: 100 }), { force, validate: (payload) => validateCatalystDetail(payload, id) });
|
|
28
|
+
loadCloudResource(catalystDetailCache, `${accessKey}:${id}:${offset}`, () => fetchCatalystDetail(id, apiClient, signal, { offset, limit: 100 }), { force, validate: (payload) => validateCatalystDetail(payload, id), signal });
|
|
@@ -147,10 +147,11 @@ export function CatalystView({ width, height, focused, symbol, litigation = fals
|
|
|
147
147
|
if (isCloudSessionRequired(error?.message)) return <SignInWall placement="catalysts-signin" action="view catalysts" needsVerification={session.needsVerification} />;
|
|
148
148
|
return <Box width={width} height={height} flexDirection="column">
|
|
149
149
|
{strip}
|
|
150
|
-
<
|
|
151
|
-
<PageStackView focused={focused} detailOpen={!!openId} onBack={() => setOpenId("")} detailTitle={open?.title ?? "Catalyst"}
|
|
150
|
+
<PageStackView focused={focused} detailOpen={!!openId} onBack={() => setOpenId("")} detailTitle={open?.title ?? "Catalyst"}
|
|
152
151
|
detailContent={open ? <CatalystEventDetail key={open.id} event={open} accessKey={accessKey} snapshot={detailSnapshots[open.id]} width={width} height={Math.max(3, height - 1)} focused={focused} initialTab={detailTab} openUpgrade={openUpgrade} /> : <PaneStatusBody loading={externalOpen.loading} error={externalOpen.error} subject="catalyst event"><EmptyState title="Event unavailable." /></PaneStatusBody>}
|
|
153
152
|
rootContent={<DataTableView<Row> rootWidth={width} rootHeight={Math.max(3, height - tabRows)} rootBefore={queryBar} focused={focused && !openId && !searchActive && !dateFieldActive}
|
|
153
|
+
// A tab or filter change loads under the query bar, which stays.
|
|
154
|
+
status={{ loading: !data && resource.loading, error: !data || denied ? error?.message : null, subject: litigation ? "company litigation" : "catalysts" }}
|
|
154
155
|
items={items} sortColumnId={null} sortDirection="asc" columns={columns} getItemKey={rowKey}
|
|
155
156
|
isNavigable={isSelectable} renderSectionHeader={(row) => row.kind === "locked" ? null : renderSectionedRowHeader(row)}
|
|
156
157
|
selection={{ kind: "id", selectedId: selected?.id ?? null, getId: rowKey, onChange: (id) => { if (!id.startsWith("locked:")) setSelectedId(id); } }}
|
|
@@ -160,6 +161,5 @@ export function CatalystView({ width, height, focused, symbol, litigation = fals
|
|
|
160
161
|
emptyStateTitle={catalystEmptyTitle(data, query, tab === "changes")}
|
|
161
162
|
bodyAfter={lockedRows && desktop ? <LockedOverlay rows={lockedRows} text="Upgrade for every catalyst and its history" onPress={openUpgrade} role="catalysts-upgrade" /> : undefined} />}
|
|
162
163
|
/>
|
|
163
|
-
</PaneStatusBody>
|
|
164
164
|
</Box>;
|
|
165
165
|
}
|
|
@@ -1188,6 +1188,7 @@ function ChartComposerSurface({
|
|
|
1188
1188
|
showLatestChangePercent={!spec.viewport.dateWindow && spec.viewport.range === "1D"}
|
|
1189
1189
|
onViewportChange={handleChartViewportChange}
|
|
1190
1190
|
onActivate={activatePane}
|
|
1191
|
+
onPress={trade?.focusChart}
|
|
1191
1192
|
onToggleSeries={toggleSeries}
|
|
1192
1193
|
isSeriesToggleable={isSeriesToggleable}
|
|
1193
1194
|
timePick={timePick}
|
|
@@ -70,8 +70,7 @@ export function CommodityBalancesPane({ width, height, focused }: PaneProps) {
|
|
|
70
70
|
const [balanceId, setBalanceId] = usePluginPaneState<string | null>("selectedBalance", null);
|
|
71
71
|
const [priceId, setPriceId] = usePluginPaneState<string | null>("selectedPrice", null);
|
|
72
72
|
const prices = data?.prices ?? EMPTY_PRICES;
|
|
73
|
-
const
|
|
74
|
-
const activeTab = showPrices && tab === "prices" ? "prices" : "balances";
|
|
73
|
+
const activeTab = tab === "prices" ? "prices" : "balances";
|
|
75
74
|
const balanceRows = useMemo(
|
|
76
75
|
() => sortBalanceRows(data?.rows ?? EMPTY_ROWS, balanceSort),
|
|
77
76
|
[balanceSort, data?.rows],
|
|
@@ -84,7 +83,7 @@ export function CommodityBalancesPane({ width, height, focused }: PaneProps) {
|
|
|
84
83
|
const [shownRequest, setShownRequest] = usePluginPaneState("cropRequest", "");
|
|
85
84
|
// The row the request scrolls to the top, until the reader sorts or moves on.
|
|
86
85
|
const [cropAnchor, setCropAnchor] = useState<{ id: string; version: number } | null>(null);
|
|
87
|
-
const { strip, rows: tabRows } = usePaneTabs(
|
|
86
|
+
const { strip, rows: tabRows } = usePaneTabs({
|
|
88
87
|
tabs: TABS,
|
|
89
88
|
activeValue: activeTab,
|
|
90
89
|
onSelect: (value) => setTab(value),
|
|
@@ -92,7 +91,7 @@ export function CommodityBalancesPane({ width, height, focused }: PaneProps) {
|
|
|
92
91
|
compact: true,
|
|
93
92
|
variant: "underline",
|
|
94
93
|
queryBarWidth: width,
|
|
95
|
-
}
|
|
94
|
+
});
|
|
96
95
|
const updatedAgo = useUpdatedAgo(updatedAt);
|
|
97
96
|
const info = useMemo<PaneFooterSegment[]>(() => {
|
|
98
97
|
const year = data?.marketYear;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCallback, useMemo, useRef } from "react";
|
|
2
2
|
import type { AppRankPayload } from "../../../api-client/app-attention";
|
|
3
3
|
import { isAccessDenied } from "../../../api-client/errors";
|
|
4
|
-
import { ChartTableHeader, DataTableView,
|
|
4
|
+
import { ChartTableHeader, DataTableView, QueryBar, useChartTableSelection, usePagedRows, usePaneStatusFooter, usePaneTabs, useTableLoadMore, type DataTableColumn } from "../../../components";
|
|
5
5
|
import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
|
|
6
6
|
import { useAsyncResource, usePaneSettingValue, usePluginPaneState } from "../../../public/react";
|
|
7
7
|
import { useThemeColors } from "../../../theme/theme-context";
|
|
@@ -56,14 +56,14 @@ export function AppRankView({ focus, accessKey, width, height, focused }: { focu
|
|
|
56
56
|
const tableColumns = tab === "evidence" ? [...columns, { id: "present", label: "In chart", width: 10, align: "left" as const }, { id: "sourceUpdatedAt", label: "Published (UTC)", width: 17, align: "left" as const }, { id: "revisionId", label: "Revision", width: 30, align: "left" as const }] : columns;
|
|
57
57
|
return <Box flexDirection="column" width={width} height={height}>
|
|
58
58
|
{strip}
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
{/* A window change reloads under the query bar, which stays. */}
|
|
60
|
+
<DataTableView columns={tableColumns} items={points} status={data ? undefined : { loading: resource.loading, error: resource.error, subject: "app rank history" }} sortColumnId="date" sortDirection="desc" getItemKey={id} rootWidth={width} rootHeight={height - tabRows} focused={focused}
|
|
61
61
|
scrollRef={scroll} onBodyScrollActivity={more} selection={{ kind: "id", getId: id, selectedId: selected ? id(selected) : null, onChange: setSelected }}
|
|
62
62
|
onActivate={(point) => void host.openExternal(point.sourceUrl)}
|
|
63
63
|
rootBefore={<ChartTableHeader width={width} height={height - tabRows} tableRows={points.length} tableColumns={tableColumns}
|
|
64
64
|
query={<QueryBar width={width} meta={`${focus.country} · ${focus.chart}`} filters={[{ id: "days", label: "Window", value: days, options: ["30", "90", "365", "730"].map((value) => ({ value, label: `${value}D` })), onChange: setDays }]} />}
|
|
65
65
|
figures={points[0] ? [{ label: "Rank", value: count(points[0].rank) }, { label: "Rating", value: points[0].rating?.toFixed(2) ?? "--", detail: "/ 5" }, { label: "Ratings", value: count(points[0].ratingCount) }] : []}
|
|
66
|
-
chart={tab === "chart" ? { series, ...selection, formatValue: (value) => `#${count(value)}`, empty: series.length ? undefined : focus.chart === "unranked" ? "Rank unavailable for this source" : "Collecting rank history", remoteKind: "app-rank-history" } : null} />}
|
|
66
|
+
chart={data && tab === "chart" ? { series, ...selection, formatValue: (value) => `#${count(value)}`, empty: series.length ? undefined : focus.chart === "unranked" ? "Rank unavailable for this source" : "Collecting rank history", remoteKind: "app-rank-history" } : null} />}
|
|
67
67
|
renderCell={(point, column) => {
|
|
68
68
|
const value = point[column.id as keyof Point];
|
|
69
69
|
if (value == null) return { text: "--", value: null, color: colors.textMuted };
|
|
@@ -74,6 +74,5 @@ export function AppRankView({ focus, accessKey, width, height, focused }: { focu
|
|
|
74
74
|
color: column.id === "rank" ? colors.textBright : column.id === "revisionId" ? colors.textDim : colors.text };
|
|
75
75
|
}}
|
|
76
76
|
emptyStateTitle="No observations for this app and chart yet." selectedTextOverridesCellColor showHorizontalScrollbar />
|
|
77
|
-
</PaneStatusBody>
|
|
78
77
|
</Box>;
|
|
79
78
|
}
|
|
@@ -3,7 +3,7 @@ import { isAccessDenied } from "../../../api-client/errors";
|
|
|
3
3
|
import { usePlanAccess } from "../../../api-client/plan-access";
|
|
4
4
|
import type { AppAttentionFilter, AppAttentionPayload, AppChart } from "../../../api-client/app-attention";
|
|
5
5
|
import type { HiringBoard, HiringPayload } from "../../../api-client/hiring";
|
|
6
|
-
import { ChartTableHeader, DataTableView,
|
|
6
|
+
import { ChartTableHeader, DataTableView, KeyValueRow, PageStackView, PaneFooterScope, QueryBar, useChartTableSelection, usePagedRows, useTableLoadMore, usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs, type PaneHint } from "../../../components";
|
|
7
7
|
import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
|
|
8
8
|
import { useAsyncResource, useAutoRefresh, usePaneSettingValue, usePluginAppActions, usePluginPaneState } from "../../../public/react";
|
|
9
9
|
import { usePaneTickerIdentity } from "../../../state/hooks/pane-ticker";
|
|
@@ -119,7 +119,8 @@ function AttentionView({ kind, symbol, width, height, focused }: Size & { kind:
|
|
|
119
119
|
const detailOpen = !!openCompany || !!evidence || !!app;
|
|
120
120
|
const tabLabels: Record<AttentionTab, string> = { table: "Table", chart: "Chart", mix: kind === "hiring" ? "Mix" : "Markets", peers: kind === "apps" && !symbol ? "Companies" : "Peers", evidence: "Evidence" };
|
|
121
121
|
const tabs = attentionTabs(kind, symbol).map((value) => ({ value, label: tabLabels[value] }));
|
|
122
|
-
const
|
|
122
|
+
const signInWall = !data && isCloudSessionRequired(resource.error);
|
|
123
|
+
const { strip, rows: tabRows } = usePaneTabs(!signInWall && !openCompany && !app ? { tabs, activeValue: tab, onSelect: (value) => { setTab(value); setOpenEvidence(null); }, focused, dense: true } : null);
|
|
123
124
|
const bodyHeight = Math.max(3, height - tabRows);
|
|
124
125
|
const series = useMemo(() => model ? attentionSeries(model, colors.textBright) : [], [model, colors.textBright]);
|
|
125
126
|
const chartSelection = useChartTableSelection({ rows, getId: rowId, getDate: rowDate, selectedId: selected?.id ?? null, onSelect: setSelected, focused: focused && !detailOpen, enabled: tab === "chart" });
|
|
@@ -142,7 +143,7 @@ function AttentionView({ kind, symbol, width, height, focused }: Size & { kind:
|
|
|
142
143
|
usePaneStatusFooter({ registrationId: `${kind}:status`, enabled: !openCompany && !app, loading: resource.loading || paged.loadingMore, error: data ? resource.error ?? paged.moreError?.message ?? paged.error?.message : null, stale: resource.data?.stale,
|
|
143
144
|
info: [...(model?.asOf ? [{ id: "observed", asOf: true, parts: [{ text: `as of ${model.asOf.slice(0, 10)}`, tone: "muted" as const }] }] : []), ...(model?.preview ? [{ id: "preview", parts: [{ text: "Pro preview", tone: "warning" as const }] }] : [])], hints });
|
|
144
145
|
usePaneNoticeFooter({ registrationId: `${kind}:notices`, enabled: !openCompany && !app, focused, notices: [...(model?.notices ?? []), ...(resource.data?.refreshError ? [resource.data.refreshError] : [])] });
|
|
145
|
-
if (
|
|
146
|
+
if (signInWall) return <SignInWall placement={kind === "hiring" ? "hiring-momentum-signin" : "app-attention-signin"} action={kind === "hiring" ? "view hiring momentum" : "view app attention"} needsVerification={session.needsVerification} />;
|
|
146
147
|
const countryOptions = data && kind === "apps" ? (data as AppAttentionPayload).coverage.configuredCountries : [];
|
|
147
148
|
const query = kind === "hiring" && tab !== "mix" ? undefined : <QueryBar width={width} filters={kind === "apps" ? [
|
|
148
149
|
{ id: "country", label: "Country", value: country, defaultValue: "", options: [{ value: "", label: "All countries" }, ...countryOptions.map((value) => ({ value, label: value }))], onChange: setCountry },
|
|
@@ -152,12 +153,14 @@ function AttentionView({ kind, symbol, width, height, focused }: Size & { kind:
|
|
|
152
153
|
] : tab === "mix" ? [{ id: "mix", label: "Group", value: mix, options: [{ value: "functions", label: "Role family" }, { value: "seniority", label: "Seniority" }, { value: "countries", label: "Countries" }, { value: "locations", label: "Locations" }, { value: "signals", label: "Signals" }], onChange: setMix }] : []} />;
|
|
153
154
|
return <Box width={width} height={height} flexDirection="column">
|
|
154
155
|
{strip}
|
|
155
|
-
<
|
|
156
|
-
{model && section ? <PageStackView focused={focused} detailOpen={detailOpen} onBack={() => { setOpenCompany(null); setOpenEvidence(null); setOpenApp(null); }} detailTitle={openCompany ?? app?.name ?? (evidence ? String(evidence.values.title ?? evidence.values.name ?? evidence.id) : undefined)}
|
|
156
|
+
<PageStackView focused={focused} detailOpen={detailOpen} onBack={() => { setOpenCompany(null); setOpenEvidence(null); setOpenApp(null); }} detailTitle={openCompany ?? app?.name ?? (evidence ? String(evidence.values.title ?? evidence.values.name ?? evidence.id) : undefined)}
|
|
157
157
|
detailContent={openCompany ? <PaneFooterScope active><AttentionView key={openCompany} kind={kind} symbol={openCompany} width={width} height={bodyHeight - 1} focused={focused} /></PaneFooterScope> : app ? <PaneFooterScope active><AppRankView focus={app} accessKey={accessKey} width={width} height={bodyHeight - 1} focused={focused} /></PaneFooterScope> : evidence ? <Evidence row={evidence} width={width} height={bodyHeight - 1} focused={focused} /> : null}
|
|
158
|
-
rootContent={<DataTableView columns={columns} items={rows} rootWidth={width} rootHeight={bodyHeight} focused={focused && !detailOpen} scrollRef={scrollRef} onBodyScrollActivity={scrollMore}
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
rootContent={<DataTableView columns={columns} items={model && section ? rows : []} rootWidth={width} rootHeight={bodyHeight} focused={focused && !detailOpen} scrollRef={scrollRef} onBodyScrollActivity={scrollMore}
|
|
159
|
+
// A filter change reloads under the query bar, which stays.
|
|
160
|
+
status={!data ? { loading: resource.loading, error: resource.error, subject: kind === "hiring" ? "hiring momentum" : "app attention" }
|
|
161
|
+
: !model || !section ? { empty: true, emptyTitle: "Observations are not available yet." } : undefined}
|
|
162
|
+
rootBefore={<ChartTableHeader width={width} height={bodyHeight} tableRows={rows.length} tableColumns={columns} query={query} figures={model?.figures}
|
|
163
|
+
chart={model && tab === "chart" ? { series, ...chartSelection, formatValue: (value) => kind === "hiring" ? count(value) : value.toFixed(1), empty: series.length ? undefined : model.unmapped ?? (model.preview ? "Full history requires Pro."
|
|
161
164
|
: model.chart.since ? `Collecting history since ${model.chart.since.slice(0, 10)}` : "Collecting history"), remoteKind: `${kind}-history` } : null} />}
|
|
162
165
|
selection={{ kind: "id", selectedId: selected?.id ?? null, getId: rowId, onChange: setSelected }} getItemKey={rowId}
|
|
163
166
|
sortColumnId={sort.column} sortDirection={sort.direction} onHeaderClick={(column) => setSort((old) => ({ column, direction: old.column === column && old.direction === "desc" ? "asc" : "desc" }))}
|
|
@@ -181,8 +184,7 @@ function AttentionView({ kind, symbol, width, height, focused }: Size & { kind:
|
|
|
181
184
|
return { text, value, color: numeric && SIGNED_COLUMNS.has(column.id) ? signedColor(value, colors)
|
|
182
185
|
: column.id === "name" || column.id === "title" ? colors.textBright : column.id === "date" || column.id === "observed" || column.id === "confidence" ? colors.textDim : colors.text };
|
|
183
186
|
}} selectedTextOverridesCellColor showHorizontalScrollbar resetScrollKey={`${kind}:${symbol}:${tab}:${mix}:${country}:${chart}`}
|
|
184
|
-
emptyStateTitle={model
|
|
185
|
-
/>
|
|
186
|
-
</PaneStatusBody>
|
|
187
|
+
emptyStateTitle={model?.unmapped ?? section?.empty ?? ""} bodyAfter={lockedRows && desktop ? <LockedOverlay rows={lockedRows} text="Upgrade for all observations" onPress={upgrade} role="attention-upgrade" /> : undefined} />}
|
|
188
|
+
/>
|
|
187
189
|
</Box>;
|
|
188
190
|
}
|
|
@@ -100,7 +100,7 @@ function CompanyView({ symbol, exchange, name, mode, width, height, focused }: P
|
|
|
100
100
|
const guidance = data && "guidance" in data ? data : null;
|
|
101
101
|
const [openingTab] = usePaneSettingValue("tab", "table");
|
|
102
102
|
const [tabValue, setTab] = usePluginPaneState<string>("company:tab", openingTab);
|
|
103
|
-
const tab = ["table", "chart", "evidence", ...(guidance ? ["history"] : [])].includes(tabValue) ? tabValue : "table";
|
|
103
|
+
const tab = ["table", "chart", "evidence", ...(mode === "guidance" ? ["history"] : [])].includes(tabValue) ? tabValue : "table";
|
|
104
104
|
const [openingMetric] = usePaneSettingValue("metric", "all");
|
|
105
105
|
const [metric, setMetric] = usePluginPaneState<string>("company:metric", openingMetric);
|
|
106
106
|
const [plotKey, setPlotKey] = usePluginPaneState<string>("company:plot", "");
|
|
@@ -166,7 +166,8 @@ function CompanyView({ symbol, exchange, name, mode, width, height, focused }: P
|
|
|
166
166
|
: guidanceSeries(guides, selected && isGuidance(selected) ? selected : undefined, { low: colors.textMuted, high: colors.warning, actual: colors.positive }), [tab, mode, kpiChart, guides, selected, colors]);
|
|
167
167
|
const link = useChartTableSelection({ rows, getId: (row) => row.id, getDate: (row) => isGuidance(row) ? new Date(row.issuedDate) : kpiChart.dates.get(row.id) ?? null,
|
|
168
168
|
selectedId: selected?.id ?? null, onSelect: setSelected, focused: focused && !openRow, enabled: tab === "chart" });
|
|
169
|
-
const
|
|
169
|
+
const signInWall = !data && isCloudSessionRequired(resource.error);
|
|
170
|
+
const { strip, rows: tabRows } = usePaneTabs(symbol && !signInWall ? { tabs: [{ value: "table", label: mode === "guidance" ? "Tracker" : "Table" }, { value: "chart", label: "Chart" },
|
|
170
171
|
...(mode === "guidance" ? [{ value: "history", label: "History" }] : []), { value: "evidence", label: "Evidence" }], activeValue: tab,
|
|
171
172
|
onSelect: (value) => { setTab(value); setOpen(null); }, focused, dense: true } : null);
|
|
172
173
|
useAutoRefresh(resource.updatedAt, resource.load);
|
|
@@ -190,7 +191,7 @@ function CompanyView({ symbol, exchange, name, mode, width, height, focused }: P
|
|
|
190
191
|
data?.truncated && data.access === "full" ? "The response reached its stored-record limit. Narrow the metric or date range in a CLI or REST query." : null,
|
|
191
192
|
data?.coverage.conflicts ? "Conflicting disclosures are retained in Evidence and excluded from comparable trends." : null].filter((value): value is string => !!value) });
|
|
192
193
|
if (!symbol) return <EmptyState title="Select a ticker." />;
|
|
193
|
-
if (
|
|
194
|
+
if (signInWall) return <SignInWall placement="company-kpis-signin" action={mode === "guidance" ? "view company guidance" : "view company KPIs"} needsVerification={session.needsVerification} />;
|
|
194
195
|
const bodyHeight = Math.max(3, height - tabRows);
|
|
195
196
|
const query = <QueryBar width={width} filters={[{ id: "metric", label: "Metric", value: metric, defaultValue: "all", options: metricOptions,
|
|
196
197
|
onChange: (value: string) => { setMetric(value); setSelected(null); setOpen(null); } }, ...(tab === "chart" && plotOptions.length ? [{ id: "series", label: "Series", value: activePlot,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { HeadlessPaneDefinition } from "../../../types/headless";
|
|
2
2
|
import type { TimeRange } from "../../../time-series/range";
|
|
3
3
|
import { formatNumber, formatSignificant } from "../../../utils/format";
|
|
4
|
+
import { logFailure } from "../../../utils/log-failure";
|
|
4
5
|
import { mixedSessionCloseNote } from "../../../market-data/market/session-close-note";
|
|
5
6
|
import { resolveHeadlessInstrument, loadHeadlessSymbols } from "../shared/headless-market-data";
|
|
6
7
|
import { buildCorrelationMatrix, buildCorrelationSeries, buildGeoCorrelationSeries, pairKey, type CorrelationSeries } from "./matrix/model";
|
|
@@ -27,7 +28,7 @@ async function loadListedHistory(ctx: HeadlessPaneContext, key: string, range: T
|
|
|
27
28
|
const { symbol, exchange } = await resolveHeadlessInstrument(ctx, key);
|
|
28
29
|
const [points, quote] = await Promise.all([
|
|
29
30
|
loadHistory(ctx, key, range),
|
|
30
|
-
exchange ? null : (async () => ctx.marketData.getQuote(symbol, "", ctx.refresh ? { cacheMode: "refresh" } : undefined))().catch((
|
|
31
|
+
exchange ? null : (async () => ctx.marketData.getQuote(symbol, "", ctx.refresh ? { cacheMode: "refresh" } : undefined))().catch(logFailure("correlation", "Listing exchange quote failed", null)),
|
|
31
32
|
]);
|
|
32
33
|
return { exchange: exchange || quote?.listingExchangeName || quote?.exchangeName || "", points };
|
|
33
34
|
}
|