@gloom-sh/terminal 0.18.5 → 0.19.1

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 (570) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/package.json +1 -1
  4. package/patches/electrobun@1.18.1.patch +163 -8
  5. package/src/api-client/app-attention.ts +3 -0
  6. package/src/api-client/beneficial-owners.ts +3 -0
  7. package/src/api-client/company-kpis.ts +3 -0
  8. package/src/api-client/cot.ts +6 -2
  9. package/src/api-client/credit-documents.ts +6 -0
  10. package/src/api-client/data.ts +19 -7
  11. package/src/api-client/equity-screener.ts +24 -0
  12. package/src/api-client/fund-facts.ts +45 -4
  13. package/src/api-client/hiring.ts +3 -0
  14. package/src/api-client/index.ts +15 -0
  15. package/src/api-client/ipo.ts +24 -1
  16. package/src/api-client/market-discovery.ts +10 -0
  17. package/src/api-client/market-movers.ts +7 -0
  18. package/src/api-client/members.ts +1 -1
  19. package/src/api-client/mna.ts +3 -0
  20. package/src/api-client/paths.ts +9 -1
  21. package/src/api-client/perps.ts +43 -0
  22. package/src/api-client/research-activity.ts +3 -2
  23. package/src/api-client/roadmap.ts +85 -0
  24. package/src/api-client/types.ts +82 -2
  25. package/src/app/pane-runtime/plugin-bindings.ts +9 -5
  26. package/src/app/runtime/broker-import.ts +0 -2
  27. package/src/app/runtime/plugin-bindings.ts +73 -56
  28. package/src/architecture/core-builtin-allowlist.json +2 -2
  29. package/src/architecture/pane-tabs-allowlist.json +3 -0
  30. package/src/architecture/ttl-caches-allowlist.json +19 -0
  31. package/src/brokers/sync-broker-instance.ts +2 -8
  32. package/src/capabilities/chart-series.ts +1 -1
  33. package/src/cli/commands/crypto-hints.ts +40 -24
  34. package/src/cli/commands/market.ts +107 -38
  35. package/src/cli/commands/news-search.ts +22 -0
  36. package/src/cli/commands/options-chain.ts +48 -7
  37. package/src/cli/commands/options-leaps.ts +9 -3
  38. package/src/cli/commands/overview.ts +147 -32
  39. package/src/cli/commands/search.ts +21 -1
  40. package/src/cli/commands/themes.ts +11 -2
  41. package/src/cli/commands/ticker.ts +87 -36
  42. package/src/cli/company-name-hint.ts +41 -0
  43. package/src/cli/earnings-calendar.ts +1 -1
  44. package/src/cli/errors.ts +25 -2
  45. package/src/cli/glossary.ts +31 -2
  46. package/src/cli/history-rows.ts +57 -9
  47. package/src/cli/index-roots.ts +0 -7
  48. package/src/cli/index.ts +2 -0
  49. package/src/cli/listing-arg.ts +235 -60
  50. package/src/cli/output-file.ts +68 -0
  51. package/src/cli/pane-functions/capabilities.ts +4 -0
  52. package/src/cli/pane-functions/catalog.ts +80 -7
  53. package/src/cli/pane-functions/discovery.ts +21 -1
  54. package/src/cli/pane-functions/dom.ts +13 -3
  55. package/src/cli/pane-functions/freshness.ts +93 -4
  56. package/src/cli/pane-functions/function-help.ts +20 -10
  57. package/src/cli/pane-functions/headless.ts +17 -7
  58. package/src/cli/pane-functions/index.ts +54 -16
  59. package/src/cli/pane-functions/market-session.ts +66 -4
  60. package/src/cli/pane-functions/options.ts +19 -1
  61. package/src/cli/pane-functions/resolver.ts +19 -8
  62. package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
  63. package/src/cli/pane-functions/screenshot-types.ts +5 -0
  64. package/src/cli/quote-answer.ts +22 -11
  65. package/src/cli/saved-copy-persistence.ts +88 -0
  66. package/src/cli/ticker-resolution.ts +3 -2
  67. package/src/components/chart/composite/composite-chart.tsx +4 -0
  68. package/src/components/chart/composite/interactions.ts +6 -0
  69. package/src/components/chart/composite/legend.tsx +45 -7
  70. package/src/components/chart/composite/panel-gestures.ts +33 -0
  71. package/src/components/chart/composite/panel-surface.tsx +52 -3
  72. package/src/components/chart/composite/rasterizer.ts +24 -13
  73. package/src/components/chart/composite/scene.ts +39 -4
  74. package/src/components/chart/composite/series-cues.ts +14 -0
  75. package/src/components/chart/composite/text-renderer.ts +9 -2
  76. package/src/components/chart/composite/tool-catalog.ts +36 -0
  77. package/src/components/chart/composite/tools.ts +203 -44
  78. package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
  79. package/src/components/command-bar/assist/inventory.ts +2 -1
  80. package/src/components/command-bar/commands/direct/index.ts +28 -13
  81. package/src/components/command-bar/list/model.ts +5 -0
  82. package/src/components/command-bar/pane-templates/items.ts +3 -2
  83. package/src/components/command-bar/routes/root/runtime.ts +6 -7
  84. package/src/components/command-bar/selection-runtime.ts +52 -3
  85. package/src/components/command-bar/surface/index.tsx +2 -0
  86. package/src/components/command-bar/workflow/collection-actions.ts +3 -3
  87. package/src/components/layout/pane/capture-stamp.ts +2 -2
  88. package/src/components/layout/pane/footer/index.tsx +22 -6
  89. package/src/components/layout/pane/footer/model.ts +83 -15
  90. package/src/components/layout/pane/status-footer.ts +35 -2
  91. package/src/components/layout/status-widget-space.ts +9 -0
  92. package/src/components/onboarding/onboarding-wizard.tsx +4 -1
  93. package/src/components/onboarding/portfolio-step/positions-panel.tsx +12 -7
  94. package/src/components/onboarding/wizard-broker-sync.ts +0 -1
  95. package/src/components/onboarding/wizard-positions.ts +3 -3
  96. package/src/components/ticker/list-table-view.tsx +8 -21
  97. package/src/components/ui/choice-dialog.tsx +23 -18
  98. package/src/components/ui/choice-groups.ts +18 -0
  99. package/src/components/ui/data-table/opentui/index.tsx +9 -7
  100. package/src/components/ui/data-table/opentui/model.ts +26 -1
  101. package/src/components/ui/data-table/types.ts +2 -0
  102. package/src/components/ui/display.tsx +18 -3
  103. package/src/components/ui/icon.tsx +4 -1
  104. package/src/components/ui/list-view.tsx +8 -4
  105. package/src/components/ui/query-bar-keys.tsx +2 -0
  106. package/src/components/ui/query-bar.tsx +1 -0
  107. package/src/components/ui/select-button.tsx +5 -1
  108. package/src/components/ui/select-field.tsx +1 -0
  109. package/src/components/ui/table-layout.ts +8 -5
  110. package/src/core/app-runtime.ts +2 -0
  111. package/src/core/app-services.ts +2 -2
  112. package/src/data/config/chart-settings.ts +1 -1
  113. package/src/data/json-ticker-repository.ts +17 -8
  114. package/src/data/plugin-cache.ts +5 -3
  115. package/src/data/saved-copies.ts +8 -0
  116. package/src/data/ticker-repository.ts +16 -9
  117. package/src/data/ticker-writes.ts +81 -0
  118. package/src/data/ttl-cache.ts +165 -0
  119. package/src/docs/snapshot.json +103 -142
  120. package/src/i18n/es.ts +20 -6
  121. package/src/i18n/ja.ts +20 -6
  122. package/src/i18n/ko.ts +20 -6
  123. package/src/i18n/zh-cn.ts +20 -6
  124. package/src/i18n/zh-tw.ts +20 -6
  125. package/src/market-data/depositary-pair.ts +27 -3
  126. package/src/market-data/fund-facts.ts +50 -0
  127. package/src/market-data/market/change-basis.ts +16 -3
  128. package/src/market-data/market/format.ts +25 -0
  129. package/src/market-data/market/freshness.ts +2 -1
  130. package/src/market-data/options-alternatives.ts +81 -4
  131. package/src/market-data/quotes/metadata.ts +7 -0
  132. package/src/news/aggregator.ts +119 -9
  133. package/src/news/cloud-session.ts +21 -0
  134. package/src/news/hooks.ts +1 -0
  135. package/src/news/news-model.ts +1 -0
  136. package/src/news/types.ts +17 -0
  137. package/src/plugins/builtin/alerts/alert-engine.ts +20 -1
  138. package/src/plugins/builtin/alerts/alert-log.ts +33 -0
  139. package/src/plugins/builtin/alerts/calendar-events.ts +10 -4
  140. package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
  141. package/src/plugins/builtin/alerts/cli.ts +362 -80
  142. package/src/plugins/builtin/alerts/command.ts +25 -1
  143. package/src/plugins/builtin/alerts/event-rows.ts +24 -3
  144. package/src/plugins/builtin/alerts/events-pane.tsx +34 -10
  145. package/src/plugins/builtin/alerts/fired-log.ts +73 -9
  146. package/src/plugins/builtin/alerts/format.ts +75 -5
  147. package/src/plugins/builtin/alerts/headless.ts +34 -12
  148. package/src/plugins/builtin/alerts/history-rows.ts +3 -2
  149. package/src/plugins/builtin/alerts/history.ts +1 -1
  150. package/src/plugins/builtin/alerts/index.tsx +79 -16
  151. package/src/plugins/builtin/alerts/metric-command.ts +77 -4
  152. package/src/plugins/builtin/alerts/metric-data.ts +54 -13
  153. package/src/plugins/builtin/alerts/metric-model.ts +163 -14
  154. package/src/plugins/builtin/alerts/metric-parity-fixture.json +1 -1
  155. package/src/plugins/builtin/alerts/pane.tsx +61 -23
  156. package/src/plugins/builtin/alerts/quotes.ts +6 -0
  157. package/src/plugins/builtin/alerts/storage.ts +12 -8
  158. package/src/plugins/builtin/alerts/types.ts +4 -0
  159. package/src/plugins/builtin/analytics/index.tsx +2 -1
  160. package/src/plugins/builtin/analytics/risk-client.ts +16 -5
  161. package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
  162. package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
  163. package/src/plugins/builtin/analytics/risk-model.ts +55 -9
  164. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  165. package/src/plugins/builtin/attention/index.tsx +2 -1
  166. package/src/plugins/builtin/attention/listing.ts +14 -0
  167. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  168. package/src/plugins/builtin/buildout/cells.tsx +12 -4
  169. package/src/plugins/builtin/buildout/format.ts +38 -0
  170. package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
  171. package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
  172. package/src/plugins/builtin/buildout/table-model.ts +23 -2
  173. package/src/plugins/builtin/builtin-plugin-meta.ts +6 -2
  174. package/src/plugins/builtin/cds/client.ts +142 -30
  175. package/src/plugins/builtin/cds/headless.ts +35 -8
  176. package/src/plugins/builtin/cds/model.ts +145 -18
  177. package/src/plugins/builtin/cds/pane.tsx +16 -5
  178. package/src/plugins/builtin/changelog/index.tsx +9 -6
  179. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -0
  180. package/src/plugins/builtin/chart-composer/cli-options.ts +4 -0
  181. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +7 -0
  182. package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -2
  183. package/src/plugins/builtin/chart-composer/follow-binding.ts +14 -2
  184. package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -1
  185. package/src/plugins/builtin/chart-composer/headless.ts +6 -1
  186. package/src/plugins/builtin/chart-composer/index.tsx +5 -6
  187. package/src/plugins/builtin/chart-composer/pane.tsx +37 -8
  188. package/src/plugins/builtin/chart-composer/settings.ts +27 -0
  189. package/src/plugins/builtin/chat/content/command-suggestions.ts +1 -0
  190. package/src/plugins/builtin/chat/controller/index.ts +5 -1
  191. package/src/plugins/builtin/chat/controller/notifications.ts +33 -3
  192. package/src/plugins/builtin/chat/controller/state.ts +25 -2
  193. package/src/plugins/builtin/chat/controller/unread-people.ts +89 -23
  194. package/src/plugins/builtin/chat/controller/utils.ts +7 -2
  195. package/src/plugins/builtin/chat/pane-links/commands.ts +7 -1
  196. package/src/plugins/builtin/chat/status-preview.ts +79 -0
  197. package/src/plugins/builtin/chat/status-widget.tsx +147 -10
  198. package/src/plugins/builtin/chat/unread-badge.ts +51 -10
  199. package/src/plugins/builtin/clinical-trials/client.ts +49 -4
  200. package/src/plugins/builtin/clinical-trials/model.ts +86 -31
  201. package/src/plugins/builtin/clinical-trials/pane.tsx +61 -49
  202. package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
  203. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +95 -15
  204. package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
  205. package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
  206. package/src/plugins/builtin/company-attention/building.tsx +5 -1
  207. package/src/plugins/builtin/company-attention/client.ts +8 -9
  208. package/src/plugins/builtin/company-attention/evidence.ts +4 -1
  209. package/src/plugins/builtin/company-attention/headless.ts +2 -1
  210. package/src/plugins/builtin/company-attention/moves.tsx +5 -1
  211. package/src/plugins/builtin/company-attention/pane.tsx +12 -2
  212. package/src/plugins/builtin/company-attention/signals.tsx +5 -1
  213. package/src/plugins/builtin/company-kpis/headless.ts +8 -3
  214. package/src/plugins/builtin/company-kpis/model.ts +73 -5
  215. package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
  216. package/src/plugins/builtin/composite-plugins.ts +2 -1
  217. package/src/plugins/builtin/correlation/headless.ts +13 -5
  218. package/src/plugins/builtin/correlation/history.ts +22 -0
  219. package/src/plugins/builtin/cot/client.ts +22 -18
  220. package/src/plugins/builtin/cot/headless.ts +15 -10
  221. package/src/plugins/builtin/cot/model.ts +14 -1
  222. package/src/plugins/builtin/cot/pane.tsx +22 -14
  223. package/src/plugins/builtin/credit-boards/client.ts +6 -10
  224. package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
  225. package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
  226. package/src/plugins/builtin/credit-conditions/model.ts +4 -0
  227. package/src/plugins/builtin/credit-conditions/series.ts +3 -1
  228. package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
  229. package/src/plugins/builtin/credit-documents/export.ts +173 -0
  230. package/src/plugins/builtin/credit-documents/headless.ts +10 -10
  231. package/src/plugins/builtin/credit-documents/model.ts +104 -14
  232. package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
  233. package/src/plugins/builtin/custom-view/sources.ts +2 -0
  234. package/src/plugins/builtin/dividend-yield/client.ts +131 -23
  235. package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
  236. package/src/plugins/builtin/dividend-yield/model.ts +8 -5
  237. package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
  238. package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
  239. package/src/plugins/builtin/dividend-yield/types.ts +20 -2
  240. package/src/plugins/builtin/dividend-yield/view.ts +30 -2
  241. package/src/plugins/builtin/docs/targets.ts +5 -1
  242. package/src/plugins/builtin/doe/headless.ts +3 -2
  243. package/src/plugins/builtin/doe/pane.tsx +13 -3
  244. package/src/plugins/builtin/earnings/board-model.ts +16 -15
  245. package/src/plugins/builtin/earnings/board.tsx +62 -71
  246. package/src/plugins/builtin/earnings/headless.ts +40 -4
  247. package/src/plugins/builtin/earnings/scope.ts +31 -43
  248. package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
  249. package/src/plugins/builtin/earnings-calls/data.ts +18 -6
  250. package/src/plugins/builtin/earnings-calls/format.ts +31 -2
  251. package/src/plugins/builtin/earnings-calls/headless.ts +20 -5
  252. package/src/plugins/builtin/earnings-calls/model.ts +6 -1
  253. package/src/plugins/builtin/earnings-calls/pane.tsx +12 -4
  254. package/src/plugins/builtin/econ/calendar-model.ts +48 -1
  255. package/src/plugins/builtin/econ/calendar-source.ts +4 -2
  256. package/src/plugins/builtin/econ/detail-view.tsx +2 -2
  257. package/src/plugins/builtin/econ/figures.ts +48 -0
  258. package/src/plugins/builtin/econ/index.tsx +14 -5
  259. package/src/plugins/builtin/econ/print-watch.ts +3 -1
  260. package/src/plugins/builtin/econ/types.ts +2 -0
  261. package/src/plugins/builtin/econ-statistics/client.ts +11 -0
  262. package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
  263. package/src/plugins/builtin/econ-statistics/headless.ts +16 -6
  264. package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
  265. package/src/plugins/builtin/econ-statistics/stats.ts +39 -9
  266. package/src/plugins/builtin/equity-screener/categories.ts +179 -0
  267. package/src/plugins/builtin/equity-screener/client.ts +4 -2
  268. package/src/plugins/builtin/equity-screener/headless.ts +144 -13
  269. package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
  270. package/src/plugins/builtin/equity-screener/model.ts +99 -10
  271. package/src/plugins/builtin/equity-screener/pane.tsx +68 -13
  272. package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
  273. package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
  274. package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
  275. package/src/plugins/builtin/fx-matrix/client.ts +6 -3
  276. package/src/plugins/builtin/fx-matrix/headless.ts +1 -1
  277. package/src/plugins/builtin/fx-matrix/index.tsx +4 -2
  278. package/src/plugins/builtin/fx-matrix/live-legs.ts +42 -3
  279. package/src/plugins/builtin/gpu/headless.ts +8 -2
  280. package/src/plugins/builtin/help/function-index.ts +4 -1
  281. package/src/plugins/builtin/help/index.tsx +62 -59
  282. package/src/plugins/builtin/help/shortcut-model.ts +1 -1
  283. package/src/plugins/builtin/help/shortcut-table.tsx +5 -6
  284. package/src/plugins/builtin/help/table-cells.tsx +13 -2
  285. package/src/plugins/builtin/holders/beneficial-client.ts +5 -1
  286. package/src/plugins/builtin/holders/beneficial-view.tsx +3 -0
  287. package/src/plugins/builtin/holders/fund-holders.ts +51 -0
  288. package/src/plugins/builtin/holders/headless.ts +22 -3
  289. package/src/plugins/builtin/holders/pane.tsx +3 -0
  290. package/src/plugins/builtin/holders/report-rows.ts +4 -1
  291. package/src/plugins/builtin/insider/headless.ts +2 -2
  292. package/src/plugins/builtin/ipo-calendar/client.ts +18 -2
  293. package/src/plugins/builtin/ipo-calendar/headless.ts +75 -11
  294. package/src/plugins/builtin/ipo-calendar/listing-ipo.ts +160 -0
  295. package/src/plugins/builtin/ipo-calendar/model.ts +98 -2
  296. package/src/plugins/builtin/iv-history/client.ts +52 -11
  297. package/src/plugins/builtin/iv-history/format.ts +36 -23
  298. package/src/plugins/builtin/iv-history/headless.ts +33 -18
  299. package/src/plugins/builtin/iv-history/index.tsx +2 -2
  300. package/src/plugins/builtin/iv-history/model.ts +163 -27
  301. package/src/plugins/builtin/iv-history/pane.tsx +10 -6
  302. package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
  303. package/src/plugins/builtin/iv-history/universe.ts +5 -1
  304. package/src/plugins/builtin/jobs/client.ts +25 -29
  305. package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
  306. package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
  307. package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
  308. package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
  309. package/src/plugins/builtin/macro-day/headless.ts +12 -5
  310. package/src/plugins/builtin/macro-day/index.tsx +1 -1
  311. package/src/plugins/builtin/macro-day/model.ts +39 -5
  312. package/src/plugins/builtin/macro-day/pane.tsx +27 -19
  313. package/src/plugins/builtin/macro-day/release-table.ts +2 -2
  314. package/src/plugins/builtin/market-heatmap/data.ts +8 -33
  315. package/src/plugins/builtin/market-movers/client.ts +27 -7
  316. package/src/plugins/builtin/market-movers/headless.ts +34 -12
  317. package/src/plugins/builtin/market-movers/index.tsx +26 -14
  318. package/src/plugins/builtin/market-movers/min-float.ts +74 -0
  319. package/src/plugins/builtin/market-movers/model.ts +4 -1
  320. package/src/plugins/builtin/market-movers/screener.ts +22 -4
  321. package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
  322. package/src/plugins/builtin/market-movers/session-table.ts +4 -3
  323. package/src/plugins/builtin/market-movers/table.ts +17 -5
  324. package/src/plugins/builtin/members/breadth-pane.tsx +2 -1
  325. package/src/plugins/builtin/mna/headless.ts +2 -0
  326. package/src/plugins/builtin/mna/pane.tsx +3 -1
  327. package/src/plugins/builtin/news/headless.ts +3 -1
  328. package/src/plugins/builtin/news/index.tsx +4 -0
  329. package/src/plugins/builtin/news/wire/categories.ts +19 -3
  330. package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
  331. package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
  332. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
  333. package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
  334. package/src/plugins/builtin/openfda/client.ts +14 -2
  335. package/src/plugins/builtin/openfda/index.tsx +3 -2
  336. package/src/plugins/builtin/openfda/model.ts +9 -0
  337. package/src/plugins/builtin/openfda/pane.tsx +42 -6
  338. package/src/plugins/builtin/options/analytics.ts +29 -5
  339. package/src/plugins/builtin/options/enrichment.ts +17 -5
  340. package/src/plugins/builtin/options/table.ts +5 -2
  341. package/src/plugins/builtin/options/types.ts +2 -0
  342. package/src/plugins/builtin/options/view.tsx +17 -4
  343. package/src/plugins/builtin/options-positioning/client.ts +11 -0
  344. package/src/plugins/builtin/options-positioning/headless.ts +21 -8
  345. package/src/plugins/builtin/options-positioning/index.tsx +1 -1
  346. package/src/plugins/builtin/options-positioning/model.ts +78 -9
  347. package/src/plugins/builtin/options-positioning/pane.tsx +21 -6
  348. package/src/plugins/builtin/options-scenario/client.ts +116 -47
  349. package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
  350. package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
  351. package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
  352. package/src/plugins/builtin/options-scenario/csp.ts +8 -2
  353. package/src/plugins/builtin/options-scenario/index.tsx +9 -7
  354. package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
  355. package/src/plugins/builtin/pe-band/model.ts +12 -49
  356. package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
  357. package/src/plugins/builtin/perps/cascade.ts +236 -0
  358. package/src/plugins/builtin/perps/client.ts +56 -2
  359. package/src/plugins/builtin/perps/headless.ts +65 -9
  360. package/src/plugins/builtin/perps/index.tsx +7 -5
  361. package/src/plugins/builtin/perps/model.ts +18 -4
  362. package/src/plugins/builtin/perps/pane.tsx +19 -22
  363. package/src/plugins/builtin/perps/status-line.ts +25 -0
  364. package/src/plugins/builtin/plugin-module.ts +5 -1
  365. package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
  366. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
  367. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
  368. package/src/plugins/builtin/portfolio-list/cli/render.ts +7 -5
  369. package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
  370. package/src/plugins/builtin/portfolio-list/pane/index.tsx +6 -7
  371. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
  372. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
  373. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +0 -14
  374. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +5 -2
  375. package/src/plugins/builtin/portfolio-list/table.tsx +15 -9
  376. package/src/plugins/builtin/primary-dealers/client.ts +13 -38
  377. package/src/plugins/builtin/realized-vol/client.ts +13 -5
  378. package/src/plugins/builtin/realized-vol/evidence.ts +29 -4
  379. package/src/plugins/builtin/realized-vol/headless.ts +8 -4
  380. package/src/plugins/builtin/realized-vol/index.tsx +2 -2
  381. package/src/plugins/builtin/realized-vol/model.ts +37 -6
  382. package/src/plugins/builtin/realized-vol/pane.tsx +11 -9
  383. package/src/plugins/builtin/relative-rotation/client.ts +18 -4
  384. package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
  385. package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
  386. package/src/plugins/builtin/relative-rotation/model.ts +16 -40
  387. package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
  388. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
  389. package/src/plugins/builtin/research/event-model.ts +40 -12
  390. package/src/plugins/builtin/research/headless.ts +1 -1
  391. package/src/plugins/builtin/research/index.tsx +7 -3
  392. package/src/plugins/builtin/research/relative-valuation-headless.ts +13 -26
  393. package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
  394. package/src/plugins/builtin/research/relative-valuation-pane.tsx +59 -20
  395. package/src/plugins/builtin/research/relative-valuation-peers.ts +217 -20
  396. package/src/plugins/builtin/research/relative-valuation-settings.ts +44 -0
  397. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
  398. package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
  399. package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
  400. package/src/plugins/builtin/risk-factors/data.ts +4 -2
  401. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  402. package/src/plugins/builtin/roadmap/index.tsx +28 -0
  403. package/src/plugins/builtin/roadmap/model.ts +67 -0
  404. package/src/plugins/builtin/roadmap/pane.tsx +244 -0
  405. package/src/plugins/builtin/scanner/flow-model.ts +50 -4
  406. package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
  407. package/src/plugins/builtin/scanner/flow-session.ts +146 -0
  408. package/src/plugins/builtin/scanner/headless.ts +204 -35
  409. package/src/plugins/builtin/scanner/index.ts +16 -5
  410. package/src/plugins/builtin/scanner/live-headless.ts +48 -5
  411. package/src/plugins/builtin/scanner/live-model.ts +11 -0
  412. package/src/plugins/builtin/scanner/live-pane.tsx +3 -3
  413. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  414. package/src/plugins/builtin/seasonality/headless.ts +10 -1
  415. package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
  416. package/src/plugins/builtin/sec/fund-facts.ts +116 -34
  417. package/src/plugins/builtin/sec/headless.ts +4 -2
  418. package/src/plugins/builtin/sec/index.tsx +3 -0
  419. package/src/plugins/builtin/series-inventory.ts +2 -0
  420. package/src/plugins/builtin/shared/cloud-resource.ts +16 -7
  421. package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
  422. package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
  423. package/src/plugins/builtin/shared/earnings-report.ts +2 -2
  424. package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
  425. package/src/plugins/builtin/shared/job-posting.ts +1 -0
  426. package/src/plugins/builtin/shared/members-menu.tsx +3 -2
  427. package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
  428. package/src/plugins/builtin/shared/report-freshness.ts +11 -2
  429. package/src/plugins/builtin/shared/research-cells.tsx +1 -1
  430. package/src/plugins/builtin/shared/series-cache.ts +4 -4
  431. package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
  432. package/src/plugins/builtin/shared/ticker-company.ts +77 -0
  433. package/src/plugins/builtin/shared/use-quote-board.ts +2 -1
  434. package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
  435. package/src/plugins/builtin/shared/volatility/pricing.ts +19 -0
  436. package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
  437. package/src/plugins/builtin/themes/model.ts +16 -7
  438. package/src/plugins/builtin/themes/pane.tsx +5 -5
  439. package/src/plugins/builtin/thirteenf/api.ts +12 -1
  440. package/src/plugins/builtin/thirteenf/data.ts +7 -7
  441. package/src/plugins/builtin/thirteenf/headless.ts +31 -4
  442. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
  443. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
  444. package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
  445. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
  446. package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
  447. package/src/plugins/builtin/ticker-detail/overview/model.ts +11 -3
  448. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
  449. package/src/plugins/builtin/ticker-detail/pane.tsx +14 -8
  450. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
  451. package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
  452. package/src/plugins/builtin/time-sales/pane.tsx +31 -19
  453. package/src/plugins/builtin/treasury-auctions/headless.ts +21 -12
  454. package/src/plugins/builtin/treasury-auctions/pane.tsx +9 -3
  455. package/src/plugins/builtin/treasury-daily/client.ts +41 -5
  456. package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
  457. package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
  458. package/src/plugins/builtin/treasury-daily/model.ts +50 -3
  459. package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
  460. package/src/plugins/builtin/vol-surface/client.ts +3 -13
  461. package/src/plugins/builtin/vol-surface/headless.ts +3 -3
  462. package/src/plugins/builtin/vol-surface/index.tsx +1 -1
  463. package/src/plugins/builtin/vol-surface/pane.tsx +7 -5
  464. package/src/plugins/builtin/volatility/index.tsx +2 -2
  465. package/src/plugins/builtin/volatility/pane.tsx +7 -4
  466. package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
  467. package/src/plugins/builtin/yield-curve/client.ts +61 -1
  468. package/src/plugins/builtin/yield-curve/headless.ts +9 -6
  469. package/src/plugins/builtin/yield-curve/index.tsx +34 -10
  470. package/src/plugins/builtin/yield-curve/swaps.tsx +9 -5
  471. package/src/plugins/catalog-browser.ts +2 -1
  472. package/src/plugins/catalog-manifest.ts +2 -1
  473. package/src/plugins/host-link.ts +1 -1
  474. package/src/plugins/marketplace/model.ts +18 -5
  475. package/src/portfolio/column-values.ts +11 -7
  476. package/src/portfolio/futures-positions.ts +6 -3
  477. package/src/portfolio/position-quantity.ts +15 -9
  478. package/src/remote/resources.ts +2 -1
  479. package/src/renderers/dom/data-table/row.tsx +26 -0
  480. package/src/renderers/dom/desktop/icons.tsx +7 -0
  481. package/src/renderers/dom/desktop/list-view.tsx +13 -10
  482. package/src/renderers/dom/desktop/query-bar.tsx +4 -2
  483. package/src/renderers/dom/desktop/select-field.tsx +4 -2
  484. package/src/renderers/dom/host/chart-surface.tsx +21 -0
  485. package/src/renderers/electrobun/bun/desktop/app-name.ts +3 -3
  486. package/src/renderers/electrobun/bun/desktop/backend.ts +21 -10
  487. package/src/renderers/electrobun/bun/desktop/host-requests.ts +15 -0
  488. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
  489. package/src/renderers/electrobun/bun/desktop/installer-folder.ts +731 -0
  490. package/src/renderers/electrobun/bun/desktop/relaunch.ts +102 -10
  491. package/src/renderers/electrobun/bun/desktop/update.ts +115 -14
  492. package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
  493. package/src/renderers/electrobun/bun/desktop/windows-script.ts +92 -0
  494. package/src/renderers/electrobun/bun/index.ts +26 -2
  495. package/src/renderers/electrobun/bun/window/create.ts +43 -4
  496. package/src/renderers/electrobun/bun/window/events.ts +20 -0
  497. package/src/renderers/electrobun/bun/window/windows-custom-chrome.ts +9 -2
  498. package/src/renderers/electrobun/bun/window/windows-native.ts +78 -0
  499. package/src/renderers/electrobun/bun/window/windows-quit.ts +57 -0
  500. package/src/renderers/electrobun/shared/app-name.ts +13 -0
  501. package/src/renderers/electrobun/shared/protocol.ts +12 -0
  502. package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
  503. package/src/renderers/electrobun/view/main.tsx +24 -1
  504. package/src/renderers/electrobun/view/news-source.ts +5 -1
  505. package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
  506. package/src/renderers/electrobun/view/ui-host.tsx +8 -0
  507. package/src/renderers/electrobun/view/view-ids.ts +85 -0
  508. package/src/renderers/opentui/start.tsx +5 -0
  509. package/src/series/registry.ts +1 -0
  510. package/src/series/types.ts +6 -0
  511. package/src/series/validate.ts +1 -0
  512. package/src/sources/gloomberb-cloud/fred-series.ts +3 -1
  513. package/src/sources/gloomberb-cloud/index.ts +43 -8
  514. package/src/sources/gloomberb-cloud/news.ts +47 -7
  515. package/src/sources/provider-router/financials.ts +15 -3
  516. package/src/sources/provider-router/index.ts +8 -5
  517. package/src/state/ticker-cleanup.ts +50 -0
  518. package/src/sync/controller.ts +4 -3
  519. package/src/sync/core-contributors.ts +90 -31
  520. package/src/telemetry/stall-reports-dom.ts +38 -0
  521. package/src/telemetry/stall-reports-node.ts +75 -0
  522. package/src/telemetry/stall-reports.ts +214 -0
  523. package/src/test-support/preload.ts +14 -0
  524. package/src/theme/themes.ts +10 -0
  525. package/src/tickers/company-name-listing.ts +53 -13
  526. package/src/tickers/company-name.ts +50 -4
  527. package/src/tickers/index-roots.ts +7 -0
  528. package/src/tickers/listing-fate.ts +2 -2
  529. package/src/tickers/listing-venues.ts +15 -3
  530. package/src/tickers/search/index.ts +6 -2
  531. package/src/tickers/search/ranking.ts +5 -1
  532. package/src/tickers/search/upsert.ts +13 -0
  533. package/src/tickers/terminal-codes.ts +19 -0
  534. package/src/tickers/ticker-changes.ts +8 -4
  535. package/src/time-series/alignment.ts +1 -1
  536. package/src/time-series/economic.ts +18 -4
  537. package/src/time-series/field-catalog.ts +1 -1
  538. package/src/time-series/filed-trailing-eps.ts +197 -0
  539. package/src/time-series/fundamentals.ts +44 -1
  540. package/src/time-series/ice-credit.ts +3 -0
  541. package/src/time-series/price-comparison.ts +30 -2
  542. package/src/time-series/resolve-context.ts +3 -1
  543. package/src/time-series/resolve.ts +56 -4
  544. package/src/time-series/session-bar.ts +11 -0
  545. package/src/time-series/spec.ts +3 -2
  546. package/src/time-series/studies.ts +8 -2
  547. package/src/time-series/total-return.ts +160 -0
  548. package/src/time-series/transforms.ts +2 -3
  549. package/src/time-series/types.ts +22 -1
  550. package/src/types/company-from.ts +40 -0
  551. package/src/types/data-provider.ts +7 -0
  552. package/src/types/financials.ts +59 -1
  553. package/src/types/headless.ts +27 -3
  554. package/src/types/news-source.ts +1 -0
  555. package/src/types/plugin.ts +9 -0
  556. package/src/ui/host.tsx +3 -0
  557. package/src/updater/failure.ts +4 -1
  558. package/src/utils/cli-output.ts +42 -1
  559. package/src/utils/crypto-pair.ts +165 -7
  560. package/src/utils/declared-dividends.ts +1 -1
  561. package/src/utils/forward-dividend.ts +2 -0
  562. package/src/utils/fundamentals.ts +60 -8
  563. package/src/utils/fx-market-hours.ts +26 -3
  564. package/src/utils/main-thread-stall.ts +35 -2
  565. package/src/utils/market-capitalization.ts +24 -1
  566. package/src/utils/price-history.ts +7 -1
  567. package/src/utils/shell-word.ts +8 -0
  568. package/src/utils/trailing-dividend.ts +53 -4
  569. package/src/version.ts +2 -2
  570. package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
