gloomberb 0.19.0 → 0.19.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/package.json +2 -1
  4. package/patches/electrobun@1.18.1.patch +23 -1
  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/data.ts +2 -1
  9. package/src/api-client/errors.ts +38 -0
  10. package/src/api-client/hiring.ts +3 -0
  11. package/src/api-client/index.ts +4 -0
  12. package/src/api-client/ipo.ts +24 -1
  13. package/src/api-client/mna.ts +3 -0
  14. package/src/api-client/paths.ts +9 -1
  15. package/src/api-client/roadmap.ts +85 -0
  16. package/src/api-client/types.ts +12 -0
  17. package/src/app/runtime/startup.ts +3 -1
  18. package/src/architecture/core-builtin-allowlist.json +2 -2
  19. package/src/capabilities/chart-series.ts +2 -1
  20. package/src/cli/commands/market.ts +44 -16
  21. package/src/cli/commands/news-search.ts +22 -0
  22. package/src/cli/commands/overview.ts +84 -19
  23. package/src/cli/commands/search.ts +13 -13
  24. package/src/cli/commands/ticker.ts +32 -2
  25. package/src/cli/context.ts +1 -0
  26. package/src/cli/errors.ts +17 -2
  27. package/src/cli/glossary.ts +8 -1
  28. package/src/cli/history-rows.ts +35 -7
  29. package/src/cli/index-roots.ts +0 -7
  30. package/src/cli/known-gaps.ts +11 -0
  31. package/src/cli/listing-arg.ts +74 -11
  32. package/src/cli/pane-functions/capabilities.ts +1 -0
  33. package/src/cli/pane-functions/discovery.ts +21 -1
  34. package/src/cli/pane-functions/freshness.ts +7 -1
  35. package/src/cli/pane-functions/function-help.ts +7 -0
  36. package/src/cli/pane-functions/index.ts +6 -3
  37. package/src/cli/quote-answer.ts +30 -7
  38. package/src/cli/saved-copy-persistence.ts +88 -0
  39. package/src/components/command-bar/list/model.ts +5 -0
  40. package/src/components/command-bar/routes/root/runtime.ts +29 -8
  41. package/src/components/command-bar/selection-runtime.ts +64 -32
  42. package/src/components/command-bar/surface/index.tsx +2 -0
  43. package/src/components/layout/detached-pane-shell.tsx +2 -2
  44. package/src/components/layout/header.tsx +2 -2
  45. package/src/components/layout/pane/capture-stamp.ts +2 -2
  46. package/src/components/layout/pane/footer/index.tsx +23 -6
  47. package/src/components/layout/pane/footer/model.ts +116 -16
  48. package/src/components/layout/pane/header.tsx +2 -2
  49. package/src/components/layout/pane/status-footer.ts +37 -3
  50. package/src/components/layout/status-widget-space.ts +9 -0
  51. package/src/components/onboarding/onboarding-frame.tsx +2 -1
  52. package/src/components/onboarding/onboarding-wizard.tsx +6 -3
  53. package/src/components/ticker/list-table-view.tsx +4 -20
  54. package/src/components/ui/display.tsx +29 -3
  55. package/src/components/ui/icon.tsx +12 -3
  56. package/src/core/app-services.ts +11 -2
  57. package/src/data/app-persistence.ts +9 -7
  58. package/src/data/plugin-cache.ts +20 -3
  59. package/src/data/plugin-state-store.ts +81 -24
  60. package/src/data/resource-store.ts +130 -58
  61. package/src/data/saved-copies.ts +8 -0
  62. package/src/data/session-store.ts +31 -8
  63. package/src/data/sqlite/access.ts +313 -0
  64. package/src/data/sqlite/checkpointer.ts +198 -0
  65. package/src/data/sqlite/database.ts +47 -4
  66. package/src/data/sqlite/retry.ts +50 -9
  67. package/src/data/ticker-store.ts +57 -23
  68. package/src/docs/snapshot.json +32 -5
  69. package/src/i18n/es.ts +2 -4
  70. package/src/i18n/ja.ts +2 -4
  71. package/src/i18n/ko.ts +2 -4
  72. package/src/i18n/zh-cn.ts +2 -4
  73. package/src/i18n/zh-tw.ts +2 -4
  74. package/src/market-data/depositary-pair.ts +25 -1
  75. package/src/market-data/quotes/metadata.ts +7 -0
  76. package/src/market-data/realized-volatility.ts +4 -2
  77. package/src/news/aggregator.ts +7 -1
  78. package/src/news/types.ts +6 -0
  79. package/src/plugins/auto-update-node.ts +10 -6
  80. package/src/plugins/builtin/alerts/calendar-events.ts +10 -4
  81. package/src/plugins/builtin/alerts/cli.ts +34 -11
  82. package/src/plugins/builtin/alerts/event-rows.ts +24 -3
  83. package/src/plugins/builtin/alerts/events-pane.tsx +15 -6
  84. package/src/plugins/builtin/alerts/headless.ts +10 -6
  85. package/src/plugins/builtin/cds/client.ts +142 -30
  86. package/src/plugins/builtin/cds/headless.ts +35 -8
  87. package/src/plugins/builtin/cds/model.ts +145 -18
  88. package/src/plugins/builtin/cds/pane.tsx +16 -5
  89. package/src/plugins/builtin/central-bank-rates/series.ts +2 -3
  90. package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
  91. package/src/plugins/builtin/chart-composer/follow-binding.ts +14 -2
  92. package/src/plugins/builtin/chart-composer/headless-schema.ts +5 -0
  93. package/src/plugins/builtin/chart-composer/index.tsx +4 -5
  94. package/src/plugins/builtin/chart-composer/pane.tsx +5 -1
  95. package/src/plugins/builtin/chart-composer/session-range.ts +5 -2
  96. package/src/plugins/builtin/chat/controller/index.ts +5 -1
  97. package/src/plugins/builtin/chat/controller/notifications.ts +33 -3
  98. package/src/plugins/builtin/chat/controller/state.ts +25 -2
  99. package/src/plugins/builtin/chat/controller/unread-people.ts +89 -23
  100. package/src/plugins/builtin/chat/controller/utils.ts +7 -2
  101. package/src/plugins/builtin/chat/status-preview.ts +79 -0
  102. package/src/plugins/builtin/chat/status-widget.tsx +147 -10
  103. package/src/plugins/builtin/chat/unread-badge.ts +51 -10
  104. package/src/plugins/builtin/clinical-trials/client.ts +49 -4
  105. package/src/plugins/builtin/clinical-trials/model.ts +86 -22
  106. package/src/plugins/builtin/clinical-trials/pane.tsx +46 -27
  107. package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
  108. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +87 -9
  109. package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
  110. package/src/plugins/builtin/commodity-balances/client.ts +4 -1
  111. package/src/plugins/builtin/company-attention/headless.ts +2 -1
  112. package/src/plugins/builtin/company-attention/pane.tsx +3 -0
  113. package/src/plugins/builtin/company-kpis/headless.ts +8 -3
  114. package/src/plugins/builtin/company-kpis/model.ts +73 -5
  115. package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
  116. package/src/plugins/builtin/composite-plugins.ts +2 -1
  117. package/src/plugins/builtin/cot/client.ts +28 -2
  118. package/src/plugins/builtin/cot/headless.ts +4 -1
  119. package/src/plugins/builtin/cot/index.tsx +4 -2
  120. package/src/plugins/builtin/cot/model.ts +58 -7
  121. package/src/plugins/builtin/cot/pane.tsx +6 -2
  122. package/src/plugins/builtin/cpi/client.ts +6 -5
  123. package/src/plugins/builtin/credit-boards/series.ts +3 -4
  124. package/src/plugins/builtin/credit-conditions/client.ts +22 -14
  125. package/src/plugins/builtin/credit-documents/headless.ts +3 -3
  126. package/src/plugins/builtin/crypto-board/client.ts +5 -5
  127. package/src/plugins/builtin/crypto-board/series.ts +4 -4
  128. package/src/plugins/builtin/debt-maturities/series.ts +3 -3
  129. package/src/plugins/builtin/doe/client.ts +6 -5
  130. package/src/plugins/builtin/doe/headless.ts +3 -2
  131. package/src/plugins/builtin/doe/pane.tsx +13 -3
  132. package/src/plugins/builtin/doe/prices.ts +2 -3
  133. package/src/plugins/builtin/doe/series.ts +4 -4
  134. package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
  135. package/src/plugins/builtin/earnings-calls/data.ts +37 -17
  136. package/src/plugins/builtin/earnings-calls/headless.ts +13 -3
  137. package/src/plugins/builtin/earnings-calls/pane.tsx +34 -14
  138. package/src/plugins/builtin/econ/calendar-model.ts +48 -1
  139. package/src/plugins/builtin/econ/calendar-source.ts +4 -2
  140. package/src/plugins/builtin/econ/detail-view.tsx +18 -4
  141. package/src/plugins/builtin/econ/figures.ts +48 -0
  142. package/src/plugins/builtin/econ/index.tsx +14 -5
  143. package/src/plugins/builtin/econ/print-watch.ts +3 -1
  144. package/src/plugins/builtin/econ-statistics/client.ts +11 -0
  145. package/src/plugins/builtin/econ-statistics/headless.ts +10 -2
  146. package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
  147. package/src/plugins/builtin/econ-statistics/stats.ts +3 -0
  148. package/src/plugins/builtin/executives/data.ts +6 -1
  149. package/src/plugins/builtin/executives/pane.tsx +9 -21
  150. package/src/plugins/builtin/fear-greed/cache.ts +4 -0
  151. package/src/plugins/builtin/fear-greed/series.ts +6 -4
  152. package/src/plugins/builtin/gpu/client.ts +4 -1
  153. package/src/plugins/builtin/gpu/series.ts +2 -3
  154. package/src/plugins/builtin/help/index.tsx +61 -59
  155. package/src/plugins/builtin/help/shortcut-table.tsx +5 -6
  156. package/src/plugins/builtin/help/table-cells.tsx +13 -2
  157. package/src/plugins/builtin/holders/beneficial-client.ts +5 -1
  158. package/src/plugins/builtin/holders/beneficial-view.tsx +4 -2
  159. package/src/plugins/builtin/holders/headless.ts +4 -2
  160. package/src/plugins/builtin/holders/pane.tsx +3 -0
  161. package/src/plugins/builtin/holders/report-rows.ts +4 -1
  162. package/src/plugins/builtin/insider/headless.ts +2 -2
  163. package/src/plugins/builtin/ipo-calendar/client.ts +18 -2
  164. package/src/plugins/builtin/ipo-calendar/headless.ts +75 -11
  165. package/src/plugins/builtin/ipo-calendar/listing-ipo.ts +160 -0
  166. package/src/plugins/builtin/ipo-calendar/model.ts +98 -2
  167. package/src/plugins/builtin/jodi/client.ts +4 -1
  168. package/src/plugins/builtin/macro-day/headless.ts +12 -5
  169. package/src/plugins/builtin/macro-day/index.tsx +1 -1
  170. package/src/plugins/builtin/macro-day/model.ts +39 -5
  171. package/src/plugins/builtin/macro-day/pane.tsx +27 -19
  172. package/src/plugins/builtin/market-movers/client.ts +3 -0
  173. package/src/plugins/builtin/market-movers/headless.ts +3 -1
  174. package/src/plugins/builtin/market-movers/index.tsx +10 -5
  175. package/src/plugins/builtin/market-movers/screener.ts +27 -6
  176. package/src/plugins/builtin/market-movers/session-body.tsx +2 -1
  177. package/src/plugins/builtin/mna/headless.ts +2 -0
  178. package/src/plugins/builtin/mna/pane.tsx +3 -1
  179. package/src/plugins/builtin/money-markets/series.ts +3 -5
  180. package/src/plugins/builtin/news/headless.ts +3 -1
  181. package/src/plugins/builtin/news/index.tsx +3 -0
  182. package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -6
  183. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +0 -14
  184. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +2 -1
  185. package/src/plugins/builtin/portfolio-list/table.tsx +4 -6
  186. package/src/plugins/builtin/power/gas-client.ts +2 -1
  187. package/src/plugins/builtin/power/prices-client.ts +3 -2
  188. package/src/plugins/builtin/realized-vol/client.ts +13 -5
  189. package/src/plugins/builtin/realized-vol/evidence.ts +29 -4
  190. package/src/plugins/builtin/realized-vol/headless.ts +8 -4
  191. package/src/plugins/builtin/realized-vol/model.ts +37 -6
  192. package/src/plugins/builtin/realized-vol/pane.tsx +9 -7
  193. package/src/plugins/builtin/research/event-model.ts +30 -4
  194. package/src/plugins/builtin/research/index.tsx +7 -3
  195. package/src/plugins/builtin/research/relative-valuation-headless.ts +8 -25
  196. package/src/plugins/builtin/research/relative-valuation-pane.tsx +52 -15
  197. package/src/plugins/builtin/research/relative-valuation-peers.ts +217 -20
  198. package/src/plugins/builtin/research/relative-valuation-settings.ts +44 -0
  199. package/src/plugins/builtin/risk-factors/data.ts +12 -6
  200. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  201. package/src/plugins/builtin/risk-factors/index.tsx +3 -1
  202. package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
  203. package/src/plugins/builtin/roadmap/index.tsx +28 -0
  204. package/src/plugins/builtin/roadmap/model.ts +67 -0
  205. package/src/plugins/builtin/roadmap/pane.tsx +244 -0
  206. package/src/plugins/builtin/scanner/live-headless.ts +21 -2
  207. package/src/plugins/builtin/scanner/lock-note.ts +26 -3
  208. package/src/plugins/builtin/sec/headless.ts +2 -1
  209. package/src/plugins/builtin/sec/index.tsx +3 -0
  210. package/src/plugins/builtin/shared/cloud-resource.ts +20 -7
  211. package/src/plugins/builtin/shared/fred-points.ts +6 -1
  212. package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
  213. package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
  214. package/src/plugins/builtin/shared/report-freshness.ts +8 -0
  215. package/src/plugins/builtin/shared/research-cells.tsx +1 -1
  216. package/src/plugins/builtin/shared/series-cache.ts +4 -4
  217. package/src/plugins/builtin/shared/series-warning.ts +13 -4
  218. package/src/plugins/builtin/short-volume/series.ts +2 -3
  219. package/src/plugins/builtin/social-mentions/client.ts +15 -10
  220. package/src/plugins/builtin/social-mentions/series.ts +2 -3
  221. package/src/plugins/builtin/thirteenf/api.ts +15 -9
  222. package/src/plugins/builtin/thirteenf/data.ts +39 -11
  223. package/src/plugins/builtin/thirteenf/headless.ts +31 -4
  224. package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
  225. package/src/plugins/builtin/thirteenf/pane.tsx +21 -5
  226. package/src/plugins/builtin/thirteenf/types.ts +2 -0
  227. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +5 -2
  228. package/src/plugins/builtin/ticker-detail/financials/model.ts +35 -2
  229. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +53 -18
  230. package/src/plugins/builtin/ticker-detail/headless.ts +59 -8
  231. package/src/plugins/builtin/ticker-detail/overview/model.ts +10 -2
  232. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
  233. package/src/plugins/builtin/ticker-detail/pane.tsx +11 -6
  234. package/src/plugins/builtin/treasury-auctions/cache.ts +20 -8
  235. package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
  236. package/src/plugins/builtin/treasury-auctions/headless.ts +24 -13
  237. package/src/plugins/builtin/treasury-auctions/pane.tsx +18 -6
  238. package/src/plugins/builtin/volatility/client.ts +43 -16
  239. package/src/plugins/builtin/volatility/headless.ts +5 -2
  240. package/src/plugins/builtin/volatility/model.ts +17 -6
  241. package/src/plugins/builtin/volatility/pane.tsx +11 -4
  242. package/src/plugins/builtin/volatility/series.ts +2 -1
  243. package/src/plugins/builtin/yield-curve/client.ts +59 -1
  244. package/src/plugins/builtin/yield-curve/headless.ts +9 -6
  245. package/src/plugins/builtin/yield-curve/index.tsx +34 -10
  246. package/src/plugins/builtin/yield-curve/swaps.tsx +9 -5
  247. package/src/plugins/catalog-browser.ts +2 -1
  248. package/src/plugins/host-link.ts +1 -1
  249. package/src/plugins/installer.ts +50 -39
  250. package/src/portfolio/column-values.ts +11 -7
  251. package/src/renderers/dom/desktop/icons.tsx +10 -2
  252. package/src/renderers/electrobun/bun/desktop/backend.ts +9 -10
  253. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
  254. package/src/renderers/electrobun/bun/desktop/host-requests.ts +8 -0
  255. package/src/renderers/electrobun/bun/desktop/initialization.ts +9 -1
  256. package/src/renderers/electrobun/bun/desktop/installer-folder.ts +731 -0
  257. package/src/renderers/electrobun/bun/desktop/relaunch.ts +102 -10
  258. package/src/renderers/electrobun/bun/desktop/windows-script.ts +92 -0
  259. package/src/renderers/electrobun/bun/index.ts +26 -2
  260. package/src/renderers/electrobun/bun/window/attention.ts +54 -0
  261. package/src/renderers/electrobun/bun/window/create.ts +45 -4
  262. package/src/renderers/electrobun/bun/window/events.ts +20 -0
  263. package/src/renderers/electrobun/bun/window/windows-custom-chrome.ts +9 -2
  264. package/src/renderers/electrobun/bun/window/windows-native.ts +78 -0
  265. package/src/renderers/electrobun/bun/window/windows-quit.ts +57 -0
  266. package/src/renderers/electrobun/launcher/preload.ts +13 -0
  267. package/src/renderers/electrobun/launcher/windows-emulation.ts +50 -0
  268. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  269. package/src/renderers/electrobun/view/news-source.ts +2 -0
  270. package/src/renderers/electrobun/view/ui-host.tsx +16 -1
  271. package/src/series/resolve.ts +2 -1
  272. package/src/series/types.ts +5 -0
  273. package/src/sources/gloomberb-cloud/fred-series.ts +8 -4
  274. package/src/sources/gloomberb-cloud/index.ts +42 -8
  275. package/src/telemetry/stall-reports-node.ts +13 -2
  276. package/src/telemetry/stall-reports.ts +189 -13
  277. package/src/test-support/outage.ts +8 -0
  278. package/src/tickers/company-name-listing.ts +110 -40
  279. package/src/tickers/company-name.ts +71 -2
  280. package/src/tickers/index-roots.ts +7 -0
  281. package/src/tickers/listing-fate.ts +2 -2
  282. package/src/tickers/search/home-listing.ts +93 -8
  283. package/src/tickers/search/index.ts +7 -3
  284. package/src/tickers/search/ranking.ts +5 -1
  285. package/src/tickers/search/upsert.ts +13 -0
  286. package/src/tickers/ticker-changes.ts +8 -4
  287. package/src/time-series/market.ts +5 -3
  288. package/src/time-series/price-comparison.ts +20 -2
  289. package/src/time-series/resolve.ts +20 -1
  290. package/src/time-series/types.ts +7 -0
  291. package/src/types/company-from.ts +40 -0
  292. package/src/types/data-provider.ts +7 -0
  293. package/src/types/financials.ts +45 -3
  294. package/src/types/headless.ts +6 -0
  295. package/src/ui/host.tsx +6 -1
  296. package/src/utils/crypto-pair.ts +121 -7
  297. package/src/utils/declared-dividends.ts +1 -1
  298. package/src/utils/financial-statements.ts +73 -16
  299. package/src/utils/fundamentals.ts +53 -10
  300. package/src/utils/lease-liabilities.ts +54 -0
  301. package/src/utils/main-thread-stall.ts +47 -2
  302. package/src/utils/perf-marks.ts +23 -11
  303. package/src/utils/price-history-integrity.ts +47 -1
  304. package/src/version.ts +2 -2
