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
@@ -2,10 +2,14 @@ import type { CotClass, CotContractPayload, CotFamily, CotHistoryPoint, CotPosit
2
2
  import { staticSeries } from "../../../components/chart/static/series";
3
3
  import type { ResolvedSeries } from "../../../time-series/types";
4
4
  import type { PricePoint } from "../../../types/financials";
5
- import { formatPriceObservation } from "../../../market-data/market/format";
5
+ import { formatMarketPriceWithCurrency, formatPriceObservation } from "../../../market-data/market/format";
6
+ import { futuresPricePer } from "../../../market-data/futures-contract";
7
+ import { unitCurrencyToken } from "../../../time-series/price-units";
6
8
  import { cleanFloat32Price } from "../../../cli/history-rows";
7
9
  import { cryptoPairCoin, isFuturesSymbol } from "../shared/crypto-pair";
8
- import { bestNameMatch, FUTURES_CONTRACTS, futuresBoardContract, futuresBoardContractForTicker, futuresContractByName, type FuturesContract } from "../../../market-data/futures-board";
10
+ import {
11
+ bestNameMatch, FUTURES_CONTRACTS, futuresBoardContract, futuresBoardContractForTicker, futuresContractByCode, futuresContractByName, type FuturesContract,
12
+ } from "../../../market-data/futures-board";
9
13
 
10
14
  export const COT_CLASSES: Record<CotFamily, Array<{ value: CotClass; label: string }>> = {
11
15
  legacy: [{ value: "noncommercial", label: "Noncommercial" }, { value: "commercial", label: "Commercial" }, { value: "nonreportable", label: "Nonreportable" }],
@@ -162,7 +166,35 @@ export function cotContractCode(value: unknown): string | null {
162
166
  if (alias) return alias;
163
167
  const market = typedMarket(value);
164
168
  if (market && "root" in market) return ROOTS[market.root]!.code;
165
- return (market ? boardMarketCode(market.board) : null) ?? (market ? null : marketCode(input));
169
+ if (market) return boardMarketCode(market.board);
170
+ const proxy = proxyMarket(value);
171
+ const proxied = proxy ? futuresContractByCode(proxy.root) : null;
172
+ return (proxied ? boardMarketCode(proxied) : null) ?? marketCode(input);
173
+ }
174
+ /**
175
+ * Markets the CFTC does not report, opened as the one traders price and hedge
176
+ * them by: jet fuel (kerosene) as NY Harbor ULSD, heating oil (HO), which
177
+ * airlines' fuel hedges and analysts follow it with. Opened this way, the
178
+ * pane's title and the report say which market stands in.
179
+ */
180
+ const PROXY_MARKETS: ReadonlyArray<{ names: readonly string[]; root: string; subject: string }> = [
181
+ { names: ["jet fuel", "jet fuels", "kerosene", "kerosine", "jet kerosene", "aviation fuel", "avtur"], root: "HO", subject: "jet fuel" },
182
+ ];
183
+ function proxyMarket(value: string) {
184
+ const typed = value.trim().toLowerCase().replace(/[\s_-]+/g, " ");
185
+ return PROXY_MARKETS.find((proxy) => proxy.names.includes(typed)) ?? null;
186
+ }
187
+ /** What a typed name opens in place of a market the CFTC does not report ("jet fuel" opens HO), or null. */
188
+ export function cotProxyFor(value: unknown): { subject: string; root: string } | null {
189
+ if (typeof value !== "string" || typedMarket(value) || rootAlias(value.trim().toUpperCase())) return null;
190
+ const proxy = proxyMarket(value);
191
+ return proxy ? { subject: proxy.subject, root: proxy.root } : null;
192
+ }
193
+ /** The line a report opened through a proxy says: "Jet fuel has no CFTC market: heating oil (NY Harbor ULSD, HO) is the usual proxy." */
194
+ export function cotProxyNote(proxy: { subject: string; root: string }): string {
195
+ const subject = `${proxy.subject[0]!.toUpperCase()}${proxy.subject.slice(1)}`;
196
+ return `${subject} has no CFTC market: heating oil (NY Harbor ULSD, ${proxy.root}) is the usual proxy. Its price: gloom quote ${proxy.root}=F; `
197
+ + "US jet fuel stocks (EIA, weekly): gloom fn G CAP:doe:jet";
166
198
  }
167
199
  /** The markets the unknown-market hint names, by the code and the word a newcomer knows them by. */
168
200
  const SUGGESTED_MARKETS: ReadonlyArray<readonly [string, string]> = [
@@ -234,10 +266,27 @@ export function cotInteger(value: number | null, signed = false): string {
234
266
  return value == null ? "--" : `${signed && value > 0 ? "+" : ""}${value.toLocaleString("en-US", { maximumFractionDigits: 0 })}`;
235
267
  }
236
268
 
269
+ /**
270
+ * The front price's unit as a quote of the future writes it after the price:
271
+ * `USD/gal` for heating oil ($4.6218/gal), `USD` for gold, from the
272
+ * contract's unit ("USD / gallon"). Null where the unit names no currency
273
+ * (index points, Treasury points) or is unknown: the price reads bare.
274
+ */
275
+ export function cotFrontPriceUnit(code: string, contractUnit: string | null | undefined): string | null {
276
+ const currency = contractUnit ? unitCurrencyToken(contractUnit.split("/")[0] ?? "") : null;
277
+ if (!currency) return null;
278
+ const root = cotRoot(code) ?? undefined;
279
+ return `${currency}${futuresPricePer({ root, spec: { unit: contractUnit } } as Parameters<typeof futuresPricePer>[0], currency)}`;
280
+ }
281
+
237
282
  /** Legend values stay exact: a compact 7.8K hides the futures price and -100K the net position. */
238
- export function cotLegendValue(value: number, series: Pick<ResolvedSeries, "id">): string {
283
+ export function cotLegendValue(value: number, series: Pick<ResolvedSeries, "id"> & Partial<Pick<ResolvedSeries, "unit">>): string {
284
+ if (series.id !== "price") return cotInteger(value, true);
239
285
  // Drop the float32 tail of provider bars without rounding away a 1/128 Treasury tick.
240
- return series.id === "price" ? formatPriceObservation(cleanFloat32Price(value)) : cotInteger(value, true);
286
+ const price = cleanFloat32Price(value);
287
+ // In its currency and per its quantity, as a quote of the future reads: $4.6218/gal.
288
+ const [currency, per] = [series.unit?.split("/")[0]?.trim(), series.unit?.includes("/") ? `/${series.unit.split("/")[1]}` : ""];
289
+ return currency ? `${formatMarketPriceWithCurrency(price, currency, { assetCategory: "FUT" })}${per}` : formatPriceObservation(price);
241
290
  }
242
291
 
243
292
  /**
@@ -263,7 +312,9 @@ export function cotNetPoints(history: readonly CotHistoryPoint[], traderClass: C
263
312
  }
264
313
  return points;
265
314
  }
266
- export function cotChartSeries(data: CotContractPayload, traderClass: CotClass, price: readonly PricePoint[], colors: { positive: string; warning: string }): ResolvedSeries[] {
315
+ /** `priceUnit` is the front price's (cotFrontPriceUnit), which its legend value is written in. */
316
+ export function cotChartSeries(data: CotContractPayload, traderClass: CotClass, price: readonly PricePoint[], colors: { positive: string; warning: string },
317
+ priceUnit: string | null = null): ResolvedSeries[] {
267
318
  const series: ResolvedSeries[] = [{
268
319
  ...staticSeries(cotNetPoints(data.history, traderClass), { id: "net", label: `${COT_CLASSES[data.reportFamily].find((row) => row.value === traderClass)?.label ?? traderClass} net`, color: colors.warning, calendarSpaced: true }),
269
320
  panelId: "net", unit: "contracts", unitGroup: "positions",
@@ -271,7 +322,7 @@ export function cotChartSeries(data: CotContractPayload, traderClass: CotClass,
271
322
  // Net positioning is the subject, so it leads the legend and the strip; the front price follows.
272
323
  if (price.length) series.push({
273
324
  ...staticSeries(price.map((row) => ({ date: row.date, observedAt: row.date, value: row.close })), { id: "price", label: "Front price", color: colors.positive, calendarSpaced: true }),
274
- panelId: "price", unit: "", unitGroup: "price",
325
+ panelId: "price", unit: priceUnit ?? "", unitGroup: "price",
275
326
  });
276
327
  return series;
277
328
  }
@@ -12,7 +12,7 @@ import { Box } from "../../../ui";
12
12
  import { useAutoRefresh } from "../../../react/auto-refresh";
13
13
  import { compareSortValues, nextHeaderSort, type SortPreference } from "../../../utils/sort-values";
14
14
  import { useResearchCloudSession } from "../shared/research-cloud-session";
15
- import { cachedCotBoard, loadCotBoard, loadCotDetail } from "./client";
15
+ import { cachedCotBoard, loadCotBoard, loadCotDetail, loadCotPriceUnit } from "./client";
16
16
  import { COT_CLASSES, COT_FAMILIES, COT_MAJOR_CODES, COT_POSITIONS, COT_SCOPES, cotChartSeries, cotClass, cotContractUnitText, cotFamily, cotInteger, cotLegendValue, cotMarketName, cotPositions, cotPositionsText, cotScope, type CotScope } from "./model";
17
17
 
18
18
  const BOARD_COLUMNS: DataTableColumn[] = [
@@ -159,10 +159,14 @@ function CotDetail({ width, height, focused, code, family, positions, traderClas
159
159
  const session = useResearchCloudSession();
160
160
  const loader = useCallback(() => loadCotDetail(code, family, undefined, positions), [code, family, positions, session.requestKey]);
161
161
  const resource = useAsyncResource(loader, { clearOnError: isAccessDenied });
162
+ // The front price's unit joins the legend once the contract terms answer, or
163
+ // the catalog's after a short wait: the chart draws without waiting for it.
164
+ const unitLoader = useCallback(() => loadCotPriceUnit(code), [code]);
165
+ const priceUnit = useAsyncResource(unitLoader).data ?? null;
162
166
  const data = resource.data;
163
167
  const payload = data?.payload;
164
168
  const current = payload?.positions.find((row) => row.id === traderClass);
165
- const series = useMemo(() => data ? cotChartSeries(data.payload, traderClass, data.price, colors) : [], [data, traderClass, colors]);
169
+ const series = useMemo(() => data ? cotChartSeries(data.payload, traderClass, data.price, colors, priceUnit) : [], [data, traderClass, colors, priceUnit]);
166
170
  // The selected class row already carries the net and both ranks, and the
167
171
  // legend names the class it charts, so the detail opens on the chart. A rank
168
172
  // over a short history is a limitation, so it sits behind the warning.
@@ -1,8 +1,8 @@
1
1
  import { apiClient } from "../../../api-client";
2
+ import { sourceDidNotAnswer } from "../../../api-client/errors";
2
3
  import type { CpiBoardPayload, CpiRow } from "../../../api-client/cpi";
3
- import { ApiRequestError } from "../../../api-client/errors";
4
4
  import { createPluginCache } from "../../../data/plugin-cache";
5
- import { cachedCloudResource, loadCloudResource } from "../shared/cloud-resource";
5
+ import { cachedCloudResource, loadCloudResource, unavailableOnServer } from "../shared/cloud-resource";
6
6
 
7
7
  /** What the pane says while the server has no months to serve: before the backend ships, or before its first read. */
8
8
  export const CPI_NOT_AVAILABLE = "US consumer prices are not available yet.";
@@ -54,11 +54,12 @@ export async function fetchCpiBoard(client: Pick<typeof apiClient, "getCloudCpiB
54
54
  try {
55
55
  payload = await client.getCloudCpiBoard();
56
56
  } catch (error) {
57
- if (error instanceof ApiRequestError && (error.status === 404 || error.status === 503)) throw new Error(CPI_NOT_AVAILABLE);
58
- throw error;
57
+ throw unavailableOnServer(error, CPI_NOT_AVAILABLE, [404, 503]);
59
58
  }
60
59
  const board = validateCpiBoard(payload);
61
- if (board.status === "unavailable" || !board.release.period || !board.rows.length) throw new Error(CPI_NOT_AVAILABLE);
60
+ // The server's source did not answer this time: a saved copy answers, as it does a 5xx.
61
+ if (board.status === "unavailable") throw sourceDidNotAnswer(CPI_NOT_AVAILABLE);
62
+ if (!board.release.period || !board.rows.length) throw new Error(CPI_NOT_AVAILABLE);
62
63
  return board;
63
64
  }
64
65
 
@@ -1,6 +1,6 @@
1
1
  import type { CreditBoardPoint } from "../../../api-client/credit-boards";
2
2
  import type { SeriesDefinition, SeriesLoadResult } from "../../../series/types";
3
- import { cachedSeriesWarning } from "../shared/series-warning";
3
+ import { cachedSeriesNote } from "../shared/series-warning";
4
4
  import { loadCdxBoard, loadSovrBoard } from "./client";
5
5
 
6
6
  /** The five on-the-run 5Y index contracts the CDX tab lists; IG and iTraxx quote spread, HY and EM price. */
@@ -27,14 +27,13 @@ function boardPoints(
27
27
  rows: ReadonlyArray<{ id: string; points: readonly CreditBoardPoint[] }>,
28
28
  id: string,
29
29
  label: string,
30
- resource: { stale?: boolean; refreshError?: string | null },
30
+ resource: { stale?: boolean; refreshError?: string | null; savedAt?: number },
31
31
  ): SeriesLoadResult {
32
32
  const row = rows.find((entry) => entry.id === id);
33
33
  if (!row) throw new Error(`${label} is not on the board right now.`);
34
- const warning = cachedSeriesWarning(resource);
35
34
  return {
36
35
  points: row.points.map((point) => ({ date: point.date, value: point.level })),
37
- ...(warning ? { warning } : {}),
36
+ ...cachedSeriesNote(resource),
38
37
  };
39
38
  }
40
39
 
@@ -66,20 +66,28 @@ async function loadSeries(
66
66
  loader: CreditSeriesLoader,
67
67
  ) {
68
68
  const request = requestFor(definition.seriesId);
69
- const result = await loadCachedFredSeries(
70
- request,
71
- async () => {
72
- const data = trimHistory(await loader(definition.seriesId, {
73
- limit: HISTORY_LIMIT,
74
- sortOrder: "desc",
75
- }));
76
- // Reject incompatible source responses before they replace usable cache.
77
- // The shared cache can then retain the dated prior value as stale fallback.
78
- normalizeCreditSeries(definition, data);
79
- return data;
80
- },
81
- { force },
82
- );
69
+ let result: Awaited<ReturnType<typeof loadCachedFredSeries>>;
70
+ try {
71
+ result = await loadCachedFredSeries(
72
+ request,
73
+ async () => {
74
+ const data = trimHistory(await loader(definition.seriesId, {
75
+ limit: HISTORY_LIMIT,
76
+ sortOrder: "desc",
77
+ }));
78
+ // Reject incompatible source responses before they replace usable cache.
79
+ normalizeCreditSeries(definition, data);
80
+ return data;
81
+ },
82
+ { force },
83
+ );
84
+ } catch (error) {
85
+ // A refusal or a reply that fails validation is no saved copy's to answer: it is this row's
86
+ // error, beside the dated prior value already read, which stays as stale rather than vanish.
87
+ const kept = getCachedFredSeries(request, { allowExpired: true });
88
+ if (!kept) throw error;
89
+ return { row: normalizeCreditSeries(definition, kept.data, true), refreshError: error instanceof Error ? error.message : String(error) };
90
+ }
83
91
  return {
84
92
  row: normalizeCreditSeries(definition, result.data, result.stale),
85
93
  refreshError: result.refreshError,
@@ -31,10 +31,10 @@ export const creditHeadless: HeadlessPaneDefinition<"bundle"> = {
31
31
  const data = await fetchCreditDocuments(args.symbols[0]!, ctx.apiClient);
32
32
  const instrument = args.options.instrument ? await fetchCreditInstrument(data.symbol, String(args.options.instrument), ctx.apiClient) : null;
33
33
  const reason = instrument ? null : creditEmptyReason(data, args.symbols[0]!);
34
- // Nothing on file says why once, instead of four empty sections.
34
+ // Nothing on file says why once, as the report's one reason, instead of four empty sections and a generic "not covered".
35
35
  if (reason) {
36
- return { complete: false, errors: data.warnings, sections: [],
37
- metadata: { symbol: data.symbol, issuer: data.issuer, asOf: data.asOf, access: data.access, lockedRows: data.lockedRows, status: data.status, reason, notices: [reason] } };
36
+ return { complete: false, errors: data.warnings, sections: [], empty: reason,
37
+ metadata: { symbol: data.symbol, issuer: data.issuer, asOf: data.asOf, access: data.access, lockedRows: data.lockedRows, status: data.status, reason } };
38
38
  }
39
39
  return { complete: data.status === "available" && data.lockedRows === 0, errors: [...data.warnings, ...(data.lockedRows ? ["Full credit documents and history require Gloom Pro"] : [])],
40
40
  sections: instrument ? instrumentSections(data, instrument) : [capitalSection(data), covenantSection(data), maturitySection(data), controlSection(data)],
@@ -145,10 +145,10 @@ export async function fetchCryptoMarkets(
145
145
  try {
146
146
  return validateCryptoMarkets(await client.getCloudCryptoMarkets());
147
147
  } catch (error) {
148
- if (error instanceof ApiRequestError && error.status === 503) {
149
- throw new Error("Crypto prices are temporarily unavailable");
150
- }
151
- throw unavailableOnServer(error, "Crypto markets are not available yet.");
148
+ // Each keeps its status: a 503 passes with time and a saved copy answers it; a 404 does not.
149
+ throw error instanceof ApiRequestError && error.status === 503
150
+ ? unavailableOnServer(error, "Crypto prices are temporarily unavailable", [503])
151
+ : unavailableOnServer(error, "Crypto markets are not available yet.");
152
152
  }
153
153
  }
154
154
 
@@ -248,7 +248,7 @@ function validateStablecoinSupplyHistory(data: StablecoinSupplyHistoryPayload):
248
248
  }
249
249
 
250
250
  function supplyError(error: unknown, what: string): unknown {
251
- if (error instanceof ApiRequestError && error.status === 503) return new Error(`${what} is temporarily unavailable`);
251
+ if (error instanceof ApiRequestError && error.status === 503) return unavailableOnServer(error, `${what} is temporarily unavailable`, [503]);
252
252
  return unavailableOnServer(error, `${what} is not available yet.`);
253
253
  }
254
254
 
@@ -1,5 +1,5 @@
1
1
  import type { SeriesDefinition, SeriesPoint } from "../../../series/types";
2
- import { cachedSeriesWarning } from "../shared/series-warning";
2
+ import { cachedSeriesNote } from "../shared/series-warning";
3
3
  import { loadDefiTvl, loadStablecoinSupply, loadStablecoinSupplyHistory } from "./client";
4
4
 
5
5
  const DAY_MS = 86_400_000;
@@ -29,7 +29,7 @@ const STABLECOIN_SUPPLY_SERIES: readonly SeriesDefinition[] = [
29
29
  const resource = await loadStablecoinSupply();
30
30
  const history = resource.payload.history;
31
31
  if (!history) throw new Error("Stablecoin supply is not available yet.");
32
- return { points: dailyPoints(history.start, history.total), warning: cachedSeriesWarning(resource) };
32
+ return { points: dailyPoints(history.start, history.total), ...cachedSeriesNote(resource) };
33
33
  },
34
34
  },
35
35
  {
@@ -45,7 +45,7 @@ const STABLECOIN_SUPPLY_SERIES: readonly SeriesDefinition[] = [
45
45
  const resource = await loadStablecoinSupplyHistory(stablecoinSymbol(ticker!));
46
46
  const history = resource.payload.history;
47
47
  if (!history) throw new Error(`${resource.payload.symbol} supply history is not available yet.`);
48
- return { points: dailyPoints(history.start, history.values), warning: cachedSeriesWarning(resource) };
48
+ return { points: dailyPoints(history.start, history.values), ...cachedSeriesNote(resource) };
49
49
  },
50
50
  },
51
51
  ];
@@ -63,7 +63,7 @@ const DEFI_TVL_SERIES: SeriesDefinition = {
63
63
  const resource = await loadDefiTvl();
64
64
  const history = resource.payload.history;
65
65
  if (!history) throw new Error("DeFi TVL is not available yet.");
66
- return { points: dailyPoints(history.start, history.total), warning: cachedSeriesWarning(resource) };
66
+ return { points: dailyPoints(history.start, history.total), ...cachedSeriesNote(resource) };
67
67
  },
68
68
  };
69
69
 
@@ -1,6 +1,6 @@
1
1
  import type { DebtHistoryPoint } from "../../../api-client/debt-maturities";
2
2
  import type { SeriesDefinition } from "../../../series/types";
3
- import { cachedSeriesWarning } from "../shared/series-warning";
3
+ import { cachedSeriesNote } from "../shared/series-warning";
4
4
  import { loadDebtMaturities } from "./client";
5
5
 
6
6
  type HistoryField = "totalPrincipal" | "next12MonthsShare" | "next3YearsShare";
@@ -36,10 +36,10 @@ export const DEBT_MATURITY_SERIES: readonly SeriesDefinition[] = FIELDS.map(({ i
36
36
  // An amount stays in the unit it is listed in: a filing in another currency is a gap, not a converted figure.
37
37
  const listed = (point: DebtHistoryPoint) => unit !== "USD" || point.currency === "USD";
38
38
  const other = resource.payload.history.find((point) => !listed(point))?.currency;
39
- const warning = other ? `Filings reported in ${other} are left out.` : cachedSeriesWarning(resource);
40
39
  return {
41
40
  points: resource.payload.history.map((point) => ({ date: point.asOf, value: listed(point) ? point[field] : null })),
42
- ...(warning ? { warning } : {}),
41
+ ...cachedSeriesNote(resource),
42
+ ...(other ? { warning: `Filings reported in ${other} are left out.` } : {}),
43
43
  };
44
44
  },
45
45
  }));
@@ -1,8 +1,8 @@
1
1
  import { apiClient } from "../../../api-client";
2
+ import { sourceDidNotAnswer } from "../../../api-client/errors";
2
3
  import type { DoeBoardPayload, DoeSeriesRow } from "../../../api-client/doe";
3
- import { ApiRequestError } from "../../../api-client/errors";
4
4
  import { createPluginCache } from "../../../data/plugin-cache";
5
- import { cachedCloudResource, loadCloudResource } from "../shared/cloud-resource";
5
+ import { cachedCloudResource, loadCloudResource, unavailableOnServer } from "../shared/cloud-resource";
6
6
 
7
7
  /** What the pane says while the server has no EIA weeks to serve: before the backend ships, or before its first read. */
8
8
  export const DOE_NOT_AVAILABLE = "Oil and gas inventories are not available yet.";
@@ -65,11 +65,12 @@ export async function fetchDoeBoard(client: Pick<typeof apiClient, "getCloudDoeB
65
65
  try {
66
66
  payload = await client.getCloudDoeBoard();
67
67
  } catch (error) {
68
- if (error instanceof ApiRequestError && (error.status === 404 || error.status === 503)) throw new Error(DOE_NOT_AVAILABLE);
69
- throw error;
68
+ throw unavailableOnServer(error, DOE_NOT_AVAILABLE, [404, 503]);
70
69
  }
71
70
  const board = validateDoeBoard(payload);
72
- if (board.status === "unavailable" || !board.series.some((row) => row.value != null)) throw new Error(DOE_NOT_AVAILABLE);
71
+ // The server's source did not answer this time: a saved copy answers, as it does a 5xx.
72
+ if (board.status === "unavailable") throw sourceDidNotAnswer(DOE_NOT_AVAILABLE);
73
+ if (!board.series.some((row) => row.value != null)) throw new Error(DOE_NOT_AVAILABLE);
73
74
  return board;
74
75
  }
75
76
 
@@ -1,6 +1,6 @@
1
1
  import type { DoeSeriesRow, DoeTab } from "../../../api-client/doe";
2
2
  import type { HeadlessPaneDefinition } from "../../../types/plugin";
3
- import { fetchDoeBoard } from "./client";
3
+ import { loadDoeBoard } from "./client";
4
4
  import {
5
5
  DOE_TABS,
6
6
  DOE_UNIT_LABEL,
@@ -61,7 +61,8 @@ export function doeHeadless(defaultTab: DoeTab | "all"): HeadlessPaneDefinition<
61
61
  "Five-year ranges read earlier years on the same calendar day.", "A range holds fewer than five years where a series is younger."] },
62
62
  describe: "EIA weekly petroleum and natural gas storage",
63
63
  async load(args) {
64
- const data = await fetchDoeBoard();
64
+ // Read afresh; a failed read answers from the board an earlier one saved, which the report says it is.
65
+ const data = (await loadDoeBoard(true)).payload;
65
66
  // Each tab is dated by its own EIA report (petroleum Wednesday, gas Thursday);
66
67
  // once the next one is a day overdue the tables have missed it. The basis
67
68
  // names what Vs 1Y and Vs 5Y avg are measured against.
@@ -13,8 +13,10 @@ import {
13
13
  usePaneTabs,
14
14
  type ChartTableChart,
15
15
  type DataTableColumn,
16
+ type PaneFooterSegment,
16
17
  } from "../../../components";
17
18
  import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
19
+ import { savedCopyFooterSegment } from "../../../components/layout/pane/footer";
18
20
  import { useAsyncResource, useAutoRefresh, usePaneSettingValue, usePluginPaneState } from "../../../public/react";
19
21
  import { usePaneInstance } from "../../../state/app/context";
20
22
  import { useThemeColors } from "../../../theme/theme-context";
@@ -136,13 +138,21 @@ export function DoePane({ width, height, focused }: PaneProps) {
136
138
  const { strip, rows: tabRows } = usePaneTabs({ tabs: [...DOE_TABS], activeValue: tab, onSelect: setTab, focused, dense: true });
137
139
  useAutoRefresh(resource.updatedAt, resource.load);
138
140
  usePaneRefreshKey(() => void resource.reload(), { focused });
141
+ // A refresh that failed shows the board an earlier read saved, and says when that copy is from.
142
+ const savedAt = resource.data?.savedAt ?? null;
139
143
  usePaneNoticeFooter({ registrationId: "doe:notices", focused,
140
- notices: [...(data?.gaps ?? []), ...(resource.data?.refreshError ? [resource.data.refreshError] : [])] });
144
+ notices: [...(data?.gaps ?? []), ...(resource.data?.refreshError && savedAt == null ? [resource.data.refreshError] : [])] });
141
145
  // What Vs 1Y and Vs 5Y are measured against on this tab: the EIA's published figures for gas storage.
142
146
  const basis = useMemo(() => data ? doeComparisonBasis(doeRows(data, tab)) : null, [data, tab]);
143
- const info = useMemo(() => basis ? [{ id: "basis", parts: [{ text: basis, tone: "muted" as const }] }] : undefined, [basis]);
147
+ const info = useMemo(() => {
148
+ const segments: PaneFooterSegment[] = [
149
+ ...(basis ? [{ id: "basis", parts: [{ text: basis, tone: "muted" as const }] }] : []),
150
+ ...(savedAt != null ? [savedCopyFooterSegment("saved-copy", savedAt)] : []),
151
+ ];
152
+ return segments.length ? segments : undefined;
153
+ }, [basis, savedAt]);
144
154
  usePaneStatusFooter({ registrationId: "doe", loading: resource.loading, error: notAvailable ? null : resource.error,
145
- stale: !!data && resource.data?.stale, info });
155
+ stale: !!data && !!resource.data?.stale && savedAt == null, info });
146
156
  return (
147
157
  <Box width={width} height={height} flexDirection="column">
148
158
  {strip}
@@ -2,7 +2,7 @@ import { apiClient } from "../../../api-client";
2
2
  import type { DoePricesPayload } from "../../../api-client/doe";
3
3
  import { createPluginCache } from "../../../data/plugin-cache";
4
4
  import type { SeriesDefinition } from "../../../series/types";
5
- import { cachedSeriesWarning } from "../shared/series-warning";
5
+ import { cachedSeriesNote } from "../shared/series-warning";
6
6
  import { loadCloudResource, unavailableOnServer } from "../shared/cloud-resource";
7
7
 
8
8
  /**
@@ -73,10 +73,9 @@ export const DOE_PRICE_SERIES: readonly SeriesDefinition[] = PRICE_SERIES.map((s
73
73
  const resource = await loadCloudResource(doePriceCache, "prices", () => fetchDoePrices(), { validate: validatePrices });
74
74
  const row = resource.payload.series.find((entry) => entry.id === series.id);
75
75
  if (!row?.points.length) throw new Error(`${series.name} is not available yet.`);
76
- const warning = cachedSeriesWarning(resource);
77
76
  return {
78
77
  points: row.points.map(([date, value]) => ({ date, value })),
79
- ...(warning ? { warning } : {}),
78
+ ...cachedSeriesNote(resource),
80
79
  // Cited as the EIA series it is, not as the route that served it.
81
80
  attribution: { source: `EIA series ${series.key}`, publisher: "EIA" },
82
81
  };
@@ -1,6 +1,6 @@
1
1
  import type { DoeSeriesRow, DoeUnit } from "../../../api-client/doe";
2
2
  import type { SeriesDefinition, SeriesPoint } from "../../../series/types";
3
- import { cachedSeriesWarning } from "../shared/series-warning";
3
+ import { cachedSeriesNote } from "../shared/series-warning";
4
4
  import { loadDoeBoard } from "./client";
5
5
  import { DOE_SERIES_OPTIONS } from "./model";
6
6
  import { DOE_PRICE_SERIES } from "./prices";
@@ -50,14 +50,14 @@ const DOE_SERIES: readonly SeriesDefinition[] = DOE_SERIES_OPTIONS.map((option):
50
50
  unit: UNITS[unit].unit,
51
51
  frequency: "weekly",
52
52
  group: "Energy",
53
- tags: gas ? ["natural gas", "storage", "inventories"] : ["oil", "petroleum", "inventories", "US"],
53
+ // Jet fuel is kerosene, the word a reader may search it by.
54
+ tags: gas ? ["natural gas", "storage", "inventories"] : ["oil", "petroleum", "inventories", "US", ...option.value === "jet" ? ["kerosene"] : []],
54
55
  publisher: "EIA",
55
56
  load: async () => {
56
57
  const resource = await loadDoeBoard();
57
58
  const row = resource.payload.series.find((entry) => entry.id === option.value);
58
59
  if (!row) throw new Error(`${option.label} is not in this week's report.`);
59
- const warning = cachedSeriesWarning(resource);
60
- return { points: doeRowPoints(row), ...(warning ? { warning } : {}) };
60
+ return { points: doeRowPoints(row), ...cachedSeriesNote(resource) };
61
61
  },
62
62
  };
63
63
  });
@@ -19,6 +19,8 @@ import {
19
19
  type SectionedRow,
20
20
  } from "../../../components";
21
21
  import { handleRefreshKey } from "../../../components/data-table/table-pane";
22
+ import { savedCopyFooterInfo } from "../../../components/layout/pane/footer";
23
+ import { isPermanentClientError } from "../../../api-client/errors";
22
24
  import { TABLE_COLUMN_GAP, tableColumnWidth } from "../../../components/ui/table-layout";
23
25
  import { useAsyncResource } from "../../../react/async-resource";
24
26
  import { useAgeClock, useAutoRefresh } from "../../../react/auto-refresh";
@@ -162,6 +164,8 @@ export function UpcomingCallsView({ focused, width, height, view, renderCompany
162
164
  }, [range]);
163
165
  const resource = useAsyncResource(entitled ? loader : null, {
164
166
  keepPreviousData: true,
167
+ // A refusal asking again will not change takes the calendar off screen, for its wall or its error.
168
+ clearOnError: isPermanentClientError,
165
169
  initialData: () => cachedUpcomingCalls(range),
166
170
  });
167
171
  const data = resource.data;
@@ -204,12 +208,16 @@ export function UpcomingCallsView({ focused, width, height, view, renderCompany
204
208
  const proRequired = !signInRequired && !verificationRequired && (!access.hasProAccess || (!data && refused === 402));
205
209
 
206
210
  const hints = useMemo<PaneHint[]>(() => [{ id: "search", key: "/", label: "search", onPress: focusSearch }], [focusSearch]);
211
+ // A refresh that failed shows the calendar an earlier read saved, and says when that copy is from.
212
+ const savedAt = data?.savedAt ?? null;
213
+ const savedInfo = useMemo(() => savedCopyFooterInfo([savedAt]), [savedAt]);
207
214
  usePaneStatusFooter({
208
215
  registrationId: "earnings-calls:calendar",
209
216
  enabled: !detailOpen && entitled,
210
217
  loading: resource.loading && !!data,
211
- error: data ? data.refreshError ?? resource.error : null,
212
- stale: !!data && (data.stale || !!resource.error),
218
+ error: data ? (savedAt == null ? data.refreshError : null) ?? resource.error : null,
219
+ stale: !!data && savedAt == null && (data.stale || !!resource.error),
220
+ info: savedInfo,
213
221
  hints,
214
222
  });
215
223
  // A calendar cut at the server's limit says so, rather than end the week early.