@@ -1,6 +1,7 @@
1
1
  import type { HeadlessPaneDefinition } from "../../../types/plugin";
2
2
  import { resolveHeadlessInstrument } from "../shared/headless-market-data";
3
3
  import { loadSeasonalityHistory } from "./client";
4
+ import { closedHistoryBarDay, formingHistoryBar } from "../ticker-detail/data-panes/history-bars";
4
5
  import { MONTH_LABELS, projectSeasonality, projectWeekdays, TURN_OF_MONTH_LABELS } from "./model";
5
6
 
6
7
  const percent = (digits: number) => (value: unknown) => typeof value === "number" ? `${(value * 100).toFixed(digits)}%` : "--";
@@ -9,7 +10,7 @@ const statColumns = (digits: number, countHeader: string) => [{ key: "mean", hea
9
10
 
10
11
  export const seasonalityHeadless: HeadlessPaneDefinition<"bundle"> = {
11
12
  shape: "bundle", argument: { kind: "ticker", description: "Ticker" },
12
- // Dated by the month of its last monthly close.
13
+ // Dated by its last monthly close, or by how far the running month's bar runs (`month to date, through Fri 23 Oct 2026`).
13
14
  freshness: { status: "not-a-feed", basis: "monthly closes", maxAgeMinutes: 45 * 24 * 60 },
14
15
  describe: (args) => `SEAS ${args.symbols[0] ?? ""}`,
15
16
  discovery: { screenshotReadiness: "live-dom", limitations: ["Local-price closes; dividends and FX are excluded.",
@@ -46,6 +47,13 @@ export const seasonalityHeadless: HeadlessPaneDefinition<"bundle"> = {
46
47
  };
47
48
  }
48
49
  const model = projectSeasonality(history.history, { symbol: instrument.symbol, lookbackYears: Number(args.options.lookbackYears) || 10 });
50
+ // Monthly bars are dated by their month's first day, which is no close while the month runs.
51
+ const bar = model.asOf?.toISOString().slice(0, 10);
52
+ // The venue whose sessions say whether the month still runs: the key's, or for a bare symbol its quote's.
53
+ const quote = bar && !instrument.exchange ? await ctx.marketData.getQuote(instrument.symbol, "").catch(() => null) : null;
54
+ const venue = instrument.exchange || quote?.listingExchangeName || quote?.exchangeName || undefined;
55
+ const forming = formingHistoryBar("1mo", bar, venue, Date.now());
56
+ const closed = forming ? null : closedHistoryBarDay("1mo", bar, venue, Date.now());
49
57
  const monthColumns = MONTH_LABELS.map((label, month) => ({ key: `m${month}`, header: label, format: percent(1) }));
50
58
  return {
51
59
  sections: [
@@ -57,6 +65,7 @@ export const seasonalityHeadless: HeadlessPaneDefinition<"bundle"> = {
57
65
  complete: !history.stale && !history.error && model.years.length > 0,
58
66
  unavailableSymbols: model.years.length ? [] : [instrument.symbol],
59
67
  ...common,
68
+ ...(forming ? { freshness: { periodToDate: forming } } : closed ? { freshness: { asOf: closed } } : {}),
60
69
  metadata: { unit: "decimal return, local-price close to close", asOf: model.asOf?.toISOString() ?? null, stale: history.stale,
61
70
  fetchedAt: history.fetchedAt, averagePath: model.averagePath, methodology: "docs/research-data.md#seasonality" },
62
71
  };
@@ -4,7 +4,7 @@ import { useAsyncResource } from "../../../react/async-resource";
4
4
  import { Box } from "../../../ui";
5
5
  import { t } from "../../../i18n";
6
6
  import type { FundFactsPayload } from "../../../api-client/fund-facts";
7
- import { cachedFundFacts, fundFactItems, fundFactsEmptyText, fundFactsErrorLine, fundFactsStaleText, loadFundFacts } from "./fund-facts";
7
+ import { cachedFundFacts, fundFactItems, fundFactsEmptyText, fundFactsErrorLine, fundFactsNavReasonText, fundFactsStaleText, loadFundFacts } from "./fund-facts";
8
8
  import type { CloudResource } from "../shared/cloud-resource";
9
9
 
10
10
  /** A fund's facts, served from the cache first and refreshed behind it; null asks for nothing. */
@@ -18,7 +18,8 @@ export function useFundFacts(symbol: string | null | undefined) {
18
18
  /**
19
19
  * The fund's facts as the summary sheet over its filings, or one line saying
20
20
  * why there are none. Loading is one muted line, so the filings never wait.
21
- * A stale NAV adds the line saying since when, under the marked figures.
21
+ * A stale NAV adds the line saying since when, under the marked figures; a
22
+ * fund without a NAV, the line saying why.
22
23
  */
23
24
  export function FundFactsSummary({ symbol, width, facts, loading, error }: {
24
25
  symbol: string;
@@ -29,10 +30,12 @@ export function FundFactsSummary({ symbol, width, facts, loading, error }: {
29
30
  }) {
30
31
  if (facts?.covered) {
31
32
  const stale = fundFactsStaleText(symbol, facts);
33
+ const noNav = fundFactsNavReasonText(symbol, facts);
32
34
  return (
33
35
  <>
34
36
  <StatGrid width={width} items={fundFactItems(facts)} />
35
37
  {stale ? <FundViewNotice lines={[stale]} tone="warning" /> : null}
38
+ {noNav ? <FundViewNotice lines={[noNav]} /> : null}
36
39
  </>
37
40
  );
38
41
  }
@@ -1,5 +1,5 @@
1
1
  import { apiClient } from "../../../api-client";
2
- import type { FundFactsPayload, FundLeverage, FundMetalHoldings } from "../../../api-client/fund-facts";
2
+ import type { FundFactsPayload, FundLeverage, FundMetalHoldings, FundStructure } from "../../../api-client/fund-facts";
3
3
  import type { StatItem } from "../../../components/ui/stat-grid";
4
4
  import { leverageFactorLabel } from "../../../time-series/leverage";
5
5
  import { t, tf } from "../../../i18n";
@@ -7,7 +7,8 @@ import { createPluginCache } from "../../../data/plugin-cache";
7
7
  import type { HeadlessPaneEntry } from "../../../types/plugin";
8
8
  import { errorMessage } from "../../../utils/errors";
9
9
  import { formatCompact } from "../../../utils/format";
10
- import { bondClassText, bondFigures, isBondFactsShape } from "../../../market-data/bond-funds";
10
+ import { bondClassText, bondFigures } from "../../../market-data/bond-funds";
11
+ import { readFundFacts, validateFundFacts } from "../../../market-data/fund-facts";
11
12
  import { cachedCloudResource, loadCloudResource, unavailableOnServer } from "../shared/cloud-resource";
12
13
 
13
14
  /** Sponsors publish once a day, in the US evening; a copy younger than this is not asked for again. */
@@ -17,28 +18,6 @@ export const fundFactsCache = createPluginCache<FundFactsPayload>({
17
18
 
18
19
  type Client = Pick<typeof apiClient, "getFundFacts">;
19
20
 
20
- const fact = (value: unknown) => value === null || (typeof value === "object" && value !== null && "value" in value && "source" in value);
21
- const finite = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value);
22
-
23
- function isMetalHoldingsShape(value: FundFactsPayload["metalHoldings"]): boolean {
24
- return value == null || ((value.metal === "gold" || value.metal === "silver") && typeof value.asOf === "string" && finite(value.ounces)
25
- && (value.tonnes === null || finite(value.tonnes)) && typeof value.source?.publisher === "string"
26
- && (value.change === null || (typeof value.change?.since === "string" && finite(value.change.ounces))));
27
- }
28
-
29
- /** Absent from servers before it; otherwise a dated factor with its reset period. */
30
- const leverageShape = (value: FundFactsPayload["leverage"]) => value === undefined || value === null
31
- || (fact(value) && typeof value.value === "number" && Number.isFinite(value.value) && (value.reset === "daily" || value.reset === "monthly"));
32
-
33
- function validateFundFacts(data: FundFactsPayload): FundFactsPayload {
34
- if (!data || typeof data.ticker !== "string" || typeof data.covered !== "boolean"
35
- || ![data.expenseRatio, data.nav, data.premiumDiscount, data.netAssets, data.sponsor, data.inception, data.index].every(fact)
36
- || !isBondFactsShape(data.bond) || !leverageShape(data.leverage) || !isMetalHoldingsShape(data.metalHoldings)) {
37
- throw new Error("Unreadable fund facts response");
38
- }
39
- return data;
40
- }
41
-
42
21
  export const FUND_FACTS_UNAVAILABLE = "Fund facts are not available on this server yet.";
43
22
 
44
23
  /** A failed load as one line: the server-not-ready message as it is, anything else after "Fund facts unavailable:". */
@@ -54,7 +33,7 @@ export function fundFactsErrorLine(error: unknown): string {
54
33
  }
55
34
 
56
35
  export async function fetchFundFacts(ticker: string, client: Client = apiClient, signal?: AbortSignal): Promise<FundFactsPayload> {
57
- try { return validateFundFacts(await (signal ? client.getFundFacts(ticker, { signal }) : client.getFundFacts(ticker))); } catch (error) { throw unavailableOnServer(error, FUND_FACTS_UNAVAILABLE); }
36
+ try { return await readFundFacts(ticker, client, signal); } catch (error) { throw unavailableOnServer(error, FUND_FACTS_UNAVAILABLE); }
58
37
  }
59
38
  const cacheKey = (ticker: string) => ticker.trim().toUpperCase();
60
39
  export const cachedFundFacts = (ticker: string) => cachedCloudResource(fundFactsCache, cacheKey(ticker), validateFundFacts);
@@ -80,6 +59,40 @@ function formatNetAssets(value: number): string {
80
59
  return `$${formatCompact(value)}`;
81
60
  }
82
61
 
62
+ /** A NAV the sponsor published as a range: `$4.12 to $4.22`. */
63
+ const estimateRange = (estimate: { low: number; high: number }) => `${formatNav(estimate.low)} to ${formatNav(estimate.high)}`;
64
+
65
+ /** Shares created less redeemed, signed: `+12.35M`, `-3.1M`. */
66
+ const signedCompact = (value: number) => `${value > 0 ? "+" : value < 0 ? "-" : ""}${formatCompact(Math.abs(value))}`;
67
+
68
+ /** A month's flows in dollars, when every day had a NAV to value them at. */
69
+ const flowsValue = (value: number) => `${value < 0 ? "-" : value > 0 ? "+" : ""}$${formatCompact(Math.abs(value))}`;
70
+
71
+ /** A closed-end fund's NAV is a month end's (a sponsor's estimate, or the filing's period end): its normal cadence, not a lag. */
72
+ const monthEndNav = (facts: FundFactsPayload) => facts.nav?.period === "month-end";
73
+
74
+ /** The session of the close a premium was computed from, when it is before the NAV's date: a month end on a weekend. */
75
+ const closeSession = (facts: FundFactsPayload) => {
76
+ const close = facts.closingPrice?.asOf;
77
+ const premium = facts.premiumDiscount?.asOf;
78
+ return close && premium && close < premium ? close : null;
79
+ };
80
+
81
+ /** A registered closed-end fund or a BDC: its NAV is a month end's or a filing's, and its price trades at its own premium to it. */
82
+ const closedEnd = (structure: FundFactsPayload["structure"]) => structure === "closed-end" || structure === "bdc";
83
+
84
+ /** Each structure by its own name in a report; one a newer server adds, by its own key rather than another's name. */
85
+ const STRUCTURE_LABELS: Record<FundStructure, string> = {
86
+ registered: "Registered fund",
87
+ trust: "Trust or pool, not an open-end fund",
88
+ "closed-end": "Closed-end fund",
89
+ bdc: "Business development company (BDC)",
90
+ };
91
+ const structureLabel = (structure: FundStructure) => STRUCTURE_LABELS[structure] ?? String(structure);
92
+
93
+ /** The pane's shorter names, for the funds whose figures read unlike an ETF's. */
94
+ const PANE_STRUCTURE: Partial<Record<FundStructure, string>> = { "closed-end": "Closed-end fund", bdc: "BDC" };
95
+
83
96
  /** Ounces and tonnes as the sponsor publishes them, to its own decimals (34,085,936.5; 1,060.18). */
84
97
  const metalFigure = (value: number, signed = false) =>
85
98
  `${signed && value > 0 ? "+" : ""}${value.toLocaleString("en-US", { maximumFractionDigits: 3 })}`;
@@ -156,9 +169,19 @@ function datedFigures(facts: FundFactsPayload): Record<"nav" | "premium" | "fee"
156
169
  const prospectus = facts.expenseRatio?.document === "prospectus";
157
170
  const nport = assets?.document === "N-PORT";
158
171
  return {
159
- nav: { detail: nav ? (facts.stale ? tf("{date} stale", { date: nav }) : nav) : undefined, marked: facts.stale },
172
+ nav: {
173
+ detail: nav
174
+ ? facts.stale ? tf("{date} stale", { date: nav }) : monthEndNav(facts) ? tf("{date} month-end", { date: nav }) : nav
175
+ : undefined,
176
+ marked: facts.stale,
177
+ },
160
178
  premium: {
161
- detail: premium?.asOf ? (premium.basis === "computed" ? tf("{date} close", { date: premium.asOf }) : premium.asOf) : undefined,
179
+ // A computed premium is dated by its close's session (a weekend month end's Friday), and says when it is an estimate's.
180
+ detail: premium?.asOf
181
+ ? premium.basis === "computed"
182
+ ? `${tf("{date} close", { date: closeSession(facts) ?? premium.asOf })}${premium.estimate ? ` · ${t("est.")}` : ""}`
183
+ : premium.asOf
184
+ : undefined,
162
185
  marked: facts.stale || (!!premium?.asOf && !!nav && premium.asOf !== nav),
163
186
  },
164
187
  fee: {
@@ -192,10 +215,29 @@ export function fundFactItems(facts: FundFactsPayload): StatItem[] {
192
215
  const cell = (id: string, label: string, value: string, figure: DatedFigure): StatItem => ({
193
216
  id, label, value, ...(figure.detail ? { detail: figure.detail } : {}), ...(figure.marked ? { tone: "warning" as const } : {}),
194
217
  });
195
- if (facts.nav) items.push(cell("nav", "NAV", formatNav(facts.nav.value), dated.nav));
218
+ if (facts.nav) {
219
+ // A sponsor's estimate is a range: its midpoint is the figure, the range follows its date.
220
+ const estimate = facts.nav.estimate;
221
+ const nav = estimate && dated.nav.detail
222
+ ? { ...dated.nav, detail: `${dated.nav.detail} · ${tf("est. {range}", { range: estimateRange(estimate) })}` } : dated.nav;
223
+ items.push(cell("nav", "NAV", formatNav(facts.nav.value), nav));
224
+ }
196
225
  if (facts.premiumDiscount) items.push(cell("premium", "Premium/discount", formatPremiumPercent(facts.premiumDiscount.value), dated.premium));
197
226
  if (facts.expenseRatio) items.push(cell("fee", expenseRatioLabel(facts), formatFeePercent(facts.expenseRatio.value), dated.fee));
198
227
  if (facts.netAssets) items.push(cell("assets", wholeFund(facts) ? "Fund net assets" : "Net assets", formatNetAssets(facts.netAssets.value), dated.netAssets));
228
+ // Shares outstanding as the sponsor states them, then the month's creations less redemptions.
229
+ if (facts.sharesOutstanding) {
230
+ items.push(cell("shares", "Shares outstanding", formatCompact(facts.sharesOutstanding.value),
231
+ { detail: facts.sharesOutstanding.asOf ?? undefined, marked: facts.stale }));
232
+ }
233
+ if (facts.flows) {
234
+ // In dollars with the shares beside them; in shares alone when a day had no NAV to value them at.
235
+ const flows = facts.flows;
236
+ const shares = tf("{count} shares", { count: signedCompact(flows.shares) });
237
+ const since = tf("since {date}", { date: flows.from });
238
+ items.push(cell("flows", "1M flows", flows.value === null ? shares : flowsValue(flows.value),
239
+ { detail: flows.value === null ? since : `${shares} · ${since}`, marked: facts.stale }));
240
+ }
199
241
  // A bond fund's yields and duration, each dated by its sponsor.
200
242
  for (const figure of facts.bond ? bondFigures(facts.bond) : []) {
201
243
  items.push(cell(figure.key, figure.label, figure.text, { detail: figure.date, marked: facts.stale }));
@@ -212,6 +254,9 @@ export function fundFactItems(facts: FundFactsPayload): StatItem[] {
212
254
  { detail: metal.change.tonnes == null ? tf("since {date}", { date: metal.change.since }) : `${change} · ${tf("since {date}", { date: metal.change.since })}`, marked: metal.stale }));
213
255
  }
214
256
  }
257
+ // A closed-end fund or BDC says which: its figures read unlike an ETF's.
258
+ const structure = facts.structure ? PANE_STRUCTURE[facts.structure] : undefined;
259
+ if (structure) items.push({ id: "structure", label: "Structure", value: t(structure) });
215
260
  if (facts.sponsor) items.push({ id: "sponsor", label: "Sponsor", value: facts.sponsor.value });
216
261
  if (facts.inception) items.push({ id: "inception", label: "Inception", value: facts.inception.value });
217
262
  // A leveraged or inverse fund's objective, dated by the sponsor page it was read from, beside its benchmark.
@@ -276,19 +321,23 @@ export function fundFactEntries(facts: FundFactsPayload): HeadlessPaneEntry[] {
276
321
  const add = (key: string, label: string, value: unknown, formatted: string, unit?: string) =>
277
322
  entries.push({ key, label, value, formatted, ...(unit ? { unit } : {}) });
278
323
  if (facts.name) add("name", "Name", facts.name, facts.name);
279
- if (facts.structure) add("structure", "Structure", facts.structure,
280
- facts.structure === "trust" ? "Trust or pool, not an open-end fund" : "Registered fund");
324
+ if (facts.structure) add("structure", "Structure", facts.structure, structureLabel(facts.structure));
281
325
  if (facts.expenseRatio) {
282
326
  const kind = feeBasis(facts);
283
327
  const date = facts.expenseRatio.asOf && facts.expenseRatio.document === "prospectus" ? `${facts.expenseRatio.asOf} prospectus` : facts.expenseRatio.asOf;
284
328
  add("expenseRatioPercent", expenseRatioLabel(facts), facts.expenseRatio.value,
285
329
  provenance(`${formatFeePercent(facts.expenseRatio.value)}${kind ? ` ${kind}` : ""}`, date, facts.expenseRatio.source), "%");
286
330
  }
287
- if (facts.nav) add("nav", "NAV", facts.nav.value, provenance(formatNav(facts.nav.value), facts.nav.asOf, facts.nav.source), "USD");
331
+ if (facts.nav) {
332
+ const date = facts.nav.asOf && monthEndNav(facts) ? `${facts.nav.asOf} month-end` : facts.nav.asOf;
333
+ const estimate = facts.nav.estimate ? `estimate ${estimateRange(facts.nav.estimate)}` : null;
334
+ add("nav", "NAV", facts.nav.value, provenance(formatNav(facts.nav.value), date, estimate, facts.nav.source), "USD");
335
+ }
288
336
  if (facts.closingPrice) add("closingPrice", "Closing price", facts.closingPrice.value,
289
337
  provenance(formatNav(facts.closingPrice.value), facts.closingPrice.asOf, facts.closingPrice.source), "USD");
290
338
  if (facts.premiumDiscount) {
291
- const basis = facts.premiumDiscount.basis === "computed" ? "closing price vs NAV" : null;
339
+ const basis = facts.premiumDiscount.basis !== "computed" ? null
340
+ : facts.premiumDiscount.estimate ? "closing price vs NAV estimate midpoint" : "closing price vs NAV";
292
341
  add("premiumDiscountPercent", "Premium/discount", facts.premiumDiscount.value,
293
342
  provenance(formatPremiumPercent(facts.premiumDiscount.value), facts.premiumDiscount.asOf, basis, facts.premiumDiscount.source), "%");
294
343
  }
@@ -298,6 +347,16 @@ export function fundFactEntries(facts: FundFactsPayload): HeadlessPaneEntry[] {
298
347
  ? `${facts.netAssets.asOf} month-end${facts.netAssets.document ? ` ${facts.netAssets.document}` : ""}` : facts.netAssets.asOf;
299
348
  add("netAssets", "Net assets", facts.netAssets.value, provenance(formatNetAssets(facts.netAssets.value), date, scope, facts.netAssets.source), "USD");
300
349
  }
350
+ if (facts.sharesOutstanding) {
351
+ add("sharesOutstanding", "Shares outstanding", facts.sharesOutstanding.value,
352
+ provenance(facts.sharesOutstanding.value.toLocaleString("en-US"), facts.sharesOutstanding.asOf, facts.sharesOutstanding.source), "shares");
353
+ }
354
+ if (facts.flows) {
355
+ // JSON keeps the dollar figure (null when a day had no NAV); `fundFacts.flows` has the shares.
356
+ const flows = facts.flows;
357
+ add("flows1M", "1M flows", flows.value, provenance(flows.value === null ? null : flowsValue(flows.value),
358
+ `${signedCompact(flows.shares)} shares`, `${flows.from} to ${flows.asOf}`, flows.source), "USD");
359
+ }
301
360
  const metal = facts.metalHoldings;
302
361
  if (metal) {
303
362
  // The trust's own figures, as its sponsor's daily holdings file states them; JSON has the tonnes in `fundFacts`.
@@ -323,7 +382,8 @@ export function fundFactEntries(facts: FundFactsPayload): HeadlessPaneEntry[] {
323
382
  }
324
383
  if (facts.index) add("index", BENCHMARK_LABEL, facts.index.value, provenance(facts.index.value, facts.index.source));
325
384
  const url = facts.nav?.url ?? facts.expenseRatio?.url ?? facts.netAssets?.url;
326
- if (url) add("url", "Sponsor page", url, url);
385
+ // The figures' own page: the sponsor's, or the SEC filing they were read from (a prospectus, an N-PORT, an 8-K).
386
+ if (url) add("url", /^https:\/\/www\.sec\.gov\//.test(url) ? "SEC filing" : "Sponsor page", url, url);
327
387
  return entries;
328
388
  }
329
389
 
@@ -338,7 +398,7 @@ export function listNames(names: readonly string[]): string {
338
398
  * the consolidated close cites Gloom Cloud on its own line, not here.
339
399
  */
340
400
  export function fundFactPublishers(facts: FundFactsPayload): string[] {
341
- return [...new Set([...[facts.nav, facts.expenseRatio, facts.netAssets, facts.leverage, facts.index, facts.bond?.secYield, facts.bond?.yieldToWorst, facts.bond?.duration]
401
+ return [...new Set([...[facts.nav, facts.expenseRatio, facts.netAssets, facts.sharesOutstanding, facts.flows, facts.leverage, facts.index, facts.bond?.secYield, facts.bond?.yieldToWorst, facts.bond?.duration]
342
402
  .flatMap((value) => value ? [value.source] : []), ...facts.metalHoldings ? [facts.metalHoldings.source.publisher] : []])];
343
403
  }
344
404
 
@@ -352,6 +412,25 @@ export function fundFactsStaleText(symbol: string, facts: FundFactsPayload): str
352
412
  return fundFactsStaleNote(symbol, facts) ? tf("{symbol}'s newest NAV is from {date}; its sponsor has not published one since.", { symbol, date: facts.nav!.asOf! }) : null;
353
413
  }
354
414
 
415
+ /**
416
+ * Why a covered fund has no NAV, in one line, translated for the pane: what
417
+ * Gloom reads for it and what that leaves out, as the server's `navReason`
418
+ * says it. Null when it has a NAV, and from servers before the reason.
419
+ */
420
+ export function fundFactsNavReasonText(symbol: string, facts: FundFactsPayload): string | null {
421
+ if (!facts.covered || facts.nav || !facts.navReason) return null;
422
+ // A sponsor that publishes the fund's other figures.
423
+ const sponsor = fundFactPublishers(facts).find((publisher) => publisher !== "SEC EDGAR");
424
+ if (sponsor) return tf("No NAV for {symbol}: {publisher} publishes figures for it, but no NAV.", { symbol, publisher: sponsor });
425
+ if (closedEnd(facts.structure)) {
426
+ return tf("No NAV for {symbol}: its SEC filings state net assets but no NAV per share, and its sponsor's NAV is not one Gloom reads.", { symbol });
427
+ }
428
+ if (facts.structure === "trust") {
429
+ return tf("No NAV for {symbol}: its sponsor's site is not one Gloom reads, and a trust files no NAV with the SEC's fund data sets.", { symbol });
430
+ }
431
+ return tf("No NAV for {symbol}: its sponsor's site is not one Gloom reads, and the SEC's prospectus and N-PORT data sets state no NAV.", { symbol });
432
+ }
433
+
355
434
  /**
356
435
  * Why a fund has no facts, in one line, and where to look instead: its SEC
357
436
  * filings, unless the caller already points there.
@@ -376,6 +455,9 @@ export function fundFactsEmptyText(symbol: string, facts: FundFactsPayload | nul
376
455
  if (facts.structure === "registered") {
377
456
  return tf("No figures for {symbol}: its sponsor's site is not one Gloom reads, and the SEC's prospectus and N-PORT data sets list none for it yet.", { symbol });
378
457
  }
458
+ if (closedEnd(facts.structure)) {
459
+ return tf("No figures for {symbol}: it files with the SEC as a closed-end fund or BDC, but neither the filings Gloom reads nor its sponsor's site state any for it yet.", { symbol });
460
+ }
379
461
  return tf("No figures for {symbol}: it lists as a fund, but the SEC's lists do not name it yet and its sponsor's site is not one Gloom reads.", { symbol });
380
462
  }
381
463
  if (facts.status === "not-a-fund") return tf("No fund facts for {symbol}: it is not on the SEC's lists of US-listed funds.", { symbol });
@@ -20,6 +20,7 @@ import { fetchFundFacts, fundFactEntries, fundFactPublishers, fundFactsEmpty, fu
20
20
  import { REPORT_ROWS } from "../shared/headless-options";
21
21
  import { fundAbroadProfile, venueAbroad, type FundAbroadProfile } from "./fund-abroad";
22
22
  import { listingAnnouncementsPage } from "../../../utils/exchange-announcements";
23
+ import { companyDataNotes } from "../../../types/company-from";
23
24
 
24
25
  const SEC_COLUMNS: HeadlessPaneColumn[] = [
25
26
  {
@@ -140,7 +141,7 @@ export function createSecHeadless(dependencies: SecHeadlessDependencies = defaul
140
141
  return {
141
142
  rows,
142
143
  textColumns: secTextColumns(rows),
143
- ...(note ? { notes: [note] } : {}),
144
+ ...(note || filings[0]?.companyFrom ? { notes: [...(note ? [note] : []), ...companyDataNotes(filings[0]?.companyFrom)] } : {}),
144
145
  ...(form ? { empty: `No ${form} filings found for ${symbol}.` }
145
146
  : page ? { empty: `${symbol} files no SEC reports. ${page.name}: ${page.url}` } : {}),
146
147
  metadata: {
@@ -268,7 +269,8 @@ export function createFundHeadless(dependencies: FundHeadlessDependencies = defa
268
269
  ...(factsResult.status === "rejected" ? [fundFactsErrorText(factsResult.reason)] : []),
269
270
  ...(filingsResult.status === "rejected" ? [`Fund filings: ${errorMessage(filingsResult.reason)}`] : []),
270
271
  ],
271
- notes: facts ? [fundFactsStaleNote(symbol, facts)].filter((note): note is string => !!note) : [],
272
+ // A stale NAV says since when; a covered fund without one says why, as the server words it.
273
+ notes: facts ? [fundFactsStaleNote(symbol, facts), facts.covered && !facts.nav ? facts.navReason : null].filter((note): note is string => !!note) : [],
272
274
  // The facts' publisher is the source, named the same way for every fund; without facts it is the SEC.
273
275
  freshness: {
274
276
  source: listNames(publishers) || "SEC EDGAR",
@@ -12,6 +12,7 @@ import { Box, Text, useUiCapabilities, type ScrollBoxRenderable } from "../../..
12
12
  import { FILL_REST } from "../../../components/ui/fill";
13
13
  import { t, tf } from "../../../i18n";
14
14
  import { EmptyState, ExternalLinkText, FeedDataTableStackView, PaneLinkMenu, QueryBar, Spinner, TickerBadgeList, usePaneNoticeFooter, useTableLoadMore, type FeedDataTableItem, type PaneHint, type QueryBarFilter } from "../../../components";
15
+ import { useCompanyDataFooter } from "../../../components/layout/pane/status-footer";
15
16
  import { displayWidth } from "../../../utils/format";
16
17
  import type { DetailFact } from "../../../components/reader/detail-reader";
17
18
  import type { ThemeColors } from "../../../theme/colors";
@@ -466,6 +467,8 @@ function SecView({ width, height, focused, paneType }: { width: number; height:
466
467
  showOpenHint: !!(openFiling ? openFiling.filingUrl : factsUrl ?? abroadPage ?? announcements),
467
468
  });
468
469
 
470
+ // A foreign line's filings are its company's, read from the home line.
471
+ useCompanyDataFooter(issuerFilings[0]?.companyFrom, !openFiling);
469
472
  usePaneNoticeFooter({
470
473
  registrationId: "sec:form-filter",
471
474
  notices: filterHistoryCapped
@@ -64,9 +64,11 @@ export const SERIES_INVENTORY: readonly SeriesInventoryEntry[] = [
64
64
  { module: "perps", chart: "Perpetual funding, open interest, premium and long/short account share", series: ["perps:*"] },
65
65
  { 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
66
  { 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." },
67
+ { module: "treasury-daily", chart: "The Treasury General Account's daily closing balance", notInCatalog: "not-yet", reason: "Daily and dated, from the Daily Treasury Statement's operating cash balance table; the pane reads it with the day's cash lines, and none is registered for the composer yet." },
67
68
  { module: "primary-dealers", chart: "A primary dealer position's weekly net level, the sum of the release's series behind the row", notInCatalog: "not-yet", reason: "Weekly and dated, one series per row of the board; the pane reads each only when its row is selected, and none is registered for the composer yet." },
68
69
  { module: "short-interest", chart: "A ticker's short interest and days to cover", series: ["short-interest:*"] },
69
70
  { module: "short-volume", chart: "A ticker's daily short volume ratio", series: ["short-volume:*"] },
71
+ { module: "scanner", chart: "Net options premium through the session, for the market or one ticker (FLOW's Tide)", notInCatalog: "per-item", reason: "One per ticker, filter set and session, summed on request from the recorded option prints." },
70
72
  { module: "social-mentions", chart: "A ticker's daily posts", series: ["social-mentions:*"] },
71
73
  { module: "jobs", chart: "A company's open job postings", series: ["jobs:*"] },
72
74
  { module: "attention", chart: "Research hours on a ticker, hour by hour", series: ["attention:*"] },
@@ -1,4 +1,5 @@
1
- import { ApiRequestError, isAccessDenied } from "../../../api-client/errors";
1
+ import { logFailure } from "../../../utils/log-failure";
2
+ import { ApiRequestError, isPermanentClientError } from "../../../api-client/errors";
2
3
  import type { PluginCacheResult } from "../../../data/plugin-cache";
3
4
 
4
5
  interface CloudCache<T> {
@@ -11,6 +12,8 @@ export interface CloudResource<T> {
11
12
  payload: T;
12
13
  stale: boolean;
13
14
  refreshError: string | null;
15
+ /** When the copy shown was saved, when it is one because its refresh failed. */
16
+ savedAt?: number;
14
17
  }
15
18
 
16
19
  /**
@@ -26,8 +29,8 @@ export function cachedCloudResource<T>(
26
29
  if (!cached) return null;
27
30
  try {
28
31
  return { payload: validate(cached.data), stale: false, refreshError: null };
29
- } catch {
30
- return null;
32
+ } catch (error) {
33
+ return logFailure("shared.cloud-resource", "Validate cached cloud payload failed", null)(error);
31
34
  }
32
35
  }
33
36
 
@@ -35,8 +38,9 @@ const isAbort = (error: unknown) => !!error && typeof error === "object" && (err
35
38
 
36
39
  /**
37
40
  * Loads through the cache, falling back to the cached copy when a refresh
38
- * fails. A refused session still throws, so the pane shows its sign-in wall
39
- * instead of data the account can no longer see.
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).
40
44
  *
41
45
  * Callers of one key share the request in flight, which runs on the first
42
46
  * caller's `signal`. When that caller goes away (a tab left while it loads,
@@ -56,8 +60,13 @@ export async function loadCloudResource<T>(
56
60
  throw error;
57
61
  });
58
62
  if (!result || cancelledByOther(result.error)) result = await cache.load(key, fetch, { force });
59
- if (isAccessDenied(result.error)) throw result.error;
60
- return { payload: validate(result.data), stale: result.stale, refreshError: result.refreshError ?? null };
63
+ const saved = result.source === "stale-fallback";
64
+ if (saved && isPermanentClientError(result.error)) throw result.error;
65
+ // A copy served because its refresh failed is one by where it came from, whatever the failure said (or did not).
66
+ return {
67
+ payload: validate(result.data), stale: result.stale, refreshError: result.refreshError ?? null,
68
+ ...(saved ? { savedAt: result.fetchedAt } : {}),
69
+ };
61
70
  }
62
71
 
63
72
  /**
@@ -1,3 +1,4 @@
1
+ import { logFailure } from "../../../utils/log-failure";
1
2
  import { useCallback, useEffect, useMemo, useRef } from "react";
2
3
  import { apiClient } from "../../../api-client";
3
4
  import { recordResearchActivity, researchUpgradeUrl } from "../../../api-client/research-activity";
@@ -159,7 +160,7 @@ async function openCheckoutWithHandoff(
159
160
  .then(({ url, link }): CheckoutHandoffTarget => ({
160
161
  url,
161
162
  link: link ?? null,
162
- opened: rendererHost.openExternal(url).then((result) => result !== false, () => false),
163
+ opened: rendererHost.openExternal(url).then((result) => result !== false, logFailure("shared.cloud-upgrade", "Open checkout in the browser failed", false)),
163
164
  }));
164
165
  void promptCheckoutHandoff(dialog, target);
165
166
  await (await target).opened;
@@ -221,10 +222,10 @@ export function useCloudUpgradeAction(placement: string): (options?: unknown) =>
221
222
  openCloudUpgradePage("cloud", async () => {
222
223
  const url = await apiClient.createBrowserHandoff()
223
224
  .then((handoff) => handoff.url)
224
- .catch(() => CLOUD_UPGRADE_URL);
225
+ .catch(logFailure("shared.cloud-upgrade", "Create Cloud page handoff failed", CLOUD_UPGRADE_URL));
225
226
  await rendererHost.openExternal(url);
226
227
  })
227
- )).catch(() => {});
228
+ )).catch(logFailure("shared.cloud-upgrade", "Open Cloud upgrade page failed"));
228
229
  };
229
230
  if (
230
231
  checkout.sheet === false ||
@@ -55,7 +55,7 @@ function calendarDateWins(calendar: EarningsDateStatus | null, other: EarningsDa
55
55
  }
56
56
 
57
57
  /** The calendar's report as a quarter on record takes it. */
58
- interface CalendarReport {
58
+ export interface CalendarReport {
59
59
  date: string;
60
60
  timing: "bmo" | "amc" | null;
61
61
  dateStatus: EarningsDateStatus | null;
@@ -86,7 +86,11 @@ function consensusIsReports(event: EarningsEvent, dateStatus: EarningsDateStatus
86
86
  return periodEnd != null && periodEnd < date;
87
87
  }
88
88
 
89
- function calendarReport(event: EarningsEvent, today: string): CalendarReport {
89
+ /**
90
+ * A calendar event read as its report, as every surface reads it: reported
91
+ * once out, and with a consensus only when it is that report's.
92
+ */
93
+ export function calendarReport(event: EarningsEvent, today: string): CalendarReport {
90
94
  const date = calendarDay(event);
91
95
  const dateStatus = calendarStatus(event, today);
92
96
  const ownConsensus = consensusIsReports(event, dateStatus, date);
@@ -54,8 +54,8 @@ export function epsBasisShort(basis: EarningsEpsBasis | null): string {
54
54
  return basis === "gaap" ? "GAAP" : basis === "adjusted" ? "adj" : "?";
55
55
  }
56
56
 
57
- /** Known bases are named in row details; unknown stays in the structured data and drilldown. */
58
- const BASIS_TEXT: Record<EpsBasisName, string> = { GAAP: "GAAP", adjusted: "adj", unknown: "" };
57
+ /** A reported row's detail names its basis as ERN's Basis column does, unknown included. */
58
+ const BASIS_TEXT: Record<EpsBasisName, string> = { GAAP: "GAAP", adjusted: "adj", unknown: "unknown" };
59
59
 
60
60
  type EarningsRecord = CorporateActionsData["earnings"][number];
61
61
 
@@ -6,7 +6,7 @@ import { clipPriceHistoryToRange } from "../../../time-series/history-window";
6
6
  import { canonicalExchange, parsePublicTickerKey } from "../../../utils/exchanges";
7
7
  import { nonUsSecListingVenue } from "../../../utils/sec";
8
8
 
9
- export async function resolveHeadlessInstrument(ctx: HeadlessPaneContext, symbol: string) {
9
+ export async function resolveHeadlessInstrument(ctx: Pick<HeadlessPaneContext, "resolveInstrument">, symbol: string) {
10
10
  return ctx.resolveInstrument ? ctx.resolveInstrument(symbol) : parsePublicTickerKey(symbol);
11
11
  }
12
12
 
@@ -39,7 +39,7 @@ export async function resolveHeadlessIssuerListing(ctx: HeadlessPaneContext, key
39
39
  return resolveIssuerListing(key, exchange, ctx.marketData);
40
40
  }
41
41
 
42
- export async function loadHeadlessFinancials(ctx: HeadlessPaneContext, key: string) {
42
+ export async function loadHeadlessFinancials(ctx: Pick<HeadlessPaneContext, "marketData" | "resolveInstrument">, key: string) {
43
43
  const { symbol, exchange } = await resolveHeadlessInstrument(ctx, key);
44
44
  return ctx.marketData.getTickerFinancials(symbol, exchange ?? "");
45
45
  }
@@ -34,6 +34,7 @@ export function countryName(code: string | null | undefined): string | null {
34
34
  try {
35
35
  return countryNames.of(code) ?? null;
36
36
  } catch {
37
+ // Intentional ignore: a region code Intl cannot name is left out of the posting's place.
37
38
  return null;
38
39
  }
39
40
  }
@@ -1,3 +1,4 @@
1
+ import { logFailure } from "../../../utils/log-failure";
1
2
  import { ChoiceDialog, TextPromptDialog, usePaneMenuItems } from "../../../components";
2
3
  import { createFormCollectionActions } from "../../../components/form-modal/deps";
3
4
  import { t, tf } from "../../../i18n";
@@ -195,7 +196,7 @@ export function useMembersMenu(registrationId: string, list: MemberList | null)
195
196
  const choice = choices.find((item) => item.id === templateId && !item.disabled);
196
197
  // CORR and RIPL resolve `arg` like the command bar; RRG and SIW read `symbols`.
197
198
  if (choice) createPaneFromTemplate(choice.id, { symbols: choice.symbols, arg: formatTickerListInput(choice.symbols) });
198
- }).catch(() => {});
199
+ }).catch(logFailure("shared.members-menu", "Open members in a function failed"));
199
200
  };
200
201
  const saveAsWatchlist = async (requested: string) => {
201
202
  // The writes hold the app for about a second with a hundred names; let the dialog close on screen first.
@@ -253,7 +254,7 @@ export function useMembersMenu(registrationId: string, list: MemberList | null)
253
254
  saveAsWatchlist(name).catch((error: unknown) => {
254
255
  notify({ type: "error", body: error instanceof Error ? error.message : t("The watchlist was not saved.") });
255
256
  });
256
- }).catch(() => {});
257
+ }).catch(logFailure("shared.members-menu", "Open save members as watchlist prompt failed"));
257
258
  } },
258
259
  ];
259
260
  }, [createPaneFromTemplate, dialog, disabledPlugins, dispatch, getState, list, notify]);
@@ -1,3 +1,4 @@
1
+ import { createTtlCache, type TtlCache } from "../../../data/ttl-cache";
1
2
  import type { HeadlessPaneContext } from "../../../types/headless";
2
3
  import type { PaneLock } from "../../../types/plugin";
3
4
  import { settleWithin, signalWithTimeout } from "../../../utils/async-deadline";
@@ -35,8 +36,7 @@ const ANSWER_TTL_MS = 5 * 60_000;
35
36
  */
36
37
  const ROUTE_CHECK_WAIT_MS = 3_000;
37
38
 
38
- interface KeptAnswer { at: number; answer: Promise<boolean> }
39
- const answers = new WeakMap<FreeRoute, Map<string, KeptAnswer>>();
39
+ const answers = new WeakMap<FreeRoute, TtlCache<boolean>>();
40
40
 
41
41
  /**
42
42
  * Whether the route has data for the symbol, null when its check failed or was
@@ -45,20 +45,20 @@ const answers = new WeakMap<FreeRoute, Map<string, KeptAnswer>>();
45
45
  * rather than hiding the route for minutes over one slow read.
46
46
  */
47
47
  async function routeAnswers(route: FreeRoute, check: NonNullable<FreeRoute["answers"]>, symbol: string, context: HeadlessPaneContext, deadline: number): Promise<boolean | null> {
48
- const bySymbol = answers.get(route) ?? new Map<string, KeptAnswer>();
48
+ const bySymbol = answers.get(route) ?? createTtlCache<boolean>({ ttlMs: ANSWER_TTL_MS });
49
49
  answers.set(route, bySymbol);
50
50
  const key = symbol.trim().toUpperCase();
51
- let kept = bySymbol.get(key);
52
- if (!kept || Date.now() - kept.at >= ANSWER_TTL_MS) {
51
+ const kept = bySymbol.load(key, () => {
53
52
  // The check's own reads stop at the deadline where they take a signal.
54
53
  const signal = signalWithTimeout(context.signal, Math.max(0, deadline - Date.now()));
55
- const entry: KeptAnswer = { at: Date.now(), answer: check(symbol, { ...context, signal }) };
56
- entry.answer.catch(logFailure("plugins.pro-lock-note", "Check a free route's data failed"));
57
- bySymbol.set(key, entry);
58
- kept = entry;
59
- }
60
- const answer = await settleWithin(kept.answer, Math.max(0, deadline - Date.now()));
61
- if (answer === null && bySymbol.get(key) === kept) bySymbol.delete(key);
54
+ return check(symbol, { ...context, signal }).catch((error: unknown) => {
55
+ logFailure("plugins.pro-lock-note", "Check a free route's data failed")(error);
56
+ throw error;
57
+ });
58
+ });
59
+ const answer = await settleWithin(kept, Math.max(0, deadline - Date.now()));
60
+ // Only this check is dropped: a newer one for the symbol stays.
61
+ if (answer === null) bySymbol.delete(key, kept);
62
62
  return answer;
63
63
  }
64
64