@@ -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
  }
@@ -47,6 +47,14 @@ export function barResolutionFromDates(dates: readonly (string | number | Date)[
47
47
  /** Company filings read from EDGAR. Old filings are not stale; there is no schedule to miss. */
48
48
  export const SEC_FILINGS: HeadlessPaneFreshness = { source: "SEC EDGAR", status: "not-a-feed", basis: "filed data", oldest: null };
49
49
 
50
+ /**
51
+ * A list of filings, dated by its newest one's filing date and by when it was
52
+ * read: `newest filing Tue 27 Oct 2026 · retrieved Wed 28 Oct 2026 19:07 UTC`.
53
+ */
54
+ export const SEC_FILING_ROWS: HeadlessPaneFreshness = {
55
+ ...SEC_FILINGS, observedKey: "filingDate", asOfLabel: "newest filing", retrievedToo: true,
56
+ };
57
+
50
58
  /**
51
59
  * What a filings view opened inside a pane that hosts others (Issuer Debt in
52
60
  * Fixed Income) declares as its template's `reportFreshness`, so a capture
@@ -14,7 +14,7 @@ import { displayWidth } from "../../../utils/format";
14
14
  */
15
15
 
16
16
  /** The one placeholder for a value the source does not give. */
17
- const NO_VALUE = "--";
17
+ export const NO_VALUE = "--";
18
18
 
19
19
  export const missingCell = (colors: ThemeColors): DataTableCell => ({ text: NO_VALUE, value: null, color: colors.textMuted });
20
20
 
@@ -74,9 +74,9 @@ export function createSeriesCache(kind: string, staleMs: number): SeriesCache {
74
74
  let generation = 0;
75
75
  let persistence: PluginPersistence | null = null;
76
76
  const withFailure = <T extends SeriesCacheLoadResult>(key: string, result: T): T => {
77
- const failure = failures.get(key);
78
- return failure
79
- ? { ...result, stale: true, source: "stale-fallback", refreshError: failure }
77
+ // A failure is one whatever it said: an Error without a message still leaves the copy a saved one.
78
+ return failures.has(key)
79
+ ? { ...result, stale: true, source: "stale-fallback", refreshError: failures.get(key) }
80
80
  : result;
81
81
  };
82
82
  const loadEntry: SeriesCache["loadEntry"] = async (key, loader, options) => {
@@ -103,7 +103,7 @@ export function createSeriesCache(kind: string, staleMs: number): SeriesCache {
103
103
  if (owner === generation && requests.get(key) === request) requests.delete(key);
104
104
  }
105
105
  }, options);
106
- return withFailure(key, { ...data, fetchedAt, stale, source, ...(refreshError ? { refreshError } : {}) });
106
+ return withFailure(key, { ...data, fetchedAt, stale, source, ...(source === "stale-fallback" ? { refreshError } : {}) });
107
107
  };
108
108
  const reset = () => {
109
109
  generation += 1;
@@ -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
  }
@@ -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
  }];
