@gloom-sh/terminal 0.19.1 → 0.19.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.
Files changed (554) hide show
  1. package/README.md +7 -1
  2. package/package.json +2 -1
  3. package/patches/electrobun@1.18.1.patch +108 -1
  4. package/src/account/upgrade-dialog.tsx +4 -23
  5. package/src/api-client/coalesce.ts +118 -0
  6. package/src/api-client/credit-boards.ts +13 -0
  7. package/src/api-client/crypto-etf-flows.ts +6 -0
  8. package/src/api-client/crypto-markets.ts +40 -0
  9. package/src/api-client/data.ts +201 -14
  10. package/src/api-client/equity-screener.ts +10 -0
  11. package/src/api-client/errors.ts +58 -0
  12. package/src/api-client/gpu.ts +29 -0
  13. package/src/api-client/index.ts +5 -2
  14. package/src/api-client/plan-access.ts +10 -1
  15. package/src/api-client/plan-rules.ts +35 -0
  16. package/src/api-client/research-activity.ts +1 -32
  17. package/src/api-client/types.ts +21 -1
  18. package/src/app/attention-notice.tsx +94 -0
  19. package/src/app/runtime/startup.ts +3 -1
  20. package/src/app.tsx +23 -1
  21. package/src/architecture/core-builtin-allowlist.json +2 -1
  22. package/src/capabilities/chart-series.ts +2 -1
  23. package/src/capabilities/factories.ts +1 -0
  24. package/src/cli/ascii-output.ts +42 -0
  25. package/src/cli/base-converter.ts +10 -2
  26. package/src/cli/command-options.ts +16 -1
  27. package/src/cli/commands/crypto-hints.ts +23 -4
  28. package/src/cli/commands/market.ts +101 -27
  29. package/src/cli/commands/news-search.ts +24 -5
  30. package/src/cli/commands/overview.ts +14 -10
  31. package/src/cli/commands/search.ts +84 -25
  32. package/src/cli/commands/system.ts +22 -30
  33. package/src/cli/commands/ticker.ts +49 -9
  34. package/src/cli/commands/version.ts +33 -0
  35. package/src/cli/context.ts +6 -9
  36. package/src/cli/data-dir.ts +7 -0
  37. package/src/cli/desktop-pane-shot-api-policy.ts +85 -0
  38. package/src/cli/desktop-pane-shot-browser.ts +320 -0
  39. package/src/cli/desktop-pane-shot-routes.ts +26 -1
  40. package/src/cli/desktop-pane-shot.ts +196 -256
  41. package/src/cli/early-command.ts +42 -0
  42. package/src/cli/earnings-calendar.ts +9 -0
  43. package/src/cli/entry.ts +19 -13
  44. package/src/cli/errors.ts +12 -2
  45. package/src/cli/glossary.ts +2 -1
  46. package/src/cli/help.ts +1 -1
  47. package/src/cli/history-export.ts +22 -2
  48. package/src/cli/history-rows.ts +189 -11
  49. package/src/cli/index.ts +18 -1
  50. package/src/cli/known-gaps.ts +51 -5
  51. package/src/cli/listing-arg.ts +63 -10
  52. package/src/cli/options.ts +1 -1
  53. package/src/cli/output-file.ts +60 -13
  54. package/src/cli/pane-functions/catalog.ts +14 -3
  55. package/src/cli/pane-functions/data.ts +1 -1
  56. package/src/cli/pane-functions/dom.ts +45 -0
  57. package/src/cli/pane-functions/freshness.ts +46 -9
  58. package/src/cli/pane-functions/function-help.ts +17 -9
  59. package/src/cli/pane-functions/headless.ts +22 -14
  60. package/src/cli/pane-functions/index.ts +6 -2
  61. package/src/cli/pane-functions/options.ts +6 -2
  62. package/src/cli/pane-functions/resolver.ts +27 -5
  63. package/src/cli/pane-functions/screenshot-assessment.ts +33 -0
  64. package/src/cli/pane-functions/screenshot-bridge.ts +94 -13
  65. package/src/cli/pane-functions/screenshot-http.ts +70 -0
  66. package/src/cli/pane-functions/screenshot-render.ts +19 -5
  67. package/src/cli/quote-answer.ts +49 -6
  68. package/src/cli/registry.ts +41 -13
  69. package/src/cli/result.ts +12 -6
  70. package/src/cli/ticker-resolution.ts +2 -1
  71. package/src/cli/types.ts +2 -0
  72. package/src/components/chart/composite/format.ts +3 -0
  73. package/src/components/chart-table/header.tsx +1 -1
  74. package/src/components/chart-table/layout.ts +28 -11
  75. package/src/components/command-bar/action-runtime.ts +4 -0
  76. package/src/components/command-bar/commands/direct/index.ts +18 -33
  77. package/src/components/command-bar/commands/direct/runtime.ts +5 -0
  78. package/src/components/command-bar/commands/registry.ts +10 -1
  79. package/src/components/command-bar/routes/root/command-items.ts +9 -4
  80. package/src/components/command-bar/routes/root/runtime.ts +51 -6
  81. package/src/components/command-bar/selection-runtime.ts +12 -29
  82. package/src/components/command-bar/surface/index.tsx +9 -3
  83. package/src/components/company-logo.tsx +2 -1
  84. package/src/components/layout/detached-pane-shell.tsx +2 -2
  85. package/src/components/layout/header.tsx +10 -5
  86. package/src/components/layout/market-summary.ts +25 -7
  87. package/src/components/layout/pane/footer/index.tsx +8 -2
  88. package/src/components/layout/pane/footer/model.ts +73 -6
  89. package/src/components/layout/pane/header.tsx +2 -2
  90. package/src/components/layout/pane/status-footer.ts +2 -1
  91. package/src/components/market-board/index.tsx +3 -2
  92. package/src/components/onboarding/desks-step.tsx +12 -4
  93. package/src/components/onboarding/first-run-workspace.ts +81 -4
  94. package/src/components/onboarding/onboarding-frame.tsx +4 -2
  95. package/src/components/onboarding/onboarding-keyboard.ts +8 -5
  96. package/src/components/onboarding/onboarding-wizard.tsx +60 -26
  97. package/src/components/onboarding/portfolio-step/positions-panel.tsx +7 -2
  98. package/src/components/onboarding/run-setup-again.ts +24 -0
  99. package/src/components/onboarding/use-onboarding-progress.ts +33 -15
  100. package/src/components/onboarding/use-onboarding-upgrade.ts +2 -6
  101. package/src/components/onboarding/wizard-model.ts +57 -3
  102. package/src/components/onboarding/wizard-positions.ts +1 -1
  103. package/src/components/ui/delayed-mark.tsx +56 -0
  104. package/src/components/ui/display.tsx +16 -5
  105. package/src/components/ui/external-link.tsx +3 -6
  106. package/src/components/ui/icon.tsx +16 -3
  107. package/src/components/ui/logo-mark.tsx +8 -5
  108. package/src/components/ui/stat-grid.tsx +263 -19
  109. package/src/core/app-services.ts +11 -2
  110. package/src/data/app-persistence.ts +9 -7
  111. package/src/data/config/store/node.ts +11 -5
  112. package/src/data/config/store/normalize.ts +16 -3
  113. package/src/data/plugin-cache.ts +15 -0
  114. package/src/data/plugin-state-store.ts +81 -24
  115. package/src/data/profile-permissions.ts +47 -0
  116. package/src/data/resource-store.ts +165 -90
  117. package/src/data/session-store.ts +31 -8
  118. package/src/data/sqlite/access.ts +324 -0
  119. package/src/data/sqlite/checkpointer.ts +285 -0
  120. package/src/data/sqlite/database.ts +80 -4
  121. package/src/data/sqlite/retry.ts +50 -9
  122. package/src/data/ticker-store.ts +57 -23
  123. package/src/docs/snapshot.json +191 -78
  124. package/src/i18n/es.ts +18 -2
  125. package/src/i18n/ja.ts +18 -2
  126. package/src/i18n/ko.ts +18 -2
  127. package/src/i18n/zh-cn.ts +18 -2
  128. package/src/i18n/zh-tw.ts +18 -2
  129. package/src/layout/desk-keys.ts +4 -1
  130. package/src/layout/desks.ts +38 -5
  131. package/src/layout-marketplace/gallery-desktop.tsx +45 -16
  132. package/src/market-data/batched-reads.ts +16 -0
  133. package/src/market-data/coordinator/auxiliary.ts +4 -2
  134. package/src/market-data/coordinator/index.ts +11 -2
  135. package/src/market-data/futures-board.ts +1 -1
  136. package/src/market-data/futures-contract.ts +3 -1
  137. package/src/market-data/market/index-level.ts +6 -4
  138. package/src/market-data/published-apac-sessions.ts +6 -0
  139. package/src/market-data/realized-volatility.ts +4 -2
  140. package/src/news/aggregator.ts +7 -4
  141. package/src/news/news-model.ts +11 -0
  142. package/src/news/story-id.ts +22 -0
  143. package/src/news/ticker-symbols.ts +17 -1
  144. package/src/notifications/app-notifier.ts +35 -14
  145. package/src/plugins/auto-update-node.ts +10 -6
  146. package/src/plugins/builtin/account-management/form-components.tsx +1 -1
  147. package/src/plugins/builtin/account-management/index.tsx +2 -3
  148. package/src/plugins/builtin/account-management/model.ts +1 -1
  149. package/src/plugins/builtin/alerts/cli-forms.ts +15 -1
  150. package/src/plugins/builtin/alerts/events-pane.tsx +7 -5
  151. package/src/plugins/builtin/alerts/events.ts +10 -2
  152. package/src/plugins/builtin/alerts/index.tsx +5 -5
  153. package/src/plugins/builtin/alerts/research-builder.ts +5 -1
  154. package/src/plugins/builtin/alerts/research-rules.ts +18 -0
  155. package/src/plugins/builtin/alerts/targets.ts +4 -1
  156. package/src/plugins/builtin/attention/headless.ts +1 -1
  157. package/src/plugins/builtin/attention/index.tsx +2 -4
  158. package/src/plugins/builtin/attention/listing.ts +1 -1
  159. package/src/plugins/builtin/attention/series.ts +1 -1
  160. package/src/plugins/builtin/awards/index.tsx +1 -2
  161. package/src/plugins/builtin/backtest/client.ts +1 -0
  162. package/src/plugins/builtin/backtest/index.tsx +1 -2
  163. package/src/plugins/builtin/backtest/rules.ts +3 -1
  164. package/src/plugins/builtin/backtest/window.ts +8 -7
  165. package/src/plugins/builtin/broker-manager/detail.tsx +16 -3
  166. package/src/plugins/builtin/broker-manager/index.tsx +1 -1
  167. package/src/plugins/builtin/buildout/cells.tsx +5 -0
  168. package/src/plugins/builtin/buildout/company/flow.tsx +367 -0
  169. package/src/plugins/builtin/buildout/company/id.ts +2 -0
  170. package/src/plugins/builtin/buildout/company/model.ts +247 -0
  171. package/src/plugins/builtin/buildout/company/pane.tsx +317 -0
  172. package/src/plugins/builtin/buildout/data-runtime.ts +31 -10
  173. package/src/plugins/builtin/buildout/facilities.ts +309 -0
  174. package/src/plugins/builtin/buildout/format.ts +9 -0
  175. package/src/plugins/builtin/buildout/index.tsx +51 -5
  176. package/src/plugins/builtin/buildout/model/index.ts +62 -10
  177. package/src/plugins/builtin/buildout/model/normalizers.ts +5 -0
  178. package/src/plugins/builtin/buildout/model/types.ts +11 -2
  179. package/src/plugins/builtin/buildout/pane/index.tsx +91 -20
  180. package/src/plugins/builtin/buildout/pane/status.tsx +1 -1
  181. package/src/plugins/builtin/buildout/pane/themes.tsx +110 -0
  182. package/src/plugins/builtin/buildout/table-model.ts +26 -2
  183. package/src/plugins/builtin/builtin-plugin-meta.ts +16 -0
  184. package/src/plugins/builtin/cds/headless.ts +10 -2
  185. package/src/plugins/builtin/cds/model.ts +7 -3
  186. package/src/plugins/builtin/central-bank-rates/index.tsx +1 -2
  187. package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -1
  188. package/src/plugins/builtin/central-bank-rates/series.ts +2 -3
  189. package/src/plugins/builtin/changelog/index.tsx +3 -2
  190. package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
  191. package/src/plugins/builtin/chart-composer/headless-schema.ts +10 -0
  192. package/src/plugins/builtin/chart-composer/headless.ts +88 -21
  193. package/src/plugins/builtin/chart-composer/index.tsx +2 -3
  194. package/src/plugins/builtin/chart-composer/pane.tsx +30 -2
  195. package/src/plugins/builtin/chart-composer/series-expression.ts +3 -2
  196. package/src/plugins/builtin/chart-composer/session-range.ts +5 -2
  197. package/src/plugins/builtin/chart-composer/studies.ts +1 -1
  198. package/src/plugins/builtin/chat/controller/index.ts +1 -1
  199. package/src/plugins/builtin/chat/controller/messages.ts +15 -4
  200. package/src/plugins/builtin/chat/ghost-user.ts +8 -0
  201. package/src/plugins/builtin/chat/index.tsx +5 -5
  202. package/src/plugins/builtin/clinical-trials/index.tsx +1 -2
  203. package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
  204. package/src/plugins/builtin/cloud/index.tsx +1 -2
  205. package/src/plugins/builtin/cloud/plugin.tsx +6 -4
  206. package/src/plugins/builtin/cloud/team/module.tsx +1 -1
  207. package/src/plugins/builtin/cloud/thesis/module.tsx +1 -1
  208. package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +24 -1
  209. package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +39 -15
  210. package/src/plugins/builtin/comment-letters/index.tsx +1 -2
  211. package/src/plugins/builtin/commodity-balances/client.ts +4 -1
  212. package/src/plugins/builtin/commodity-balances/index.tsx +3 -3
  213. package/src/plugins/builtin/commodity-balances/pane.tsx +2 -2
  214. package/src/plugins/builtin/congress-trades/aggregates.ts +13 -2
  215. package/src/plugins/builtin/congress-trades/detail.tsx +20 -4
  216. package/src/plugins/builtin/congress-trades/headless.ts +2 -0
  217. package/src/plugins/builtin/congress-trades/model.ts +92 -64
  218. package/src/plugins/builtin/congress-trades/table.tsx +14 -2
  219. package/src/plugins/builtin/connections/index.ts +2 -3
  220. package/src/plugins/builtin/cot/client.ts +28 -2
  221. package/src/plugins/builtin/cot/headless.ts +4 -1
  222. package/src/plugins/builtin/cot/index.tsx +5 -4
  223. package/src/plugins/builtin/cot/model.ts +58 -7
  224. package/src/plugins/builtin/cot/pane.tsx +6 -2
  225. package/src/plugins/builtin/cpi/client.ts +6 -5
  226. package/src/plugins/builtin/cpi/index.tsx +1 -2
  227. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  228. package/src/plugins/builtin/credit-boards/client.ts +7 -2
  229. package/src/plugins/builtin/credit-boards/headless.ts +25 -12
  230. package/src/plugins/builtin/credit-boards/model.ts +29 -0
  231. package/src/plugins/builtin/credit-boards/series.ts +3 -4
  232. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +9 -4
  233. package/src/plugins/builtin/credit-conditions/client.ts +22 -14
  234. package/src/plugins/builtin/crypto-board/bitcoin-network.ts +143 -0
  235. package/src/plugins/builtin/crypto-board/client.ts +5 -5
  236. package/src/plugins/builtin/crypto-board/headless.ts +4 -3
  237. package/src/plugins/builtin/crypto-board/index.tsx +4 -2
  238. package/src/plugins/builtin/crypto-board/series.ts +6 -4
  239. package/src/plugins/builtin/crypto-etf-flows/client.ts +9 -2
  240. package/src/plugins/builtin/crypto-etf-flows/headless.ts +61 -6
  241. package/src/plugins/builtin/crypto-etf-flows/index.tsx +1 -2
  242. package/src/plugins/builtin/crypto-etf-flows/model.ts +14 -1
  243. package/src/plugins/builtin/crypto-etf-flows/pane.tsx +8 -5
  244. package/src/plugins/builtin/debt-maturities/headless.ts +4 -3
  245. package/src/plugins/builtin/debt-maturities/model.ts +17 -1
  246. package/src/plugins/builtin/debt-maturities/pane.tsx +18 -4
  247. package/src/plugins/builtin/debt-maturities/series.ts +3 -3
  248. package/src/plugins/builtin/debug/index.tsx +1 -1
  249. package/src/plugins/builtin/dividend-yield/client.ts +32 -2
  250. package/src/plugins/builtin/dividend-yield/headless.ts +58 -33
  251. package/src/plugins/builtin/dividend-yield/index.tsx +37 -2
  252. package/src/plugins/builtin/dividend-yield/pane.tsx +32 -13
  253. package/src/plugins/builtin/dividend-yield/ranking-reads.ts +73 -0
  254. package/src/plugins/builtin/dividend-yield/ranking-view.tsx +163 -0
  255. package/src/plugins/builtin/dividend-yield/ranking.ts +1 -1
  256. package/src/plugins/builtin/dividend-yield/view.ts +21 -0
  257. package/src/plugins/builtin/docs/index.tsx +2 -3
  258. package/src/plugins/builtin/doe/client.ts +6 -5
  259. package/src/plugins/builtin/doe/headless.ts +16 -3
  260. package/src/plugins/builtin/doe/index.tsx +5 -4
  261. package/src/plugins/builtin/doe/prices.ts +2 -3
  262. package/src/plugins/builtin/doe/series.ts +4 -4
  263. package/src/plugins/builtin/earnings/board.tsx +2 -1
  264. package/src/plugins/builtin/earnings/history.tsx +2 -1
  265. package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
  266. package/src/plugins/builtin/earnings-calls/data.ts +19 -11
  267. package/src/plugins/builtin/earnings-calls/pane.tsx +27 -14
  268. package/src/plugins/builtin/earnings-ripple/client.ts +32 -7
  269. package/src/plugins/builtin/earnings-ripple/index.tsx +1 -2
  270. package/src/plugins/builtin/econ/calendar-model.ts +29 -6
  271. package/src/plugins/builtin/econ/detail-view.tsx +16 -2
  272. package/src/plugins/builtin/econ-statistics/index.tsx +1 -2
  273. package/src/plugins/builtin/equity-screener/client.ts +1 -1
  274. package/src/plugins/builtin/equity-screener/headless.ts +1 -0
  275. package/src/plugins/builtin/equity-screener/ideas.ts +30 -1
  276. package/src/plugins/builtin/equity-screener/index.tsx +1 -2
  277. package/src/plugins/builtin/equity-screener/model.ts +28 -4
  278. package/src/plugins/builtin/equity-screener/pane.tsx +5 -0
  279. package/src/plugins/builtin/equity-screener/screen-check.ts +23 -1
  280. package/src/plugins/builtin/estimate-revisions/index.tsx +1 -2
  281. package/src/plugins/builtin/executives/data.ts +6 -1
  282. package/src/plugins/builtin/executives/pane.tsx +9 -21
  283. package/src/plugins/builtin/exposure/holdings.ts +35 -13
  284. package/src/plugins/builtin/exposure/index.tsx +1 -2
  285. package/src/plugins/builtin/fear-greed/cache.ts +4 -0
  286. package/src/plugins/builtin/fear-greed/cli.ts +44 -0
  287. package/src/plugins/builtin/fear-greed/index.tsx +5 -53
  288. package/src/plugins/builtin/fear-greed/pane.tsx +2 -1
  289. package/src/plugins/builtin/fear-greed/series.ts +6 -4
  290. package/src/plugins/builtin/fixed-income/index.tsx +1 -2
  291. package/src/plugins/builtin/futures/board-reads.ts +25 -0
  292. package/src/plugins/builtin/futures/coalesced-bump.ts +44 -0
  293. package/src/plugins/builtin/futures/index.tsx +15 -0
  294. package/src/plugins/builtin/futures/model.ts +14 -0
  295. package/src/plugins/builtin/futures/use-board-terms.ts +22 -15
  296. package/src/plugins/builtin/futures/use-front-returns.ts +20 -14
  297. package/src/plugins/builtin/futures-curve/headless.ts +6 -1
  298. package/src/plugins/builtin/futures-curve/index.tsx +1 -2
  299. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  300. package/src/plugins/builtin/gpu/board.tsx +51 -12
  301. package/src/plugins/builtin/gpu/client.ts +19 -3
  302. package/src/plugins/builtin/gpu/equities.tsx +5 -2
  303. package/src/plugins/builtin/gpu/headless.ts +14 -2
  304. package/src/plugins/builtin/gpu/index.tsx +1 -2
  305. package/src/plugins/builtin/gpu/model.ts +20 -2
  306. package/src/plugins/builtin/gpu/pane.tsx +1 -1
  307. package/src/plugins/builtin/gpu/series.ts +2 -3
  308. package/src/plugins/builtin/help/index.tsx +1 -1
  309. package/src/plugins/builtin/holders/beneficial-view.tsx +1 -2
  310. package/src/plugins/builtin/holders/index-holders.ts +24 -12
  311. package/src/plugins/builtin/holders/index.tsx +2 -3
  312. package/src/plugins/builtin/ipo-calendar/index.tsx +1 -2
  313. package/src/plugins/builtin/iv-history/index.tsx +1 -2
  314. package/src/plugins/builtin/jodi/client.ts +4 -1
  315. package/src/plugins/builtin/jodi/index.tsx +1 -2
  316. package/src/plugins/builtin/kelly-sizer/index.tsx +1 -2
  317. package/src/plugins/builtin/layout-manager/index.tsx +9 -8
  318. package/src/plugins/builtin/macro-day/pane.tsx +4 -2
  319. package/src/plugins/builtin/market-halts/index.tsx +1 -2
  320. package/src/plugins/builtin/market-heatmap/index.tsx +11 -2
  321. package/src/plugins/builtin/market-movers/client.ts +1 -0
  322. package/src/plugins/builtin/market-movers/index.tsx +16 -5
  323. package/src/plugins/builtin/market-movers/screener.ts +23 -6
  324. package/src/plugins/builtin/market-movers/session-body.tsx +6 -2
  325. package/src/plugins/builtin/market-valuation/index.tsx +1 -2
  326. package/src/plugins/builtin/members/index.tsx +1 -2
  327. package/src/plugins/builtin/money-markets/index.tsx +1 -2
  328. package/src/plugins/builtin/money-markets/pane.tsx +2 -1
  329. package/src/plugins/builtin/money-markets/series.ts +3 -5
  330. package/src/plugins/builtin/news/headless.ts +7 -0
  331. package/src/plugins/builtin/news/wire/index.tsx +1 -1
  332. package/src/plugins/builtin/news/wire/news/pop-out.tsx +9 -3
  333. package/src/plugins/builtin/notes/index.tsx +3 -6
  334. package/src/plugins/builtin/openfda/index.tsx +1 -2
  335. package/src/plugins/builtin/options/view.tsx +6 -5
  336. package/src/plugins/builtin/options-calculator/index.tsx +1 -2
  337. package/src/plugins/builtin/options-positioning/index.tsx +1 -2
  338. package/src/plugins/builtin/options-scenario/index.tsx +2 -4
  339. package/src/plugins/builtin/perps/index.tsx +1 -2
  340. package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -3
  341. package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -1
  342. package/src/plugins/builtin/plugin-module.ts +16 -2
  343. package/src/plugins/builtin/portfolio-list/cli/render.ts +53 -3
  344. package/src/plugins/builtin/portfolio-list/index.tsx +1 -2
  345. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -0
  346. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -2
  347. package/src/plugins/builtin/power/gas-client.ts +2 -1
  348. package/src/plugins/builtin/power/index.tsx +1 -2
  349. package/src/plugins/builtin/power/prices-client.ts +3 -2
  350. package/src/plugins/builtin/rate-path/index.tsx +1 -2
  351. package/src/plugins/builtin/realized-vol/headless.ts +2 -0
  352. package/src/plugins/builtin/realized-vol/index.tsx +1 -2
  353. package/src/plugins/builtin/realized-vol/model.ts +16 -2
  354. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  355. package/src/plugins/builtin/relative-rotation/index.tsx +1 -2
  356. package/src/plugins/builtin/research/analyst-headless.ts +33 -6
  357. package/src/plugins/builtin/research/analyst-model.ts +37 -2
  358. package/src/plugins/builtin/research/analyst-pane.tsx +25 -5
  359. package/src/plugins/builtin/research/index.tsx +5 -8
  360. package/src/plugins/builtin/research-search/index.tsx +1 -2
  361. package/src/plugins/builtin/revenue-breakdown/headless.ts +22 -8
  362. package/src/plugins/builtin/revenue-breakdown/model.ts +23 -0
  363. package/src/plugins/builtin/reverse-dcf/pane.tsx +4 -2
  364. package/src/plugins/builtin/risk-factors/data.ts +8 -4
  365. package/src/plugins/builtin/risk-factors/index.tsx +3 -1
  366. package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
  367. package/src/plugins/builtin/roadmap/index.tsx +2 -3
  368. package/src/plugins/builtin/roadmap/pane.tsx +49 -2
  369. package/src/plugins/builtin/scanner/hilo-model.ts +18 -1
  370. package/src/plugins/builtin/scanner/hilo-pane.tsx +12 -2
  371. package/src/plugins/builtin/scanner/live-pane.tsx +7 -4
  372. package/src/plugins/builtin/scanner/lock-note.ts +26 -3
  373. package/src/plugins/builtin/sec/filing-content.ts +38 -6
  374. package/src/plugins/builtin/sec/headless.ts +5 -1
  375. package/src/plugins/builtin/sec/index.tsx +4 -2
  376. package/src/plugins/builtin/sec/model.ts +2 -0
  377. package/src/plugins/builtin/sectors/index.tsx +11 -5
  378. package/src/plugins/builtin/series-inventory.ts +2 -0
  379. package/src/plugins/builtin/shared/cloud-resource.ts +73 -8
  380. package/src/plugins/builtin/shared/cloud-upgrade.ts +24 -1
  381. package/src/plugins/builtin/shared/forming-bars.ts +29 -0
  382. package/src/plugins/builtin/shared/fred-points.ts +6 -1
  383. package/src/plugins/builtin/shared/fx-rate-freshness.ts +17 -0
  384. package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
  385. package/src/plugins/builtin/shared/series-detail-chart.tsx +4 -2
  386. package/src/plugins/builtin/shared/series-warning.ts +13 -4
  387. package/src/plugins/builtin/shared/use-quote-board.ts +5 -0
  388. package/src/plugins/builtin/short-interest/publications-pane.tsx +2 -1
  389. package/src/plugins/builtin/short-volume/pane.tsx +2 -1
  390. package/src/plugins/builtin/short-volume/series.ts +2 -3
  391. package/src/plugins/builtin/social-mentions/client.ts +15 -10
  392. package/src/plugins/builtin/social-mentions/index.tsx +1 -2
  393. package/src/plugins/builtin/social-mentions/pane.tsx +2 -1
  394. package/src/plugins/builtin/social-mentions/series.ts +2 -3
  395. package/src/plugins/builtin/supply-chain/client.ts +22 -1
  396. package/src/plugins/builtin/supply-chain/graph-client.ts +22 -1
  397. package/src/plugins/builtin/supply-chain/index.tsx +1 -2
  398. package/src/plugins/builtin/thirteenf/api.ts +127 -29
  399. package/src/plugins/builtin/thirteenf/data.ts +165 -35
  400. package/src/plugins/builtin/thirteenf/index.tsx +2 -4
  401. package/src/plugins/builtin/thirteenf/model.ts +2 -0
  402. package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
  403. package/src/plugins/builtin/thirteenf/pane.tsx +41 -16
  404. package/src/plugins/builtin/thirteenf/types.ts +14 -0
  405. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +22 -6
  406. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +81 -11
  407. package/src/plugins/builtin/ticker-detail/financials/model.ts +35 -2
  408. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +53 -18
  409. package/src/plugins/builtin/ticker-detail/headless.ts +70 -11
  410. package/src/plugins/builtin/ticker-detail/index.tsx +5 -10
  411. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +16 -6
  412. package/src/plugins/builtin/time-sales/index.tsx +1 -2
  413. package/src/plugins/builtin/time-sales/pane.tsx +3 -1
  414. package/src/plugins/builtin/trace-bonds/headless.ts +11 -2
  415. package/src/plugins/builtin/trace-bonds/model.ts +44 -10
  416. package/src/plugins/builtin/trace-bonds/pane.tsx +12 -7
  417. package/src/plugins/builtin/treasury-auctions/cache.ts +20 -8
  418. package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
  419. package/src/plugins/builtin/treasury-auctions/headless.ts +3 -1
  420. package/src/plugins/builtin/treasury-auctions/pane.tsx +10 -4
  421. package/src/plugins/builtin/vol-surface/client.ts +18 -3
  422. package/src/plugins/builtin/vol-surface/index.tsx +1 -2
  423. package/src/plugins/builtin/volatility/client.ts +43 -16
  424. package/src/plugins/builtin/volatility/headless.ts +5 -2
  425. package/src/plugins/builtin/volatility/index.tsx +1 -2
  426. package/src/plugins/builtin/volatility/model.ts +17 -6
  427. package/src/plugins/builtin/volatility/pane.tsx +6 -2
  428. package/src/plugins/builtin/volatility/series.ts +2 -1
  429. package/src/plugins/builtin/world-indices/footer.ts +6 -1
  430. package/src/plugins/builtin/world-venue-map/client.ts +10 -1
  431. package/src/plugins/builtin/world-venue-map/index.ts +6 -2
  432. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +48 -7
  433. package/src/plugins/builtin/world-venue-map/layers.ts +17 -0
  434. package/src/plugins/builtin/world-venue-map/local-layers.ts +141 -0
  435. package/src/plugins/builtin/world-venue-map/map-symbols.ts +18 -0
  436. package/src/plugins/builtin/world-venue-map/map.tsx +4 -2
  437. package/src/plugins/builtin/world-venue-map/scope.ts +63 -0
  438. package/src/plugins/builtin/yield-curve/client.ts +4 -6
  439. package/src/plugins/builtin/yield-curve/curves.ts +16 -2
  440. package/src/plugins/builtin/yield-curve/headless.ts +3 -3
  441. package/src/plugins/bundle.ts +52 -2
  442. package/src/plugins/catalog-browser.ts +2 -2
  443. package/src/plugins/catalog-cli.ts +20 -0
  444. package/src/plugins/host-link.ts +13 -1
  445. package/src/plugins/installer.ts +64 -42
  446. package/src/plugins/loader.ts +43 -11
  447. package/src/plugins/peer-imports.ts +201 -0
  448. package/src/plugins/peer-retry.ts +26 -29
  449. package/src/plugins/plugin-process.ts +128 -0
  450. package/src/plugins/redirection-guard.ts +45 -0
  451. package/src/remote/endpoint.ts +16 -3
  452. package/src/renderers/browser/main.tsx +3 -2
  453. package/src/renderers/cloudflare/http-proxy.ts +7 -123
  454. package/src/renderers/dom/desktop/icons.tsx +5 -2
  455. package/src/renderers/dom/host/image-surface.tsx +46 -21
  456. package/src/renderers/dom/styles.css +7 -0
  457. package/src/renderers/dom/toast-host.tsx +2 -0
  458. package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
  459. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
  460. package/src/renderers/electrobun/bun/desktop/initialization.ts +12 -3
  461. package/src/renderers/electrobun/bun/desktop/rpc-timing.ts +88 -0
  462. package/src/renderers/electrobun/bun/desktop/update.ts +20 -4
  463. package/src/renderers/electrobun/bun/desktop/view-ids.ts +8 -4
  464. package/src/renderers/electrobun/bun/external-plugins.ts +4 -0
  465. package/src/renderers/electrobun/bun/window/attention.ts +54 -0
  466. package/src/renderers/electrobun/bun/window/create.ts +6 -2
  467. package/src/renderers/electrobun/bun/window/native-timing.ts +48 -0
  468. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +82 -4
  469. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +13 -5
  470. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +3 -0
  471. package/src/renderers/electrobun/view/main.tsx +7 -2
  472. package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
  473. package/src/renderers/electrobun/view/ui-host.tsx +9 -2
  474. package/src/renderers/opentui/browser-open.ts +10 -4
  475. package/src/renderers/opentui/image/surface.tsx +1 -1
  476. package/src/renderers/opentui/start.tsx +12 -6
  477. package/src/renderers/opentui/toast-host.tsx +2 -1
  478. package/src/series/registry.ts +18 -0
  479. package/src/series/resolve.ts +2 -1
  480. package/src/series/standalone.ts +10 -1
  481. package/src/series/types.ts +12 -0
  482. package/src/series/validate.ts +11 -2
  483. package/src/sources/gloomberb-cloud/fred-series.ts +5 -3
  484. package/src/sources/gloomberb-cloud/index.ts +28 -5
  485. package/src/sources/gloomberb-cloud/news.ts +24 -3
  486. package/src/sources/gloomberb-cloud/normalizers.ts +17 -2
  487. package/src/sources/provider-errors.ts +26 -3
  488. package/src/sources/provider-router/history.ts +5 -0
  489. package/src/sources/provider-router/index.ts +13 -0
  490. package/src/sync/core-contributors.ts +32 -0
  491. package/src/telemetry/attention-counts.ts +64 -6
  492. package/src/telemetry/attention-setting.ts +33 -0
  493. package/src/telemetry/crash-reports-node.ts +3 -1
  494. package/src/telemetry/stall-clocks.ts +93 -0
  495. package/src/telemetry/stall-reports-node.ts +25 -9
  496. package/src/telemetry/stall-reports.ts +218 -13
  497. package/src/test-support/outage.ts +8 -0
  498. package/src/test-support/preload.ts +9 -0
  499. package/src/tickers/company-name-listing.ts +59 -29
  500. package/src/tickers/company-name.ts +84 -1
  501. package/src/tickers/listing-fate.ts +14 -3
  502. package/src/tickers/saved-instrument.ts +17 -0
  503. package/src/tickers/search/home-listing.ts +104 -8
  504. package/src/tickers/search/index.ts +5 -2
  505. package/src/tickers/search/types.ts +6 -1
  506. package/src/time-series/bar-aggregation.ts +7 -1
  507. package/src/time-series/base-series.ts +3 -3
  508. package/src/time-series/chart-bounds.ts +1 -1
  509. package/src/time-series/chart-data.ts +5 -2
  510. package/src/time-series/chart-seed.ts +1 -1
  511. package/src/time-series/coin-session-close.ts +211 -0
  512. package/src/time-series/live-bars.ts +13 -2
  513. package/src/time-series/live-quotes.ts +1 -1
  514. package/src/time-series/market.ts +5 -3
  515. package/src/time-series/official-close.ts +159 -0
  516. package/src/time-series/price-comparison.ts +75 -8
  517. package/src/time-series/resolve.ts +32 -4
  518. package/src/time-series/series-presentation.ts +1 -1
  519. package/src/time-series/studies/formula.ts +205 -0
  520. package/src/time-series/studies/indicators.ts +299 -0
  521. package/src/time-series/studies/leverage.ts +75 -0
  522. package/src/time-series/studies/pair.ts +138 -0
  523. package/src/time-series/studies/resolve.ts +333 -0
  524. package/src/time-series/studies/shared.ts +141 -0
  525. package/src/time-series/studies/units.ts +243 -0
  526. package/src/time-series/studies/vwap.ts +274 -0
  527. package/src/time-series/types.ts +7 -0
  528. package/src/types/config.ts +18 -2
  529. package/src/types/data-provider.ts +23 -0
  530. package/src/types/financials.ts +40 -3
  531. package/src/types/headless.ts +42 -3
  532. package/src/types/plugin.ts +32 -3
  533. package/src/ui/host.tsx +17 -1
  534. package/src/ui/toast.tsx +26 -1
  535. package/src/utils/ascii-text.ts +140 -0
  536. package/src/utils/cli-output.ts +91 -18
  537. package/src/utils/concurrency.ts +24 -0
  538. package/src/utils/exchanges.ts +6 -0
  539. package/src/utils/external-url.ts +20 -7
  540. package/src/utils/financial-statements.ts +73 -16
  541. package/src/utils/fundamentals.ts +17 -5
  542. package/src/utils/futures-generic.ts +4 -4
  543. package/src/utils/http-proxy-upstream.ts +124 -0
  544. package/src/utils/lease-liabilities.ts +54 -0
  545. package/src/utils/main-thread-stall.ts +122 -5
  546. package/src/utils/perf-marks.ts +58 -12
  547. package/src/utils/price-history-integrity.ts +47 -1
  548. package/src/utils/private-files.ts +141 -0
  549. package/src/utils/public-address.ts +99 -0
  550. package/src/account/upgrade-personalization.ts +0 -57
  551. package/src/account/upgrade-tickers.ts +0 -124
  552. package/src/api-client/upgrade-experiment.ts +0 -91
  553. package/src/plugins/builtin/cloud/browser.tsx +0 -3
  554. package/src/time-series/studies.ts +0 -1628