@@ -1,9 +1,10 @@
1
1
  import { unavailableText } from "../../../components/ui/status";
2
2
  import { apiClient } from "../../../api-client";
3
- import { ApiRequestError, isAccessDenied } from "../../../api-client/errors";
3
+ import { isAccessDenied } from "../../../api-client/errors";
4
4
  import type { SocialMentionDayPosts, SocialMentionPost, SocialMentionsPayload, SocialMentionsRange } from "../../../api-client/social-mentions";
5
- import { createPluginCache } from "../../../data/plugin-cache";
5
+ import { createPluginCache, savedCopyForTransientFailure } from "../../../data/plugin-cache";
6
6
  import { CLOUD_SESSION_REQUIRED } from "../shared/research-cloud-session";
7
+ import { unavailableOnServer } from "../shared/cloud-resource";
7
8
 
8
9
  export const socialMentionsCache = createPluginCache<SocialMentionsPayload>({
9
10
  kind: "social-mentions", source: "gloom-cloud", schemaVersion: 1,
@@ -56,12 +57,15 @@ export function validateSocialMentions(payload: SocialMentionsPayload, symbol: s
56
57
  export async function fetchSocialMentions(symbol: string, range: SocialMentionsRange = "1y",
57
58
  client: Pick<typeof apiClient, "getCloudSocialMentions"> = apiClient): Promise<SocialMentionsPayload> {
58
59
  try { return validateSocialMentions(await client.getCloudSocialMentions(symbol, range), symbol, range); }
59
- catch (error) {
60
- if (error instanceof ApiRequestError && error.status === 404) throw new Error(unavailableText("social mentions"));
61
- throw error;
62
- }
60
+ catch (error) { throw unavailableOnServer(error, unavailableText("social mentions")); }
61
+ }
62
+ export interface SocialMentionsResource {
63
+ payload: SocialMentionsPayload;
64
+ stale: boolean;
65
+ refreshError: string | null;
66
+ /** When the copy shown was saved, when it is one because its refresh failed. */
67
+ savedAt?: number;
63
68
  }
64
- export interface SocialMentionsResource { payload: SocialMentionsPayload; stale: boolean; refreshError: string | null }
65
69
  const cacheKey = (symbol: string, range: SocialMentionsRange) => `${range}:${symbol}`;
66
70
  export function cachedSocialMentions(symbol: string, range: SocialMentionsRange): SocialMentionsResource | null {
67
71
  const cached = socialMentionsCache.get(cacheKey(symbol, range), { allowExpired: true });
@@ -70,9 +74,10 @@ export function cachedSocialMentions(symbol: string, range: SocialMentionsRange)
70
74
  catch { return null; }
71
75
  }
72
76
  export async function loadSocialMentions(symbol: string, range: SocialMentionsRange, force = false): Promise<SocialMentionsResource> {
73
- const result = await socialMentionsCache.load(cacheKey(symbol, range), () => fetchSocialMentions(symbol, range), { force });
74
- if (result.error instanceof ApiRequestError && [401, 403].includes(result.error.status ?? 0)) throw result.error;
75
- return { payload: validateSocialMentions(result.data, symbol, range), stale: result.stale, refreshError: result.refreshError ?? null };
77
+ // A refusal (a signed-out session, any other 4xx but 408 and 429) or a reply that fails validation shows as itself.
78
+ const result = savedCopyForTransientFailure(await socialMentionsCache.load(cacheKey(symbol, range), () => fetchSocialMentions(symbol, range), { force }));
79
+ return { payload: validateSocialMentions(result.data, symbol, range), stale: result.stale, refreshError: result.refreshError ?? null,
80
+ ...(result.source === "stale-fallback" ? { savedAt: result.fetchedAt } : {}) };
76
81
  }
77
82
 
78
83
  /**
@@ -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 { loadSocialMentions } from "./client";
4
4
 
5
5
  /** How often a ticker is posted about each day, over the last year. */
@@ -14,10 +14,9 @@ export const SOCIAL_MENTION_SERIES: readonly SeriesDefinition[] = [{
14
14
  style: "columns",
15
15
  load: async ({ ticker }) => {
16
16
  const resource = await loadSocialMentions(ticker!, "1y");
17
- const warning = cachedSeriesWarning(resource);
18
17
  return {
19
18
  points: resource.payload.x.days.map((row) => ({ date: row.day, value: row.mentions })),
20
- ...(warning ? { warning } : {}),
19
+ ...cachedSeriesNote(resource),
21
20
  };
22
21
  },
23
22
  }];
@@ -10,7 +10,7 @@ import type {
10
10
  } from "./types";
11
11
  import type { PluginPersistence } from "../../../types/plugin";
12
12
  import { apiClient } from "../../../api-client";
13
- import { ApiRequestError, isPermanentClientError } from "../../../api-client/errors";
13
+ import { ApiRequestError, isPermanentClientError, isTransientFailure } from "../../../api-client/errors";
14
14
  import { httpFetch } from "../../../utils/http-transport";
15
15
  import { asRecord, finiteOrNull } from "../../../utils/guards";
16
16
  import { logFailure } from "../../../utils/log-failure";
@@ -30,7 +30,14 @@ const FORMS_13F_CACHE_POLICY = {
30
30
  } as const;
31
31
 
32
32
  export interface Forms13FReadOptions {
33
+ /** A part of the answer that could not be read, said beside what was. */
33
34
  onWarning?: (warning: string) => void;
35
+ /**
36
+ * A read answered from the copy saved at `savedAt` (ms) because its refresh
37
+ * failed. Only a caller that says when its copy was saved (the footer's
38
+ * "saved copy from", a command's closing line) is answered from one.
39
+ */
40
+ onSavedCopy?: (savedAt: number) => void;
34
41
  forceRefresh?: boolean;
35
42
  }
36
43
 
@@ -150,18 +157,17 @@ async function fetchForms13F<T>(
150
157
  } catch (error) {
151
158
  options.signal?.throwIfAborted();
152
159
  if (current()) failedRefreshes.add(key);
153
- if (isPermanentClientError(error)) {
154
- if (current()) store?.deleteResource(CACHE_KIND, key, { sourceKey: CACHE_SOURCE });
160
+ // A saved copy answers only for a failure asking again may get past; a refusal or a reply that
161
+ // fails validation is the error, and a missing or denied resource also drops its copy.
162
+ if (!isTransientFailure(error)) {
163
+ if (isPermanentClientError(error) && current()) store?.deleteResource(CACHE_KIND, key, { sourceKey: CACHE_SOURCE });
155
164
  throw error;
156
165
  }
157
166
  const retained = options.cache !== false
158
167
  ? store?.getResource<T>(CACHE_KIND, key, { ...cacheOptions, allowExpired: true }) : null;
159
- // A caller may retain old research only when it can surface its provenance.
160
- if (!retained || !options.onWarning) throw error;
161
- const retrieved = new Date(retained.fetchedAt);
162
- const age = Number.isFinite(retrieved.getTime()) ? `retrieved ${retrieved.toISOString()}` : "with unavailable retrieval time";
163
- const reason = (error instanceof Error ? error.message : String(error)).trim() || "Request failed";
164
- options.onWarning(`${key}: refresh failed: ${reason}. Retained data ${age}.`);
168
+ // A caller may retain old research only when it can say when it was saved.
169
+ if (!retained || !options.onSavedCopy || !Number.isFinite(retained.fetchedAt)) throw error;
170
+ options.onSavedCopy(retained.fetchedAt);
165
171
  return retained.value;
166
172
  } finally {
167
173
  if (activeRequests.get(key) === slot.promise) activeRequests.delete(key);
@@ -106,6 +106,8 @@ export interface BrowserLoadResult {
106
106
  period?: string;
107
107
  quarter?: string;
108
108
  warning?: string;
109
+ /** A refresh failed and the oldest copy that answered instead was saved at this time (ms). */
110
+ savedAt?: number;
109
111
  hasMore?: boolean;
110
112
  nextOffset?: number;
111
113
  reportsLoaded?: Promise<void>;
@@ -115,14 +117,34 @@ export async function loadBrowserRows(
115
117
  tab: ThirteenFBrowserTab,
116
118
  query: string,
117
119
  signal?: AbortSignal,
118
- options: { forceRefresh?: boolean; offset?: number; limit?: number } = {},
120
+ options: Pick<Forms13FReadOptions, "forceRefresh" | "onSavedCopy"> & { offset?: number; limit?: number } = {},
119
121
  ): Promise<BrowserLoadResult> {
120
122
  const warnings: string[] = [];
121
- const result = await loadBrowserPage(tab, query, signal, { ...options, onWarning: warning => warnings.push(warning) });
123
+ const page: { result?: BrowserLoadResult; savedAt?: number } = {};
124
+ const onSavedCopy = (savedAt: number) => {
125
+ page.savedAt = Math.min(page.savedAt ?? savedAt, savedAt);
126
+ // The rows' reports answer after the page does, and their copies date it too.
127
+ if (page.result) page.result.savedAt = page.savedAt;
128
+ options.onSavedCopy?.(savedAt);
129
+ };
130
+ const result = await loadBrowserPage(tab, query, signal, { ...options, onSavedCopy, onWarning: warning => warnings.push(warning) });
131
+ page.result = result;
132
+ if (page.savedAt != null) result.savedAt = page.savedAt;
122
133
  if (warnings.length) result.warning = [...new Set([result.warning, ...warnings].filter(Boolean))].join(" ") || undefined;
123
134
  return result;
124
135
  }
125
136
 
137
+ /** The oldest saved copy a load answered from (`savedAt`), each also passed on to `onSavedCopy`. */
138
+ function savedCopies(onSavedCopy?: (savedAt: number) => void) {
139
+ const copies: { savedAt?: number; note: (savedAt: number) => void } = {
140
+ note: (savedAt) => {
141
+ copies.savedAt = Math.min(copies.savedAt ?? savedAt, savedAt);
142
+ onSavedCopy?.(savedAt);
143
+ },
144
+ };
145
+ return copies;
146
+ }
147
+
126
148
  async function loadBrowserPage(
127
149
  tab: ThirteenFBrowserTab,
128
150
  query: string,
@@ -136,13 +158,13 @@ async function loadBrowserPage(
136
158
  const offset = Math.max(0, options.offset ?? 0);
137
159
  const browserLimit = Math.max(1, options.limit ?? BROWSER_PAGE_LIMIT);
138
160
  const latestLimit = Math.max(1, options.limit ?? LATEST_FILINGS_PAGE_LIMIT);
139
- const apiOptions = { forceRefresh: options.forceRefresh, onWarning: options.onWarning };
161
+ const apiOptions = { forceRefresh: options.forceRefresh, onWarning: options.onWarning, onSavedCopy: options.onSavedCopy };
140
162
  const pageApiOptions = { ...apiOptions, offset };
141
163
 
142
164
  if (tab === "performance") {
143
165
  const topFunds = await listTopThirteenFFunds(quarter, browserLimit, signal, pageApiOptions);
144
166
  const quarters = priorQuarters(quarter);
145
- const history = await loadRankingHistory(quarters, signal, { ...apiOptions, onWarning: options.onWarning });
167
+ const history = await loadRankingHistory(quarters, signal, apiOptions);
146
168
  const funds: ThirteenFFund[] = topFunds.map((fund) => ({ cik: fund.cik, name: fund.name }));
147
169
  const rows = buildBrowserRows({ topFunds, source: "performance" }).map(row => ({ ...row, priorReturns: quarters.map((previous, index) => ({ quarter: previous, value: history[index]?.get(row.cik) ?? null })) }));
148
170
  const result: BrowserLoadResult = {
@@ -437,10 +459,11 @@ export async function loadFundPerformance(
437
459
  cik: string,
438
460
  fallbackName: string,
439
461
  signal?: AbortSignal,
440
- options: { forceRefresh?: boolean; splitFactor?: SplitFactorLookup } = {},
462
+ options: Pick<Forms13FReadOptions, "forceRefresh" | "onSavedCopy"> & { splitFactor?: SplitFactorLookup } = {},
441
463
  ): Promise<FundPerformanceResult> {
442
464
  const warnings: string[] = [];
443
- const apiOptions = { forceRefresh: options.forceRefresh, onWarning: (warning: string) => warnings.push(warning) };
465
+ const saved = savedCopies(options.onSavedCopy);
466
+ const apiOptions = { forceRefresh: options.forceRefresh, onSavedCopy: saved.note, onWarning: (warning: string) => warnings.push(warning) };
444
467
  const detail = await loadFundDetail(cik, fallbackName, signal, options);
445
468
  const period = detail.latestForm?.periodOfReport;
446
469
  const quarter = period ? periodToQuarter(period) : null;
@@ -482,7 +505,9 @@ export async function loadFundPerformance(
482
505
  : filed?.skipped.length ? `No share position held through the quarter to ${period} that could be priced across it.`
483
506
  : `No share position held through the quarter to ${period}.`;
484
507
  const warning = [...new Set([...(detail.warnings ?? []), ...warnings])].join(" ") || undefined;
508
+ const savedAt = Math.min(detail.savedAt ?? Infinity, saved.savedAt ?? Infinity);
485
509
  return {
510
+ ...(Number.isFinite(savedAt) ? { savedAt } : {}),
486
511
  rows: [row], ...(quarter ? { quarter } : {}), ...(period ? { period } : {}), hasMore: false, basis, ranking,
487
512
  ...(filed?.splits.length ? { splits: filed.splits } : {}),
488
513
  ...(filed?.skipped.length ? { skipped: filed.skipped } : {}),
@@ -496,11 +521,12 @@ export async function loadFundDetail(
496
521
  fallbackName: string,
497
522
  signal?: AbortSignal,
498
523
  /** `filingGap: false` skips EDGAR's filing list, for a load that never shows it (an overlap). */
499
- options: { forceRefresh?: boolean; filingGap?: boolean } = {},
524
+ options: Pick<Forms13FReadOptions, "forceRefresh" | "onSavedCopy"> & { filingGap?: boolean } = {},
500
525
  ): Promise<FundDetailData> {
501
526
  const now = new Date();
502
527
  const warnings: string[] = [];
503
- const apiOptions = { forceRefresh: options.forceRefresh, onWarning: (warning: string) => warnings.push(warning) };
528
+ const saved = savedCopies(options.onSavedCopy);
529
+ const apiOptions = { forceRefresh: options.forceRefresh, onSavedCopy: saved.note, onWarning: (warning: string) => warnings.push(warning) };
504
530
 
505
531
  const forms = await listThirteenFForms(
506
532
  cik,
@@ -554,6 +580,7 @@ export async function loadFundDetail(
554
580
  previousReport,
555
581
  warnings,
556
582
  filingGap,
583
+ ...(saved.savedAt != null ? { savedAt: saved.savedAt } : {}),
557
584
  };
558
585
  }
559
586
 
@@ -562,12 +589,13 @@ export async function loadFilingPositions(
562
589
  accessionNumber: string,
563
590
  signal?: AbortSignal,
564
591
  options: { forceRefresh?: boolean; offset?: number; limit?: number } = {},
565
- ): Promise<{ rows: ThirteenFHoldingRecord[]; hasMore: boolean; warnings: string[] }> {
592
+ ): Promise<{ rows: ThirteenFHoldingRecord[]; hasMore: boolean; warnings: string[]; savedAt?: number }> {
566
593
  const warnings: string[] = [];
594
+ const saved = savedCopies();
567
595
  const result = await listThirteenFFormHoldingsPage(cik, accessionNumber, signal, {
568
- ...options, onWarning: warning => warnings.push(warning),
596
+ ...options, onSavedCopy: saved.note, onWarning: warning => warnings.push(warning),
569
597
  });
570
- return { ...result, warnings };
598
+ return { ...result, warnings, ...(saved.savedAt != null ? { savedAt: saved.savedAt } : {}) };
571
599
  }
572
600
 
573
601
  /** `2026Q2` for a quarter end such as 2026-06-30. */
@@ -6,6 +6,7 @@ import type {
6
6
  HeadlessShotState,
7
7
  } from "../../../types/plugin";
8
8
  import { formatShortDate } from "../../../utils/datetime-format";
9
+ import { servedFromSavedCopy } from "../../../data/saved-copies";
9
10
  import { normalizeCik, searchThirteenFFunds, TOP_FUNDS_MAX_LIMIT } from "./api";
10
11
  import { ApiRequestError } from "../../../api-client/errors";
11
12
  import { REPORT_PAGE, REPORT_ROWS } from "../shared/headless-options";
@@ -229,15 +230,40 @@ export interface ThirteenFHeadlessDependencies {
229
230
  loadEtfHoldings?(ticker: string, ctx: HeadlessPaneContext): Promise<FundMembersPayload>;
230
231
  }
231
232
 
233
+ /** The oldest copy a load's 13F reads answered from because their refresh failed, by the load's context. */
234
+ const savedCopies = new WeakMap<HeadlessPaneContext, { savedAt: number | null }>();
235
+
236
+ /** How a 13F read for the load `ctx` says it answered from the copy saved at `savedAt`. */
237
+ const noteSavedCopy = (ctx: HeadlessPaneContext) => (savedAt: number) => {
238
+ const entry = savedCopies.get(ctx);
239
+ if (entry) entry.savedAt = Math.min(entry.savedAt ?? savedAt, savedAt);
240
+ };
241
+
242
+ /**
243
+ * One load, whose report ends with when the oldest saved copy it answered
244
+ * from was saved (`couldn't refresh; saved copy from <time>`), as every
245
+ * command says it, instead of listing each failed refresh as an error.
246
+ */
247
+ async function readingSavedCopies<T extends object>(ctx: HeadlessPaneContext, run: () => Promise<T>): Promise<T> {
248
+ const entry: { savedAt: number | null } = { savedAt: null };
249
+ savedCopies.set(ctx, entry);
250
+ try {
251
+ const result = await run();
252
+ return entry.savedAt == null ? result : servedFromSavedCopy(result, entry.savedAt);
253
+ } finally {
254
+ if (savedCopies.get(ctx) === entry) savedCopies.delete(ctx);
255
+ }
256
+ }
257
+
232
258
  const defaultDependencies: Required<ThirteenFHeadlessDependencies> = {
233
259
  loadBrowser: (tab, query, limit, _args, ctx) => loadBrowserRows(
234
260
  tab,
235
261
  query,
236
262
  ctx.signal,
237
- { limit },
263
+ { limit, onSavedCopy: noteSavedCopy(ctx) },
238
264
  ),
239
265
  // An overlap never shows a fund's missed quarter, so it does not ask EDGAR.
240
- loadDetail: (cik, name, args, ctx) => loadFundDetail(cik, name, ctx.signal, { filingGap: !isOverlapLoad(args) }),
266
+ loadDetail: (cik, name, args, ctx) => loadFundDetail(cik, name, ctx.signal, { filingGap: !isOverlapLoad(args), onSavedCopy: noteSavedCopy(ctx) }),
241
267
  loadSplitFactors: (detail, ctx) => loadSplitFactors(
242
268
  detail,
243
269
  (ticker, after, through) => corporateSplitFactor(ctx.marketData, ticker, after, through),
@@ -247,6 +273,7 @@ const defaultDependencies: Required<ThirteenFHeadlessDependencies> = {
247
273
  searchFunds: (query, ctx) => searchThirteenFFunds(query, THIRTEENF_MAX_LIMIT, ctx.signal),
248
274
  loadPerformance: (cik, name, ctx) => loadFundPerformance(cik, name, ctx.signal, {
249
275
  splitFactor: (ticker, after, through) => corporateSplitFactor(ctx.marketData, ticker, after, through),
276
+ onSavedCopy: noteSavedCopy(ctx),
250
277
  }),
251
278
  loadEtfFunds: async (ctx) => (await fetchFunds(ctx.apiClient)).funds,
252
279
  loadEtfHoldings: (ticker, ctx) => fetchMembers(ticker, ctx.apiClient),
@@ -541,7 +568,7 @@ export function createThirteenFHeadless(
541
568
  ],
542
569
  describe: (args, result) => thirteenFTitle(args, result?.metadata),
543
570
  shotState: async (args, load) => thirteenFShotState(args, (await load()).metadata ?? {}),
544
- async load(args, ctx) {
571
+ load: (args, ctx) => readingSavedCopies(ctx, async () => {
545
572
  const query = typeof args.argument === "string" ? args.argument.trim() : "";
546
573
  const requestedView = String(args.options.view) as HeadlessThirteenFView;
547
574
  const compare = typeof args.options.compare === "string" ? args.options.compare.trim() : "";
@@ -920,7 +947,7 @@ export function createThirteenFHeadless(
920
947
  ],
921
948
  },
922
949
  };
923
- },
950
+ }),
924
951
  };
925
952
  }
926
953
 
@@ -10,6 +10,7 @@ import { usePluginPaneState, usePluginTickerActions } from "../../runtime";
10
10
  import { useMineTickers } from "../shared/mine-tickers";
11
11
  import { usePaneStatusFooter } from "../../../components/layout/pane/status-footer";
12
12
  import { handleRefreshKey } from "../../../components/data-table/table-pane";
13
+ import { savedCopyFooterInfo } from "../../../components/layout/pane/footer";
13
14
  import { useAsyncResource } from "../../../react/async-resource";
14
15
  import { cachedFunds, cachedMembers, loadFunds, loadMembers } from "../members/client";
15
16
  import { coveredFund } from "../members/model";
@@ -115,7 +116,10 @@ export function FundOverlapView({ data, focused, width }: { data: FundDetailData
115
116
  const period = peer ? overlapPeriod(data, peer) : null;
116
117
  const mismatch = peer && !period;
117
118
  const listError = etf ? etfFile.error : error ?? paged.error?.message ?? paged.moreError?.message ?? null;
118
- usePaneStatusFooter({ registrationId: "13f-overlap", loading: loading || loadingMore || paged.loading || (!!etf && etfFile.loading), error: listError, hints: open ? [{ id: "mine", key: "m", label: mineOnly ? "all tickers" : "mine", title: mineOnly ? "All Tickers" : "Mine Only", onPress: () => setMineOnly(value => !value) }] : [{ id: "search", key: "/", label: "search", onPress: focusSearch }] });
119
+ // Either side answered from a copy a failed refresh left says when it was saved.
120
+ const etfSavedAt = etf ? etfFile.data?.savedAt : null;
121
+ const savedInfo = useMemo(() => savedCopyFooterInfo([data.savedAt, peer?.savedAt, etfSavedAt]), [data.savedAt, peer?.savedAt, etfSavedAt]);
122
+ usePaneStatusFooter({ registrationId: "13f-overlap", loading: loading || loadingMore || paged.loading || (!!etf && etfFile.loading), error: listError, info: savedInfo, hints: open ? [{ id: "mine", key: "m", label: mineOnly ? "all tickers" : "mine", title: mineOnly ? "All Tickers" : "Mine Only", onPress: () => setMineOnly(value => !value) }] : [{ id: "search", key: "/", label: "search", onPress: focusSearch }] });
119
123
  usePaneNoticeFooter({ registrationId: "13f-overlap-notice", focused, notices: [
120
124
  ...(peer?.warnings ?? []),
121
125
  ...(mismatch ? ["No reporting quarter is loaded for both funds; overlap is unavailable."] : []),
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import {
3
3
  DataTableStackView,
4
4
  DataTableView,
5
- EmptyState, PaneStatusBody, QueryBar, StatGrid, usePagedRows, usePaneNoticeFooter, usePaneTabs, useQueryBarSearch, useTableLoadMore, type DataTableKeyEvent,
5
+ EmptyState, PaneStatusBody, QueryBar, StatGrid, usePagedRows, usePaneFooter, usePaneNoticeFooter, usePaneTabs, useQueryBarSearch, useTableLoadMore, type DataTableKeyEvent,
6
6
  type DataTableRootKeyContext, type PageRequest, type PaneFooterSegment, type PaneHint, type StatItem
7
7
  } from "../../../components";
8
8
  import { useShortcut } from "../../../react/input";
@@ -26,7 +26,7 @@ import { usePaneStatusFooter, usePaneStatusLinkFooter } from "../../../component
26
26
  import { corporateSplitFactor, loadBrowserRows, loadFilingPositions, loadFundDetail, loadSplitFactors } from "./data";
27
27
  import { FundOverlapView } from "./overlap-pane";
28
28
  import { ThirteenFCrowdingPane, ThirteenFTickerHoldingsView } from "./signals-pane";
29
- import { PaneFooterScope } from "../../../components/layout/pane/footer";
29
+ import { PaneFooterScope, savedCopyFooterInfo } from "../../../components/layout/pane/footer";
30
30
  import { handleRefreshKey } from "../../../components/data-table/table-pane";
31
31
  import { useClaimResearchTabKeys } from "../ticker-detail/research-tab-keys";
32
32
  import {
@@ -129,6 +129,9 @@ function ThirteenFBrowserPane({ focused, width, height, onDetailChange }: PanePr
129
129
  const notes = [...(pagesSettled ? browser.pages.map((page) => page.warning) : []), browser.moreError?.message];
130
130
  return [...new Set(notes.filter(Boolean))].join(" ") || null;
131
131
  }, [browser.moreError, browser.pages, pagesSettled, reportPaint]);
132
+ // A page a failed refresh answered from a saved copy says when it was saved, as every pane does.
133
+ const savedInfo = useMemo(() => savedCopyFooterInfo(pagesSettled ? browser.pages.map((page) => page.savedAt) : []),
134
+ [browser.pages, pagesSettled, reportPaint]);
132
135
  const [detailSeed, setDetailSeed] = useState<FundSeed | null>(() => (
133
136
  initialCik ? { cik: String(initialCik), name: normalizedQuery || String(initialCik) } : null
134
137
  ));
@@ -166,7 +169,9 @@ function ThirteenFBrowserPane({ focused, width, height, onDetailChange }: PanePr
166
169
  }, { allowEditable: true });
167
170
 
168
171
  const browserSort = useMemo(() => browserSortFor(sortPreference, browserMode), [sortPreference, browserMode]);
169
- const sortedRows = useMemo(() => sortBrowserRows(rows, browserSort), [rows, browserSort, reportPaint]);
172
+ // The rows' reports fill the rows in place after their page answers (from the network or a saved copy);
173
+ // a row the table drew before then is drawn again only as a new object.
174
+ const sortedRows = useMemo(() => sortBrowserRows(rows.map((row) => ({ ...row })), browserSort), [rows, browserSort, reportPaint]);
170
175
  const columns = useMemo(() => {
171
176
  if (browserMode !== "performance") return buildBrowserColumns(false);
172
177
  const history = rows[0]?.priorReturns ?? [];
@@ -202,7 +207,8 @@ function ThirteenFBrowserPane({ focused, width, height, onDetailChange }: PanePr
202
207
  const browserStatusInfo = useMemo<PaneFooterSegment[]>(() => [
203
208
  ...(loadingMore ? [{ id: "loading-more", parts: [{ text: "loading more", tone: "muted" as const }] }] : []),
204
209
  ...(warning ? [{ id: "warning", parts: [{ text: warning, tone: "warning" as const }] }] : []),
205
- ], [loadingMore, warning]);
210
+ ...savedInfo,
211
+ ], [loadingMore, warning, savedInfo]);
206
212
  const searchHints = useMemo(() => [{ id: "search", key: "/", label: "search", onPress: focusSearch }], [focusSearch]);
207
213
  usePaneStatusFooter({
208
214
  registrationId: THIRTEENF_PANE_ID,
@@ -544,6 +550,9 @@ export function FundDetailView({
544
550
  const detailHints = useMemo<PaneHint[]>(() => hasHoldingFiling
545
551
  ? [{ id: "filing", key: "f", label: "iling", title: "Open Filing", onPress: openSelectedFilingInPane }]
546
552
  : [], [hasHoldingFiling, openSelectedFilingInPane]);
553
+ // An open filing and the overlap tab say when their own saved copies are from.
554
+ const detailSavedInfo = useMemo(() => savedCopyFooterInfo(openFiling || activeTab === "overlap" ? [] : [data?.savedAt]),
555
+ [activeTab, data?.savedAt, openFiling]);
547
556
  usePaneStatusLinkFooter({
548
557
  registrationId: "thirteenf-detail",
549
558
  focused,
@@ -551,6 +560,7 @@ export function FundDetailView({
551
560
  source: openFiling ? "SEC 13F" : null,
552
561
  loading: activeTab !== "overlap" && status === "loading",
553
562
  error: activeTab === "overlap" ? null : error,
563
+ info: detailSavedInfo,
554
564
  showOpenHint: true,
555
565
  hints: detailHints,
556
566
  });
@@ -590,7 +600,7 @@ export function FundDetailView({
590
600
  onBack={closeOpenFiling}
591
601
  detailTitle={openFiling ? `${openFiling.periodOfReport} filing` : "13F filing"}
592
602
  detailContent={openFiling ? (
593
- <FilingDetailView focused={focused} filing={openFiling} width={width} sourceWarnings={data?.warnings ?? []} />
603
+ <FilingDetailView focused={focused} filing={openFiling} width={width} sourceWarnings={data?.warnings ?? []} sourceSavedAt={data?.savedAt} />
594
604
  ) : (
595
605
  <Box flexGrow={1} />
596
606
  )}
@@ -659,11 +669,14 @@ function FilingDetailView({
659
669
  filing,
660
670
  width,
661
671
  sourceWarnings,
672
+ sourceSavedAt,
662
673
  }: {
663
674
  focused: boolean;
664
675
  filing: FundTimelineRow;
665
676
  width: number;
666
677
  sourceWarnings: string[];
678
+ /** When the fund's saved copy was saved, when a failed refresh left it on screen. */
679
+ sourceSavedAt?: number;
667
680
  }) {
668
681
  const { pinTicker } = usePluginTickerActions();
669
682
  const [sortPreference, setSortPreference] = usePluginPaneState<FundSortPreference<FilingPositionColumnId>>(
@@ -711,6 +724,9 @@ function FilingDetailView({
711
724
  );
712
725
 
713
726
  usePaneNoticeFooter({ registrationId: "thirteenf-filing-notices", notices: [...new Set([...sourceWarnings, ...warnings])], focused });
727
+ const savedInfo = useMemo(() => savedCopyFooterInfo([sourceSavedAt, ...(pagesSettled ? positions.pages.map((page) => page.savedAt) : [])]),
728
+ [pagesSettled, positions.pages, sourceSavedAt]);
729
+ usePaneFooter("thirteenf-filing-saved", () => ({ info: savedInfo }), [savedInfo]);
714
730
 
715
731
  // An untyped amendment may replace or add to the report, so only it is a caveat.
716
732
  const kind = amendmentKind(filing);
@@ -99,6 +99,8 @@ export interface FundDetailData {
99
99
  warnings?: string[];
100
100
  /** Set when the latest report predates the newest quarter past its deadline. */
101
101
  filingGap?: ThirteenFFilingGap | null;
102
+ /** A refresh failed and the oldest copy that answered instead was saved at this time (ms). */
103
+ savedAt?: number;
102
104
  }
103
105
 
104
106
  /** A manager a 13F-NT names as reporting its holdings. */