@@ -1,12 +1,34 @@
1
1
  import type { PaneLock } from "../../../types/plugin";
2
- import { proLockNote } from "../shared/pro-lock-note";
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: [{ text: (ticker, company) => `gloom options ${ticker} lists ${company}'s option chain, each contract with its volume and open interest.` }],
39
+ free: [OPTIONS_ROUTE],
40
+ noneFor: (ticker) => `Options are not available for ${ticker}, so FLOW has no prints of it.`,
18
41
  });
19
42
  }
@@ -3,6 +3,7 @@ import { useSecFilingContent } from "../../../market-data/hooks";
3
3
  import { getSharedMarketDataCoordinator, resolveEntryValue } from "../../../market-data/coordinator";
4
4
  import type { QueryEntry } from "../../../market-data/result-types";
5
5
  import type { SecFilingDocument, SecFilingItem } from "../../../types/data-provider";
6
+ import { logFailure } from "../../../utils/log-failure";
6
7
  import { documentContentTarget, isInlineExhibitDocument } from "./filing-documents";
7
8
 
8
9
  interface ScopedFilingContentCache {
@@ -41,18 +42,32 @@ export function buildInlineFilingContentTargets(
41
42
  .map((document) => documentContentTarget(filing, document));
42
43
  }
43
44
 
44
- export function nextUncachedFilingContentTarget(
45
+ /** Filings whose content is read at once: the queue's head and the next ones read ahead. */
46
+ const CONTENT_READS_AT_ONCE = 4;
47
+
48
+ /** The first `count` targets not cached yet, once each, in the caller's priority order. */
49
+ export function nextUncachedFilingContentTargets(
45
50
  targets: readonly SecFilingItem[],
46
51
  cache: ReadonlyMap<string, string | null>,
47
- ): SecFilingItem | null {
52
+ count: number,
53
+ ): SecFilingItem[] {
48
54
  const seen = new Set<string>();
55
+ const next: SecFilingItem[] = [];
49
56
  for (const target of targets) {
57
+ if (next.length >= count) break;
50
58
  const key = target.accessionNumber;
51
59
  if (seen.has(key)) continue;
52
60
  seen.add(key);
53
- if (!cache.has(key)) return target;
61
+ if (!cache.has(key)) next.push(target);
54
62
  }
55
- return null;
63
+ return next;
64
+ }
65
+
66
+ export function nextUncachedFilingContentTarget(
67
+ targets: readonly SecFilingItem[],
68
+ cache: ReadonlyMap<string, string | null>,
69
+ ): SecFilingItem | null {
70
+ return nextUncachedFilingContentTargets(targets, cache, 1)[0] ?? null;
56
71
  }
57
72
 
58
73
  export function useSecFilingContentCache({
@@ -81,12 +96,29 @@ export function useSecFilingContentCache({
81
96
  return () => { retryGeneration.current += 1; };
82
97
  }, [scopeKey]);
83
98
 
84
- const nextTarget = useMemo(
85
- () => nextUncachedFilingContentTarget(targets, contentCache),
99
+ const upcoming = useMemo(
100
+ () => nextUncachedFilingContentTargets(targets, contentCache, CONTENT_READS_AT_ONCE),
86
101
  [contentCache, targets],
87
102
  );
103
+ const nextTarget = upcoming[0] ?? null;
88
104
  const contentEntry = useSecFilingContent(nextTarget);
89
105
 
106
+ // The queue takes one filing at a time; the next few are read beside it, and
107
+ // taken from the coordinator once the head settles (below). Each is asked
108
+ // once per scope: the coordinator shares a read in flight and keeps it.
109
+ const readAhead = useRef<{ scopeKey: string; keys: Set<string> }>({ scopeKey, keys: new Set() });
110
+ useEffect(() => {
111
+ const coordinator = getSharedMarketDataCoordinator();
112
+ if (!coordinator) return;
113
+ if (readAhead.current.scopeKey !== scopeKey) readAhead.current = { scopeKey, keys: new Set() };
114
+ const asked = readAhead.current.keys;
115
+ for (const target of upcoming.slice(1)) {
116
+ if (asked.has(target.accessionNumber)) continue;
117
+ asked.add(target.accessionNumber);
118
+ void coordinator.loadSecFilingContent(target).catch(logFailure("sec.filing-content", "Reading filing content ahead failed"));
119
+ }
120
+ }, [scopeKey, upcoming]);
121
+
90
122
  useEffect(() => {
91
123
  if (!nextTarget) return;
92
124
  const first = settledFilingContent(contentEntry);
@@ -39,8 +39,12 @@ const SEC_COLUMNS: HeadlessPaneColumn[] = [
39
39
  /** Some row has 8-K item codes; a column every row leaves `-` takes no room in the text table (exports keep it). */
40
40
  const hasItems = (rows: readonly HeadlessPaneRow[]) => rows.some((row) => typeof row.items === "string" && row.items !== "");
41
41
 
42
+ /** A current report's press release exhibit, printed only when some row has one; exports keep it with the filing's link. */
43
+ const PRESS_RELEASE_COLUMN: HeadlessPaneColumn = { key: "pressReleaseUrl", header: "Press release" };
44
+
42
45
  function secTextColumns(rows: readonly HeadlessPaneRow[]): HeadlessPaneColumn[] {
43
- return hasItems(rows) ? SEC_COLUMNS : SEC_COLUMNS.filter((column) => column.key !== "items");
46
+ const columns = hasItems(rows) ? SEC_COLUMNS : SEC_COLUMNS.filter((column) => column.key !== "items");
47
+ return rows.some((row) => typeof row.pressReleaseUrl === "string" && row.pressReleaseUrl) ? [...columns, PRESS_RELEASE_COLUMN] : columns;
44
48
  }
45
49
 
46
50
  /**
@@ -457,9 +457,11 @@ function SecView({ width, height, focused, paneType }: { width: number; height:
457
457
  usePaneStatusLinkFooter({
458
458
  registrationId: "sec",
459
459
  focused,
460
- url: openFiling ? openFiling.filingUrl : factsUrl ?? abroadPage ?? announcements?.url,
460
+ // A current report links its press release, where its figures are, when its index names one.
461
+ url: openFiling ? openFiling.pressReleaseUrl ?? openFiling.filingUrl : factsUrl ?? abroadPage ?? announcements?.url,
461
462
  source: openFiling ? openFiling.form : facts?.covered ? listNames(fundFactPublishers(facts)) : announcements?.name ?? null,
462
- label: openFiling ? "filing" : factsUrl ? "source" : abroadPage ? "issuer page" : announcements ? "announcements" : "filing",
463
+ label: openFiling ? openFiling.pressReleaseUrl ? "press release" : "filing"
464
+ : factsUrl ? "source" : abroadPage ? "issuer page" : announcements ? "announcements" : "filing",
463
465
  // Rows read in the background for their insiders do not hold up the pane.
464
466
  loading: loading || loadingDocuments || (!!loadingKey && viewedTargets.some((target) => target.accessionNumber === loadingKey)),
465
467
  error: authWall ? null : error ?? detailError,
@@ -227,6 +227,8 @@ export function buildSecFilingRows(filings: readonly SecFilingItem[]) {
227
227
  cik: filing.cik,
228
228
  companyName: filing.companyName ?? null,
229
229
  url: filing.filingUrl,
230
+ // A current report's press release exhibit, where its index names one: the page its figures are on.
231
+ ...(filing.pressReleaseUrl ? { pressReleaseUrl: filing.pressReleaseUrl } : {}),
230
232
  };
231
233
  });
232
234
  }
@@ -16,6 +16,7 @@ import type { ExtendedSession } from "../../../market-data/market/status";
16
16
  import { useAutoRefresh, useUpdatedAgo } from "../../../react/auto-refresh";
17
17
  import { useLiveStreamingSetting } from "../../../state/hooks/live-streaming";
18
18
  import { isStreamCarryingQuote, useVisibleBoardSymbols } from "../shared/use-quote-board";
19
+ import { useQuoteDelayFooter } from "../shared/cloud-upgrade";
19
20
  import { SectorMoveBar } from "./move-bar";
20
21
  import {
21
22
  SECTOR_COLLECTIONS,
@@ -345,16 +346,21 @@ function EtfBoard({ collectionId, focused, width, height, rootBefore }: EtfBoard
345
346
  .map((row) => `${row.etf}: ${sectorRowIssues(row).join(" · ")}`);
346
347
  usePaneNoticeFooter({ registrationId: "sectors:row-issues", notices: rowIssueNotices, focused });
347
348
 
349
+ // Signed out or free, the ETFs' quotes are delayed ones.
350
+ const quoteDelay = useQuoteDelayFooter(rows.map((row) => (row.loading ? null : { dataSource: row.quoteDataSource })), "sectors-delay");
348
351
  usePaneFooter("sectors", () => {
349
352
  const info = loadingErrorFooterInfo(loading, loadError);
350
353
  if (returnAsOfDate) info.push({ id: "return-as-of", parts: [{ text: `returns as of ${returnAsOfDate}`, tone: "muted" }] });
351
354
  if (updatedAgo) info.push({ id: "updated", parts: [{ text: `checked ${updatedAgo}`, tone: "muted" }] });
352
355
  // Keep the leading current failure readable when the pane is narrow; separate
353
356
  // flex children would each shrink it to a few characters beside routine status.
354
- return { info: info.length > 0 ? [{ id: "status", parts: info.flatMap((segment, index) => [
355
- ...(index > 0 ? [{ text: "·", tone: "muted" as const }] : []), ...segment.parts,
356
- ]) }] : [] };
357
- }, [loadError, loading, returnAsOfDate, updatedAgo]);
357
+ return { info: [
358
+ ...(info.length > 0 ? [{ id: "status", parts: info.flatMap((segment, index) => [
359
+ ...(index > 0 ? [{ text: "·", tone: "muted" as const }] : []), ...segment.parts,
360
+ ]) }] : []),
361
+ ...(quoteDelay ? [quoteDelay] : []),
362
+ ] };
363
+ }, [loadError, loading, quoteDelay, returnAsOfDate, updatedAgo]);
358
364
 
359
365
  return (
360
366
  <DataTableView<SectorRow, SectorColumn>
@@ -442,7 +448,7 @@ export const sectorsModule: PluginModule = {
442
448
  paneId: "sectors",
443
449
  label: "Thematic Baskets",
444
450
  description: "Curated themes with equal-weight returns, breadth and member leaders and laggards.",
445
- keywords: ["themes", "thematic", "baskets", "AI compute", "datacenters", "electrification", "power grid", "nuclear", "uranium", "defense", "aerospace", "cybersecurity", "robotics", "automation", "critical minerals", "crypto", "bitcoin", "solar", "clean energy", "gold", "silver", "homebuilders", "memory", "chip equipment", "cloud software"],
451
+ keywords: ["themes", "thematic", "baskets", "AI compute", "datacenters", "electrification", "power grid", "nuclear", "uranium", "defense", "aerospace", "cybersecurity", "robotics", "automation", "critical minerals", "crypto", "bitcoin", "bitcoin miners", "miners", "bitcoin mining", "solar", "clean energy", "gold", "silver", "homebuilders", "memory", "chip equipment", "cloud software"],
446
452
  shortcut: { prefix: "THEM", argKind: "text", argPlaceholder: "theme", argOptional: true },
447
453
  headless: themesHeadless,
448
454
  // The theme param opens the Themes tab, and the named theme's members.
@@ -61,6 +61,7 @@ export const SERIES_INVENTORY: readonly SeriesInventoryEntry[] = [
61
61
  { module: "futures", chart: "Front-month futures board, charted from the board", series: ["fut:*"] },
62
62
  { module: "futures-curve", chart: "Futures curve by contract month", notInCatalog: "not-a-time-series", reason: "Plotted by delivery month; each front month is in the catalog as FUT:code." },
63
63
  { module: "crypto-board", chart: "Price sparklines for the large coins", series: ["crypto:*"] },
64
+ { module: "crypto-board", chart: "Bitcoin network hash rate, difficulty, next retarget estimate and hashprice", series: ["bitcoin:*"] },
64
65
  { module: "perps", chart: "Perpetual funding, open interest, premium and long/short account share", series: ["perps:*"] },
65
66
  { module: "crypto-etf-flows", chart: "The running net flow of the US spot bitcoin or ether ETFs since launch", notInCatalog: "not-yet", reason: "Daily and dated: the running total and each fund's daily flow could be crypto ETF series, and none is registered for the composer yet." },
66
67
  { module: "perps", chart: "Perpetual mark price", notInCatalog: "not-yet", reason: "The pair's price is in the catalog; the perpetual's own mark price could be a crypto series." },
@@ -96,6 +97,7 @@ export const SERIES_INVENTORY: readonly SeriesInventoryEntry[] = [
96
97
  { module: "analytics", chart: "Portfolio value, drawdown and factor exposure", notInCatalog: "account", reason: "Built from the user's own portfolio; the factor legs are tickers in the catalog." },
97
98
  { module: "holders", chart: "Holders treemap", notInCatalog: "not-a-time-series", reason: "A treemap of one filing date." },
98
99
  { module: "market-heatmap", chart: "Market heat map", notInCatalog: "not-a-time-series", reason: "A treemap of today's moves." },
100
+ { module: "buildout", chart: "TheBuildout's theme board", notInCatalog: "not-a-time-series", reason: "A treemap of the themes' market caps, coloured by their median 1Y return today." },
99
101
  { module: "portfolio-list", chart: "Portfolio treemap", notInCatalog: "not-a-time-series", reason: "A treemap of current positions." },
100
102
  { module: "world-venue-map", chart: "Chokepoint transits and the map's place series", series: ["geo:*"] },
101
103
  ];
@@ -1,6 +1,7 @@
1
1
  import { logFailure } from "../../../utils/log-failure";
2
- import { ApiRequestError, isPermanentClientError } from "../../../api-client/errors";
3
- import type { PluginCacheResult } from "../../../data/plugin-cache";
2
+ import { ApiRequestError } from "../../../api-client/errors";
3
+ import { createMissingRouteLatch } from "../../../api-client/coalesce";
4
+ import { savedCopyForTransientFailure, type PluginCacheResult } from "../../../data/plugin-cache";
4
5
 
5
6
  interface CloudCache<T> {
6
7
  get(key: string, options?: { allowExpired?: boolean }): PluginCacheResult<T> | null;
@@ -38,9 +39,11 @@ const isAbort = (error: unknown) => !!error && typeof error === "object" && (err
38
39
 
39
40
  /**
40
41
  * Loads through the cache, falling back to the cached copy when a refresh
41
- * fails. A refusal asking again will not change still throws: a refused
42
- * session, so the pane shows its sign-in wall instead of data the account can
43
- * no longer see, and any other permanent client error (an unknown series).
42
+ * fails for a reason asking again may get past (`isTransientFailure`). A
43
+ * refusal still throws: a refused session, so the pane shows its sign-in wall
44
+ * instead of data the account can no longer see, any other permanent client
45
+ * error (an unknown series, a route not served), and a reply that fails
46
+ * validation.
44
47
  *
45
48
  * Callers of one key share the request in flight, which runs on the first
46
49
  * caller's `signal`. When that caller goes away (a tab left while it loads,
@@ -60,8 +63,7 @@ export async function loadCloudResource<T>(
60
63
  throw error;
61
64
  });
62
65
  if (!result || cancelledByOther(result.error)) result = await cache.load(key, fetch, { force });
63
- const saved = result.source === "stale-fallback";
64
- if (saved && isPermanentClientError(result.error)) throw result.error;
66
+ const saved = savedCopyForTransientFailure(result).source === "stale-fallback";
65
67
  // A copy served because its refresh failed is one by where it came from, whatever the failure said (or did not).
66
68
  return {
67
69
  payload: validate(result.data), stale: result.stale, refreshError: result.refreshError ?? null,
@@ -72,7 +74,70 @@ export async function loadCloudResource<T>(
72
74
  /**
73
75
  * Turns a route the server does not serve yet (404, or the given statuses)
74
76
  * into a message that says what is unavailable; other errors pass through.
77
+ * The status stays, so a 404 is still a refusal no saved copy answers for and
78
+ * a 503 still a failure one may.
75
79
  */
76
80
  export function unavailableOnServer(error: unknown, message: string, statuses: readonly number[] = [404]): unknown {
77
- return error instanceof ApiRequestError && statuses.includes(error.status ?? 0) ? new Error(message) : error;
81
+ return error instanceof ApiRequestError && statuses.includes(error.status ?? 0)
82
+ ? new ApiRequestError(message, error.status, error.retryAfterMs, error.code, error.details)
83
+ : error;
84
+ }
85
+
86
+ /**
87
+ * Reads ahead many items' payloads in batch requests (`read`, at most `max`
88
+ * ids each, sent side by side) into the cache their own loads read under
89
+ * `key`, skipping an item cached fresh unless `force`. Answers the ids stored, which a forced load can
90
+ * take as fresh; an item the batch failed or left out stays for its own load
91
+ * to ask, which says why. A batch route the server does not serve (404) is
92
+ * left alone until it is asked again (BATCH_ROUTE_REPROBE_MS).
93
+ */
94
+ export function createCloudBatchAhead<T>(
95
+ cache: CloudCache<T>,
96
+ options: {
97
+ max: number;
98
+ validate: (payload: T) => T;
99
+ read: (ids: string[]) => Promise<Array<{ id: string; payload: T }>>;
100
+ },
101
+ ): (items: ReadonlyArray<{ id: string; key: string }>, force?: boolean) => Promise<Set<string>> {
102
+ const route = createMissingRouteLatch();
103
+ return async (items, force = false) => {
104
+ const stored = new Set<string>();
105
+ const keys = new Map<string, string>();
106
+ for (const { id, key } of items) {
107
+ const cached = cache.get(key);
108
+ if (force || !cached || cached.stale) keys.set(id, key);
109
+ }
110
+ const ids = [...keys.keys()];
111
+ if (route.missing() || ids.length < 2) return stored;
112
+ const chunks = Array.from({ length: Math.ceil(ids.length / options.max) }, (_, index) => ids.slice(index * options.max, (index + 1) * options.max));
113
+ await Promise.all(chunks.map(async (chunk) => {
114
+ const read = await options.read(chunk).catch((error: unknown) => {
115
+ route.refused(error);
116
+ if (!(error instanceof ApiRequestError && error.status === 404)) logFailure("shared.cloud-resource", "Read a batch of cloud payloads ahead failed")(error);
117
+ return [];
118
+ });
119
+ await Promise.all(read.map(async ({ id, payload }) => {
120
+ const key = keys.get(id);
121
+ if (!key) return;
122
+ try {
123
+ const valid = options.validate(payload);
124
+ await cache.load(key, async () => valid, { force: true });
125
+ stored.add(id);
126
+ } catch (error) {
127
+ logFailure("shared.cloud-resource", "Store a batched cloud payload failed")(error);
128
+ }
129
+ }));
130
+ }));
131
+ return stored;
132
+ };
133
+ }
134
+
135
+ /**
136
+ * A batch reply's items. A reply without them is a service that read the
137
+ * batch path as something else (an older one's `/:symbol` route, for a symbol
138
+ * named "batch"), which is a service without the route: a 404.
139
+ */
140
+ export function batchReplyItems<T>(reply: { items?: T[] } | null | undefined): T[] {
141
+ if (!Array.isArray(reply?.items)) throw new ApiRequestError("This service does not read these in batches", 404);
142
+ return reply.items;
78
143
  }
@@ -11,7 +11,7 @@ import { useRendererHost } from "../../../ui";
11
11
  import type { RendererHost } from "../../../ui";
12
12
  import { getSharedRegistry } from "../../registry";
13
13
  import { requestAccountManagementTab } from "../account-management/navigation";
14
- import { resolvePlanAccess, usePlanAccess, type PlanAccess } from "../../../api-client/plan-access";
14
+ import { freeQuotesDelayMinutes, resolvePlanAccess, usePlanAccess, type PlanAccess, type QuoteDelayFacts } from "../../../api-client/plan-access";
15
15
  import { useViewport } from "../../../react/input";
16
16
  import { type DialogApi, useOptionalDialog } from "../../../ui/dialog";
17
17
  import { promptCloudUpgrade } from "../../../account/upgrade-dialog";
@@ -365,3 +365,26 @@ export function useCloudAccessFooter({
365
365
 
366
366
  return { access, hint, openUpgrade, segment };
367
367
  }
368
+
369
+ /**
370
+ * The footer's clock for a pane whose prices this account sees late: the free
371
+ * tier's delay (`freeQuotesDelayMinutes`) on any of `quotes`, pressed for the
372
+ * upgrade, as the quote footer's clock is. Null with Pro or a trial, and while
373
+ * every price shown is live, so those accounts see nothing new. A pane whose
374
+ * data comes as one board says so with one `{ dataSource }` for the board.
375
+ */
376
+ export function useQuoteDelayFooter(
377
+ quotes: Iterable<QuoteDelayFacts | null | undefined>,
378
+ placement: string,
379
+ ): PaneFooterSegment | null {
380
+ const language = useAppLanguage();
381
+ const minutes = freeQuotesDelayMinutes(usePlanAccess(), quotes);
382
+ const openUpgrade = useCloudUpgradeAction(placement);
383
+ return useMemo(() => (minutes == null ? null : delayedFooterSegment({
384
+ id: "quote-delay",
385
+ detail: tf("{delay} delayed · try Pro live", { delay: `${minutes}m` }),
386
+ delay: `${minutes}m`,
387
+ onPress: openUpgrade,
388
+ quoteStatus: true,
389
+ })), [language, minutes, openUpgrade]);
390
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The step a daily study takes before it reads closes (BT's run, HVG's
3
+ * estimates): the newest session is left out while it is not final, and the
4
+ * report says so. Not final is a session still trading (`partial`: its close
5
+ * is the latest price) or a future's closed session whose settlement is not
6
+ * in yet (`unsettled`: its close is the last trade). Either close moves
7
+ * before the day is done, so a study that kept it would give another answer
8
+ * read again.
9
+ */
10
+ interface DatedBar {
11
+ partial?: boolean;
12
+ unsettled?: boolean;
13
+ }
14
+
15
+ export function isFormingBar(bar: DatedBar): boolean {
16
+ return bar.partial === true || bar.unsettled === true;
17
+ }
18
+
19
+ /**
20
+ * What the bars left out are, after their dates: "is still forming
21
+ * (partial)", "is not settled yet (unsettled)", or for several of both kinds
22
+ * "are still forming or not settled yet".
23
+ */
24
+ export function formingBarsState(bars: readonly DatedBar[]): string {
25
+ const verb = bars.length === 1 ? "is" : "are";
26
+ if (bars.every((bar) => bar.partial === true)) return `${verb} still forming (partial)`;
27
+ if (bars.every((bar) => bar.unsettled === true && bar.partial !== true)) return `${verb} not settled yet (unsettled)`;
28
+ return `${verb} still forming or not settled yet`;
29
+ }
@@ -1,11 +1,15 @@
1
1
  import { apiClient } from "../../../api-client";
2
2
  import { loadCachedFredSeries } from "../../../sources/gloomberb-cloud/fred-series";
3
3
 
4
- /** A FRED series from `start` on, oldest first, through the shared FRED cache. */
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
  }
@@ -2,6 +2,7 @@ import { intradayBarText } from "../../../market-data/quotes/intraday-bar";
2
2
  import type { Quote, QuoteIntradayBar } from "../../../types/financials";
3
3
  import { isFxMarketOpen } from "../../../utils/fx-market-hours";
4
4
  import { quoteFreshnessFields } from "./report-freshness";
5
+ import { formatUtcDayTime } from "../../../utils/utc-time";
5
6
 
6
7
  /** The venue FX pairs trade on, so a report can place the FX market. */
7
8
  const FX_VENUE = "CCY";
@@ -56,6 +57,22 @@ export function fxRateNotes(legs: ReadonlyMap<string, FxRateTiming>, now = Date.
56
57
  });
57
58
  }
58
59
 
60
+ /**
61
+ * What `fx --refresh` says of a rate it read again and found still stale with
62
+ * no reason given (`ZAR/USD: --refresh found no newer rate than Tue 27 Oct
63
+ * 14:00 UTC.`): the refresh did run, and the data service has nothing newer.
64
+ * A rate whose refresh failed says that instead, and one with a stated reason
65
+ * says the reason (fxRateNotes).
66
+ */
67
+ export function refreshedFxRateNotes(legs: ReadonlyMap<string, FxRateTiming>, now = Date.now()): string[] {
68
+ return [...legs].flatMap(([currency, timing]) => {
69
+ if (currency === "USD" || timing.error != null || timing.staleReason || timing.staleAt == null || timing.staleAt > now) return [];
70
+ const since = timing.asOf != null && Number.isFinite(timing.asOf)
71
+ ? ` than ${formatUtcDayTime({ time: timing.asOf, dateOnly: false }, now)} UTC` : "";
72
+ return [`${currency}/USD: --refresh found no newer rate${since}.`];
73
+ });
74
+ }
75
+
59
76
  function isoTime(time: number | undefined | null): string | undefined {
60
77
  return time != null && Number.isFinite(time) ? new Date(time).toISOString() : undefined;
61
78
  }
@@ -70,19 +70,23 @@ async function routeAnswers(route: FreeRoute, check: NonNullable<FreeRoute["answ
70
70
  * symbol the routes read `<ticker>` and the first is named; without a way to
71
71
  * read data, only a route that answers for every symbol is.
72
72
  */
73
- async function answeringFreeRoute(lock: PaneLock, routes: readonly FreeRoute[]): Promise<FreeRoute | null> {
73
+ async function answeringFreeRoute(lock: PaneLock, routes: readonly FreeRoute[]): Promise<{ route: FreeRoute | null; noneHasData: boolean }> {
74
74
  const { symbol, context } = lock;
75
- if (!symbol) return routes[0] ?? null;
75
+ if (!symbol) return { route: routes[0] ?? null, noneHasData: false };
76
76
  // Routes past the first that needs no check are never named, so they are not checked.
77
77
  const unchecked = routes.findIndex((route) => !route.answers);
78
78
  const candidates = unchecked < 0 ? routes : routes.slice(0, unchecked + 1);
79
79
  const deadline = Date.now() + ROUTE_CHECK_WAIT_MS;
80
80
  const checks = candidates.map((route) => !route.answers ? Promise.resolve(true)
81
- : context ? routeAnswers(route, route.answers, symbol, context, deadline) : Promise.resolve(false));
81
+ : context ? routeAnswers(route, route.answers, symbol, context, deadline) : Promise.resolve(null));
82
+ const answered: Array<boolean | null> = [];
82
83
  for (const [index, check] of checks.entries()) {
83
- if (await check) return candidates[index]!;
84
+ const answer = await check;
85
+ if (answer) return { route: candidates[index]!, noneHasData: false };
86
+ answered.push(answer);
84
87
  }
85
- return null;
88
+ // Every check answered no: a failed or stalled one is not known to.
89
+ return { route: null, noneHasData: candidates.length > 0 && answered.every((answer) => answer === false) };
86
90
  }
87
91
 
88
92
  /**
@@ -114,6 +118,14 @@ export async function proLockNote(
114
118
  * command repeats the ticker only when the reader gave one. Default `ticker`.
115
119
  */
116
120
  argument?: "ticker" | "optional" | "none";
121
+ /**
122
+ * For a function that reads what its free routes read (RISK the SEC's
123
+ * 10-Ks and 20-Fs, FLOW the listing's options): the sentence the note
124
+ * says when every route answered that the symbol has none. The note then
125
+ * names no command for the symbol, which would read nothing once unlocked
126
+ * either: IAG:LSE files neither form, and LHA:XETRA has no options served.
127
+ */
128
+ noneFor?: (ticker: string) => string;
117
129
  },
118
130
  ): Promise<string> {
119
131
  const { gate, symbol, token: typed, verifyEmail } = lock;
@@ -129,12 +141,15 @@ export async function proLockNote(
129
141
  const unlock = gate === "pro"
130
142
  ? `To read ${pane.shows} in ${token}, upgrade in the app (gloom, then Ctrl+P, UPGRADE)`
131
143
  : `To read ${pane.shows} in ${token}, open the app (gloom), ${account}${pro ? ", then upgrade (Ctrl+P, UPGRADE)" : ""}`;
132
- const bare = pane.argument === "none" || (pane.argument === "optional" && !symbol);
133
- const command = bare ? `gloom fn ${token}` : `gloom fn ${token} ${ticker}`;
134
- const free = pane.free ? await answeringFreeRoute(lock, pane.free) : null;
144
+ const { route: free, noneHasData } = pane.free ? await answeringFreeRoute(lock, pane.free) : { route: null, noneHasData: false };
145
+ // Unlocking reads nothing for a symbol none of the function's own data covers: it is named only where it has some.
146
+ const uncovered = noneHasData && !!pane.noneFor;
147
+ const bare = pane.argument === "none" || (pane.argument === "optional" && (!symbol || uncovered));
148
+ const command = bare ? `gloom fn ${token}` : `gloom fn ${token} ${uncovered ? "<ticker>" : ticker}`;
135
149
  return [
136
150
  `${token} ${what}: ${pane.does}. ${without} it shows none of ${pane.shows}.`,
137
151
  ...free ? [`Free, with no sign-in: ${free.text(ticker, symbol ?? "the company")}`] : [],
152
+ ...uncovered ? [pane.noneFor!(ticker)] : [],
138
153
  `${unlock}; ${command} then reads with that session.`,
139
154
  ].join("\n");
140
155
  }
@@ -111,7 +111,9 @@ export function SeriesDetailChart({
111
111
  }) {
112
112
  const [userViewport, setUserViewport] = useState<{ start: Date; end: Date } | null>(null);
113
113
  const chartWidth = Math.max(24, width);
114
- const statRows = statGridRows(stats, width);
114
+ // A figure wraps only into rows the chart's least leaves, so the pane never spills.
115
+ const statMaxLines = height - MIN_CHART_ROWS - aboveChartRows - belowChartRows;
116
+ const statRows = statGridRows(stats, width, undefined, { maxLines: statMaxLines });
115
117
  const chartHeight = Math.max(MIN_CHART_ROWS, height - statRows - aboveChartRows - belowChartRows);
116
118
  const { unit, unitGroup, nativeFrequency } = axis;
117
119
 
@@ -139,7 +141,7 @@ export function SeriesDetailChart({
139
141
 
140
142
  return (
141
143
  <Box flexDirection="column" width={width}>
142
- <StatGrid items={stats} width={width} />
144
+ <StatGrid items={stats} width={width} maxLines={statMaxLines} />
143
145
  {drawable ? (
144
146
  <>
145
147
  {aboveChart?.(chartWidth)}
@@ -1,5 +1,14 @@
1
- /** The note a loaded catalog series carries when it is drawn from an old copy. */
2
- export function cachedSeriesWarning(resource: { stale?: boolean; refreshError?: string | null }): string | undefined {
3
- if (resource.refreshError) return `Refresh failed (${resource.refreshError}); showing cached data.`;
4
- return resource.stale ? "Showing cached data; the source has not updated since." : undefined;
1
+ /**
2
+ * What a loaded catalog series says of an old copy. A copy that answered
3
+ * because its refresh failed carries when it was saved (`savedAt`), which the
4
+ * chart's footer shows as "saved copy from …", the note every pane gives it.
5
+ * A cached copy the source has not updated since is a note on the chart.
6
+ */
7
+ export function cachedSeriesNote(resource: { stale?: boolean; refreshError?: string | null; savedAt?: number | null }): {
8
+ warning?: string;
9
+ savedAt?: number;
10
+ } {
11
+ if (resource.savedAt != null && Number.isFinite(resource.savedAt)) return { savedAt: resource.savedAt };
12
+ if (resource.refreshError) return { warning: `Refresh failed (${resource.refreshError}); showing cached data.` };
13
+ return resource.stale ? { warning: "Showing cached data; the source has not updated since." } : {};
5
14
  }
@@ -520,6 +520,11 @@ export function quoteBoardStatus(quotes: BoardQuoteMap): QuoteBoardStatus {
520
520
  return { loading, stale, unavailable, latestTs, savedAt: oldestSavedCopyAt(Array.from(quotes.values(), (state) => state.quote)) };
521
521
  }
522
522
 
523
+ /** The quotes a board shows, for the footer's delayed clock (`useQuoteDelayFooter`). */
524
+ export function* boardQuotes(quotes: BoardQuoteMap): Generator<Quote | null> {
525
+ for (const state of quotes.values()) yield state.quote;
526
+ }
527
+
523
528
  /** Board footer status: everything here changes as loads succeed or fail. */
524
529
  export function quoteBoardFooterInfo(status: QuoteBoardStatus): PaneFooterSegment[] {
525
530
  const info: PaneFooterSegment[] = [];
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useMemo } from "react";
2
2
  import { DataTableStackView, EmptyState, PaneStatusBody, usePaneNoticeFooter, usePaneStatusLinkFooter, type DataTableCell } from "../../../components";
3
+ import { ageFooterSegment } from "../../../components/layout/pane/footer";
3
4
  import { DetailReader } from "../../../components/reader/detail-reader";
4
5
  import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
5
6
  import { useAsyncResource, useAutoRefresh, usePluginPaneState, useUpdatedAgo } from "../../../public/react";
@@ -40,7 +41,7 @@ export function ShortSidePublicationPane({ kind, width, height, focused }: Pick<
40
41
  notices: [...(data?.warnings ?? []), ...(resource.data?.refreshError ? [resource.data.refreshError] : [])] });
41
42
  usePaneStatusLinkFooter({ registrationId: kind, focused, loading: resource.loading, error: resource.error,
42
43
  url: selected?.sourceUrl ?? data?.source.url ?? null, showOpenHint: true,
43
- info: data && updatedAgo ? [{ id: "updated", parts: [{ text: updatedAgo, tone: "muted" as const }] }] : [],
44
+ info: data && updatedAgo ? [ageFooterSegment(updatedAgo)] : [],
44
45
  stale: !!data && resource.data?.stale,
45
46
  });
46
47
  if (!symbol) return <EmptyState title="No ticker selected." message={`Select a US stock to view ${subject}.`} />;
@@ -3,6 +3,7 @@ import { listingIdentity } from "../shared/ticker-request";
3
3
  import { Box, ScrollBox, useUiCapabilities } from "../../../ui";
4
4
  import { useAsyncResource, useAutoRefresh, usePaneSettingValue, usePluginPaneState, useUpdatedAgo } from "../../../public/react";
5
5
  import { ChartTableHeader, DataTableStackView, EmptyState, formatPercentAxis, KeyValueRow, PaneStatusBody, useChartTableSelection, usePaneNoticeFooter, usePaneStatusLinkFooter, type DataTableCell, type StatItem } from "../../../components";
6
+ import { ageFooterSegment } from "../../../components/layout/pane/footer";
6
7
  import { colors } from "../../../theme/colors";
7
8
  import type { ShortVolumeObservation } from "../../../api-client/short-volume";
8
9
  import { isAccessDenied } from "../../../api-client/errors";
@@ -83,7 +84,7 @@ export function ShortVolumePane({ width, height, focused }: Pick<PaneProps, "wid
83
84
  notices: [...(data?.warnings ?? []), ...(resource.data?.refreshError ? [resource.data.refreshError] : [])] });
84
85
  usePaneStatusLinkFooter({ registrationId: "short-volume", focused, loading: resource.loading, error: resource.error,
85
86
  url: selected?.sourceUrl ?? data?.source.url ?? null, showOpenHint: true,
86
- info: data && updatedAgo ? [{ id: "updated", parts: [{ text: updatedAgo, tone: "muted" as const }] }] : [],
87
+ info: data && updatedAgo ? [ageFooterSegment(updatedAgo)] : [],
87
88
  stale: !!data && resource.data?.stale,
88
89
  });
89
90
  if (!data && isCloudSessionRequired(resource.error)) return <SignInWall placement="short-volume-signin" action="view daily short volume" needsVerification={session.needsVerification} />;
@@ -1,5 +1,5 @@
1
1
  import type { SeriesDefinition } from "../../../series/types";
2
- import { cachedSeriesWarning } from "../shared/series-warning";
2
+ import { cachedSeriesNote } from "../shared/series-warning";
3
3
  import { loadShortVolume } from "./client";
4
4
 
5
5
  /** The share of a ticker's daily on-exchange volume reported as short sales. */
@@ -14,10 +14,9 @@ export const SHORT_VOLUME_SERIES: readonly SeriesDefinition[] = [{
14
14
  input: "ticker",
15
15
  load: async ({ ticker }) => {
16
16
  const resource = await loadShortVolume(ticker!, "nms");
17
- const warning = cachedSeriesWarning(resource);
18
17
  return {
19
18
  points: resource.payload.history.map((point) => ({ date: point.date, value: point.ratioPercent })),
20
- ...(warning ? { warning } : {}),
19
+ ...cachedSeriesNote(resource),
21
20
  };
22
21
  },
23
22
  }];