@gloom-sh/terminal 0.19.1 → 0.19.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (554) hide show
  1. package/README.md +7 -1
  2. package/package.json +2 -1
  3. package/patches/electrobun@1.18.1.patch +108 -1
  4. package/src/account/upgrade-dialog.tsx +4 -23
  5. package/src/api-client/coalesce.ts +118 -0
  6. package/src/api-client/credit-boards.ts +13 -0
  7. package/src/api-client/crypto-etf-flows.ts +6 -0
  8. package/src/api-client/crypto-markets.ts +40 -0
  9. package/src/api-client/data.ts +201 -14
  10. package/src/api-client/equity-screener.ts +10 -0
  11. package/src/api-client/errors.ts +58 -0
  12. package/src/api-client/gpu.ts +29 -0
  13. package/src/api-client/index.ts +5 -2
  14. package/src/api-client/plan-access.ts +10 -1
  15. package/src/api-client/plan-rules.ts +35 -0
  16. package/src/api-client/research-activity.ts +1 -32
  17. package/src/api-client/types.ts +21 -1
  18. package/src/app/attention-notice.tsx +94 -0
  19. package/src/app/runtime/startup.ts +3 -1
  20. package/src/app.tsx +23 -1
  21. package/src/architecture/core-builtin-allowlist.json +2 -1
  22. package/src/capabilities/chart-series.ts +2 -1
  23. package/src/capabilities/factories.ts +1 -0
  24. package/src/cli/ascii-output.ts +42 -0
  25. package/src/cli/base-converter.ts +10 -2
  26. package/src/cli/command-options.ts +16 -1
  27. package/src/cli/commands/crypto-hints.ts +23 -4
  28. package/src/cli/commands/market.ts +101 -27
  29. package/src/cli/commands/news-search.ts +24 -5
  30. package/src/cli/commands/overview.ts +14 -10
  31. package/src/cli/commands/search.ts +84 -25
  32. package/src/cli/commands/system.ts +22 -30
  33. package/src/cli/commands/ticker.ts +49 -9
  34. package/src/cli/commands/version.ts +33 -0
  35. package/src/cli/context.ts +6 -9
  36. package/src/cli/data-dir.ts +7 -0
  37. package/src/cli/desktop-pane-shot-api-policy.ts +85 -0
  38. package/src/cli/desktop-pane-shot-browser.ts +320 -0
  39. package/src/cli/desktop-pane-shot-routes.ts +26 -1
  40. package/src/cli/desktop-pane-shot.ts +196 -256
  41. package/src/cli/early-command.ts +42 -0
  42. package/src/cli/earnings-calendar.ts +9 -0
  43. package/src/cli/entry.ts +19 -13
  44. package/src/cli/errors.ts +12 -2
  45. package/src/cli/glossary.ts +2 -1
  46. package/src/cli/help.ts +1 -1
  47. package/src/cli/history-export.ts +22 -2
  48. package/src/cli/history-rows.ts +189 -11
  49. package/src/cli/index.ts +18 -1
  50. package/src/cli/known-gaps.ts +51 -5
  51. package/src/cli/listing-arg.ts +63 -10
  52. package/src/cli/options.ts +1 -1
  53. package/src/cli/output-file.ts +60 -13
  54. package/src/cli/pane-functions/catalog.ts +14 -3
  55. package/src/cli/pane-functions/data.ts +1 -1
  56. package/src/cli/pane-functions/dom.ts +45 -0
  57. package/src/cli/pane-functions/freshness.ts +46 -9
  58. package/src/cli/pane-functions/function-help.ts +17 -9
  59. package/src/cli/pane-functions/headless.ts +22 -14
  60. package/src/cli/pane-functions/index.ts +6 -2
  61. package/src/cli/pane-functions/options.ts +6 -2
  62. package/src/cli/pane-functions/resolver.ts +27 -5
  63. package/src/cli/pane-functions/screenshot-assessment.ts +33 -0
  64. package/src/cli/pane-functions/screenshot-bridge.ts +94 -13
  65. package/src/cli/pane-functions/screenshot-http.ts +70 -0
  66. package/src/cli/pane-functions/screenshot-render.ts +19 -5
  67. package/src/cli/quote-answer.ts +49 -6
  68. package/src/cli/registry.ts +41 -13
  69. package/src/cli/result.ts +12 -6
  70. package/src/cli/ticker-resolution.ts +2 -1
  71. package/src/cli/types.ts +2 -0
  72. package/src/components/chart/composite/format.ts +3 -0
  73. package/src/components/chart-table/header.tsx +1 -1
  74. package/src/components/chart-table/layout.ts +28 -11
  75. package/src/components/command-bar/action-runtime.ts +4 -0
  76. package/src/components/command-bar/commands/direct/index.ts +18 -33
  77. package/src/components/command-bar/commands/direct/runtime.ts +5 -0
  78. package/src/components/command-bar/commands/registry.ts +10 -1
  79. package/src/components/command-bar/routes/root/command-items.ts +9 -4
  80. package/src/components/command-bar/routes/root/runtime.ts +51 -6
  81. package/src/components/command-bar/selection-runtime.ts +12 -29
  82. package/src/components/command-bar/surface/index.tsx +9 -3
  83. package/src/components/company-logo.tsx +2 -1
  84. package/src/components/layout/detached-pane-shell.tsx +2 -2
  85. package/src/components/layout/header.tsx +10 -5
  86. package/src/components/layout/market-summary.ts +25 -7
  87. package/src/components/layout/pane/footer/index.tsx +8 -2
  88. package/src/components/layout/pane/footer/model.ts +73 -6
  89. package/src/components/layout/pane/header.tsx +2 -2
  90. package/src/components/layout/pane/status-footer.ts +2 -1
  91. package/src/components/market-board/index.tsx +3 -2
  92. package/src/components/onboarding/desks-step.tsx +12 -4
  93. package/src/components/onboarding/first-run-workspace.ts +81 -4
  94. package/src/components/onboarding/onboarding-frame.tsx +4 -2
  95. package/src/components/onboarding/onboarding-keyboard.ts +8 -5
  96. package/src/components/onboarding/onboarding-wizard.tsx +60 -26
  97. package/src/components/onboarding/portfolio-step/positions-panel.tsx +7 -2
  98. package/src/components/onboarding/run-setup-again.ts +24 -0
  99. package/src/components/onboarding/use-onboarding-progress.ts +33 -15
  100. package/src/components/onboarding/use-onboarding-upgrade.ts +2 -6
  101. package/src/components/onboarding/wizard-model.ts +57 -3
  102. package/src/components/onboarding/wizard-positions.ts +1 -1
  103. package/src/components/ui/delayed-mark.tsx +56 -0
  104. package/src/components/ui/display.tsx +16 -5
  105. package/src/components/ui/external-link.tsx +3 -6
  106. package/src/components/ui/icon.tsx +16 -3
  107. package/src/components/ui/logo-mark.tsx +8 -5
  108. package/src/components/ui/stat-grid.tsx +263 -19
  109. package/src/core/app-services.ts +11 -2
  110. package/src/data/app-persistence.ts +9 -7
  111. package/src/data/config/store/node.ts +11 -5
  112. package/src/data/config/store/normalize.ts +16 -3
  113. package/src/data/plugin-cache.ts +15 -0
  114. package/src/data/plugin-state-store.ts +81 -24
  115. package/src/data/profile-permissions.ts +47 -0
  116. package/src/data/resource-store.ts +165 -90
  117. package/src/data/session-store.ts +31 -8
  118. package/src/data/sqlite/access.ts +324 -0
  119. package/src/data/sqlite/checkpointer.ts +285 -0
  120. package/src/data/sqlite/database.ts +80 -4
  121. package/src/data/sqlite/retry.ts +50 -9
  122. package/src/data/ticker-store.ts +57 -23
  123. package/src/docs/snapshot.json +191 -78
  124. package/src/i18n/es.ts +18 -2
  125. package/src/i18n/ja.ts +18 -2
  126. package/src/i18n/ko.ts +18 -2
  127. package/src/i18n/zh-cn.ts +18 -2
  128. package/src/i18n/zh-tw.ts +18 -2
  129. package/src/layout/desk-keys.ts +4 -1
  130. package/src/layout/desks.ts +38 -5
  131. package/src/layout-marketplace/gallery-desktop.tsx +45 -16
  132. package/src/market-data/batched-reads.ts +16 -0
  133. package/src/market-data/coordinator/auxiliary.ts +4 -2
  134. package/src/market-data/coordinator/index.ts +11 -2
  135. package/src/market-data/futures-board.ts +1 -1
  136. package/src/market-data/futures-contract.ts +3 -1
  137. package/src/market-data/market/index-level.ts +6 -4
  138. package/src/market-data/published-apac-sessions.ts +6 -0
  139. package/src/market-data/realized-volatility.ts +4 -2
  140. package/src/news/aggregator.ts +7 -4
  141. package/src/news/news-model.ts +11 -0
  142. package/src/news/story-id.ts +22 -0
  143. package/src/news/ticker-symbols.ts +17 -1
  144. package/src/notifications/app-notifier.ts +35 -14
  145. package/src/plugins/auto-update-node.ts +10 -6
  146. package/src/plugins/builtin/account-management/form-components.tsx +1 -1
  147. package/src/plugins/builtin/account-management/index.tsx +2 -3
  148. package/src/plugins/builtin/account-management/model.ts +1 -1
  149. package/src/plugins/builtin/alerts/cli-forms.ts +15 -1
  150. package/src/plugins/builtin/alerts/events-pane.tsx +7 -5
  151. package/src/plugins/builtin/alerts/events.ts +10 -2
  152. package/src/plugins/builtin/alerts/index.tsx +5 -5
  153. package/src/plugins/builtin/alerts/research-builder.ts +5 -1
  154. package/src/plugins/builtin/alerts/research-rules.ts +18 -0
  155. package/src/plugins/builtin/alerts/targets.ts +4 -1
  156. package/src/plugins/builtin/attention/headless.ts +1 -1
  157. package/src/plugins/builtin/attention/index.tsx +2 -4
  158. package/src/plugins/builtin/attention/listing.ts +1 -1
  159. package/src/plugins/builtin/attention/series.ts +1 -1
  160. package/src/plugins/builtin/awards/index.tsx +1 -2
  161. package/src/plugins/builtin/backtest/client.ts +1 -0
  162. package/src/plugins/builtin/backtest/index.tsx +1 -2
  163. package/src/plugins/builtin/backtest/rules.ts +3 -1
  164. package/src/plugins/builtin/backtest/window.ts +8 -7
  165. package/src/plugins/builtin/broker-manager/detail.tsx +16 -3
  166. package/src/plugins/builtin/broker-manager/index.tsx +1 -1
  167. package/src/plugins/builtin/buildout/cells.tsx +5 -0
  168. package/src/plugins/builtin/buildout/company/flow.tsx +367 -0
  169. package/src/plugins/builtin/buildout/company/id.ts +2 -0
  170. package/src/plugins/builtin/buildout/company/model.ts +247 -0
  171. package/src/plugins/builtin/buildout/company/pane.tsx +317 -0
  172. package/src/plugins/builtin/buildout/data-runtime.ts +31 -10
  173. package/src/plugins/builtin/buildout/facilities.ts +309 -0
  174. package/src/plugins/builtin/buildout/format.ts +9 -0
  175. package/src/plugins/builtin/buildout/index.tsx +51 -5
  176. package/src/plugins/builtin/buildout/model/index.ts +62 -10
  177. package/src/plugins/builtin/buildout/model/normalizers.ts +5 -0
  178. package/src/plugins/builtin/buildout/model/types.ts +11 -2
  179. package/src/plugins/builtin/buildout/pane/index.tsx +91 -20
  180. package/src/plugins/builtin/buildout/pane/status.tsx +1 -1
  181. package/src/plugins/builtin/buildout/pane/themes.tsx +110 -0
  182. package/src/plugins/builtin/buildout/table-model.ts +26 -2
  183. package/src/plugins/builtin/builtin-plugin-meta.ts +16 -0
  184. package/src/plugins/builtin/cds/headless.ts +10 -2
  185. package/src/plugins/builtin/cds/model.ts +7 -3
  186. package/src/plugins/builtin/central-bank-rates/index.tsx +1 -2
  187. package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -1
  188. package/src/plugins/builtin/central-bank-rates/series.ts +2 -3
  189. package/src/plugins/builtin/changelog/index.tsx +3 -2
  190. package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
  191. package/src/plugins/builtin/chart-composer/headless-schema.ts +10 -0
  192. package/src/plugins/builtin/chart-composer/headless.ts +88 -21
  193. package/src/plugins/builtin/chart-composer/index.tsx +2 -3
  194. package/src/plugins/builtin/chart-composer/pane.tsx +30 -2
  195. package/src/plugins/builtin/chart-composer/series-expression.ts +3 -2
  196. package/src/plugins/builtin/chart-composer/session-range.ts +5 -2
  197. package/src/plugins/builtin/chart-composer/studies.ts +1 -1
  198. package/src/plugins/builtin/chat/controller/index.ts +1 -1
  199. package/src/plugins/builtin/chat/controller/messages.ts +15 -4
  200. package/src/plugins/builtin/chat/ghost-user.ts +8 -0
  201. package/src/plugins/builtin/chat/index.tsx +5 -5
  202. package/src/plugins/builtin/clinical-trials/index.tsx +1 -2
  203. package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
  204. package/src/plugins/builtin/cloud/index.tsx +1 -2
  205. package/src/plugins/builtin/cloud/plugin.tsx +6 -4
  206. package/src/plugins/builtin/cloud/team/module.tsx +1 -1
  207. package/src/plugins/builtin/cloud/thesis/module.tsx +1 -1
  208. package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +24 -1
  209. package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +39 -15
  210. package/src/plugins/builtin/comment-letters/index.tsx +1 -2
  211. package/src/plugins/builtin/commodity-balances/client.ts +4 -1
  212. package/src/plugins/builtin/commodity-balances/index.tsx +3 -3
  213. package/src/plugins/builtin/commodity-balances/pane.tsx +2 -2
  214. package/src/plugins/builtin/congress-trades/aggregates.ts +13 -2
  215. package/src/plugins/builtin/congress-trades/detail.tsx +20 -4
  216. package/src/plugins/builtin/congress-trades/headless.ts +2 -0
  217. package/src/plugins/builtin/congress-trades/model.ts +92 -64
  218. package/src/plugins/builtin/congress-trades/table.tsx +14 -2
  219. package/src/plugins/builtin/connections/index.ts +2 -3
  220. package/src/plugins/builtin/cot/client.ts +28 -2
  221. package/src/plugins/builtin/cot/headless.ts +4 -1
  222. package/src/plugins/builtin/cot/index.tsx +5 -4
  223. package/src/plugins/builtin/cot/model.ts +58 -7
  224. package/src/plugins/builtin/cot/pane.tsx +6 -2
  225. package/src/plugins/builtin/cpi/client.ts +6 -5
  226. package/src/plugins/builtin/cpi/index.tsx +1 -2
  227. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  228. package/src/plugins/builtin/credit-boards/client.ts +7 -2
  229. package/src/plugins/builtin/credit-boards/headless.ts +25 -12
  230. package/src/plugins/builtin/credit-boards/model.ts +29 -0
  231. package/src/plugins/builtin/credit-boards/series.ts +3 -4
  232. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +9 -4
  233. package/src/plugins/builtin/credit-conditions/client.ts +22 -14
  234. package/src/plugins/builtin/crypto-board/bitcoin-network.ts +143 -0
  235. package/src/plugins/builtin/crypto-board/client.ts +5 -5
  236. package/src/plugins/builtin/crypto-board/headless.ts +4 -3
  237. package/src/plugins/builtin/crypto-board/index.tsx +4 -2
  238. package/src/plugins/builtin/crypto-board/series.ts +6 -4
  239. package/src/plugins/builtin/crypto-etf-flows/client.ts +9 -2
  240. package/src/plugins/builtin/crypto-etf-flows/headless.ts +61 -6
  241. package/src/plugins/builtin/crypto-etf-flows/index.tsx +1 -2
  242. package/src/plugins/builtin/crypto-etf-flows/model.ts +14 -1
  243. package/src/plugins/builtin/crypto-etf-flows/pane.tsx +8 -5
  244. package/src/plugins/builtin/debt-maturities/headless.ts +4 -3
  245. package/src/plugins/builtin/debt-maturities/model.ts +17 -1
  246. package/src/plugins/builtin/debt-maturities/pane.tsx +18 -4
  247. package/src/plugins/builtin/debt-maturities/series.ts +3 -3
  248. package/src/plugins/builtin/debug/index.tsx +1 -1
  249. package/src/plugins/builtin/dividend-yield/client.ts +32 -2
  250. package/src/plugins/builtin/dividend-yield/headless.ts +58 -33
  251. package/src/plugins/builtin/dividend-yield/index.tsx +37 -2
  252. package/src/plugins/builtin/dividend-yield/pane.tsx +32 -13
  253. package/src/plugins/builtin/dividend-yield/ranking-reads.ts +73 -0
  254. package/src/plugins/builtin/dividend-yield/ranking-view.tsx +163 -0
  255. package/src/plugins/builtin/dividend-yield/ranking.ts +1 -1
  256. package/src/plugins/builtin/dividend-yield/view.ts +21 -0
  257. package/src/plugins/builtin/docs/index.tsx +2 -3
  258. package/src/plugins/builtin/doe/client.ts +6 -5
  259. package/src/plugins/builtin/doe/headless.ts +16 -3
  260. package/src/plugins/builtin/doe/index.tsx +5 -4
  261. package/src/plugins/builtin/doe/prices.ts +2 -3
  262. package/src/plugins/builtin/doe/series.ts +4 -4
  263. package/src/plugins/builtin/earnings/board.tsx +2 -1
  264. package/src/plugins/builtin/earnings/history.tsx +2 -1
  265. package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
  266. package/src/plugins/builtin/earnings-calls/data.ts +19 -11
  267. package/src/plugins/builtin/earnings-calls/pane.tsx +27 -14
  268. package/src/plugins/builtin/earnings-ripple/client.ts +32 -7
  269. package/src/plugins/builtin/earnings-ripple/index.tsx +1 -2
  270. package/src/plugins/builtin/econ/calendar-model.ts +29 -6
  271. package/src/plugins/builtin/econ/detail-view.tsx +16 -2
  272. package/src/plugins/builtin/econ-statistics/index.tsx +1 -2
  273. package/src/plugins/builtin/equity-screener/client.ts +1 -1
  274. package/src/plugins/builtin/equity-screener/headless.ts +1 -0
  275. package/src/plugins/builtin/equity-screener/ideas.ts +30 -1
  276. package/src/plugins/builtin/equity-screener/index.tsx +1 -2
  277. package/src/plugins/builtin/equity-screener/model.ts +28 -4
  278. package/src/plugins/builtin/equity-screener/pane.tsx +5 -0
  279. package/src/plugins/builtin/equity-screener/screen-check.ts +23 -1
  280. package/src/plugins/builtin/estimate-revisions/index.tsx +1 -2
  281. package/src/plugins/builtin/executives/data.ts +6 -1
  282. package/src/plugins/builtin/executives/pane.tsx +9 -21
  283. package/src/plugins/builtin/exposure/holdings.ts +35 -13
  284. package/src/plugins/builtin/exposure/index.tsx +1 -2
  285. package/src/plugins/builtin/fear-greed/cache.ts +4 -0
  286. package/src/plugins/builtin/fear-greed/cli.ts +44 -0
  287. package/src/plugins/builtin/fear-greed/index.tsx +5 -53
  288. package/src/plugins/builtin/fear-greed/pane.tsx +2 -1
  289. package/src/plugins/builtin/fear-greed/series.ts +6 -4
  290. package/src/plugins/builtin/fixed-income/index.tsx +1 -2
  291. package/src/plugins/builtin/futures/board-reads.ts +25 -0
  292. package/src/plugins/builtin/futures/coalesced-bump.ts +44 -0
  293. package/src/plugins/builtin/futures/index.tsx +15 -0
  294. package/src/plugins/builtin/futures/model.ts +14 -0
  295. package/src/plugins/builtin/futures/use-board-terms.ts +22 -15
  296. package/src/plugins/builtin/futures/use-front-returns.ts +20 -14
  297. package/src/plugins/builtin/futures-curve/headless.ts +6 -1
  298. package/src/plugins/builtin/futures-curve/index.tsx +1 -2
  299. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  300. package/src/plugins/builtin/gpu/board.tsx +51 -12
  301. package/src/plugins/builtin/gpu/client.ts +19 -3
  302. package/src/plugins/builtin/gpu/equities.tsx +5 -2
  303. package/src/plugins/builtin/gpu/headless.ts +14 -2
  304. package/src/plugins/builtin/gpu/index.tsx +1 -2
  305. package/src/plugins/builtin/gpu/model.ts +20 -2
  306. package/src/plugins/builtin/gpu/pane.tsx +1 -1
  307. package/src/plugins/builtin/gpu/series.ts +2 -3
  308. package/src/plugins/builtin/help/index.tsx +1 -1
  309. package/src/plugins/builtin/holders/beneficial-view.tsx +1 -2
  310. package/src/plugins/builtin/holders/index-holders.ts +24 -12
  311. package/src/plugins/builtin/holders/index.tsx +2 -3
  312. package/src/plugins/builtin/ipo-calendar/index.tsx +1 -2
  313. package/src/plugins/builtin/iv-history/index.tsx +1 -2
  314. package/src/plugins/builtin/jodi/client.ts +4 -1
  315. package/src/plugins/builtin/jodi/index.tsx +1 -2
  316. package/src/plugins/builtin/kelly-sizer/index.tsx +1 -2
  317. package/src/plugins/builtin/layout-manager/index.tsx +9 -8
  318. package/src/plugins/builtin/macro-day/pane.tsx +4 -2
  319. package/src/plugins/builtin/market-halts/index.tsx +1 -2
  320. package/src/plugins/builtin/market-heatmap/index.tsx +11 -2
  321. package/src/plugins/builtin/market-movers/client.ts +1 -0
  322. package/src/plugins/builtin/market-movers/index.tsx +16 -5
  323. package/src/plugins/builtin/market-movers/screener.ts +23 -6
  324. package/src/plugins/builtin/market-movers/session-body.tsx +6 -2
  325. package/src/plugins/builtin/market-valuation/index.tsx +1 -2
  326. package/src/plugins/builtin/members/index.tsx +1 -2
  327. package/src/plugins/builtin/money-markets/index.tsx +1 -2
  328. package/src/plugins/builtin/money-markets/pane.tsx +2 -1
  329. package/src/plugins/builtin/money-markets/series.ts +3 -5
  330. package/src/plugins/builtin/news/headless.ts +7 -0
  331. package/src/plugins/builtin/news/wire/index.tsx +1 -1
  332. package/src/plugins/builtin/news/wire/news/pop-out.tsx +9 -3
  333. package/src/plugins/builtin/notes/index.tsx +3 -6
  334. package/src/plugins/builtin/openfda/index.tsx +1 -2
  335. package/src/plugins/builtin/options/view.tsx +6 -5
  336. package/src/plugins/builtin/options-calculator/index.tsx +1 -2
  337. package/src/plugins/builtin/options-positioning/index.tsx +1 -2
  338. package/src/plugins/builtin/options-scenario/index.tsx +2 -4
  339. package/src/plugins/builtin/perps/index.tsx +1 -2
  340. package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -3
  341. package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -1
  342. package/src/plugins/builtin/plugin-module.ts +16 -2
  343. package/src/plugins/builtin/portfolio-list/cli/render.ts +53 -3
  344. package/src/plugins/builtin/portfolio-list/index.tsx +1 -2
  345. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -0
  346. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -2
  347. package/src/plugins/builtin/power/gas-client.ts +2 -1
  348. package/src/plugins/builtin/power/index.tsx +1 -2
  349. package/src/plugins/builtin/power/prices-client.ts +3 -2
  350. package/src/plugins/builtin/rate-path/index.tsx +1 -2
  351. package/src/plugins/builtin/realized-vol/headless.ts +2 -0
  352. package/src/plugins/builtin/realized-vol/index.tsx +1 -2
  353. package/src/plugins/builtin/realized-vol/model.ts +16 -2
  354. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  355. package/src/plugins/builtin/relative-rotation/index.tsx +1 -2
  356. package/src/plugins/builtin/research/analyst-headless.ts +33 -6
  357. package/src/plugins/builtin/research/analyst-model.ts +37 -2
  358. package/src/plugins/builtin/research/analyst-pane.tsx +25 -5
  359. package/src/plugins/builtin/research/index.tsx +5 -8
  360. package/src/plugins/builtin/research-search/index.tsx +1 -2
  361. package/src/plugins/builtin/revenue-breakdown/headless.ts +22 -8
  362. package/src/plugins/builtin/revenue-breakdown/model.ts +23 -0
  363. package/src/plugins/builtin/reverse-dcf/pane.tsx +4 -2
  364. package/src/plugins/builtin/risk-factors/data.ts +8 -4
  365. package/src/plugins/builtin/risk-factors/index.tsx +3 -1
  366. package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
  367. package/src/plugins/builtin/roadmap/index.tsx +2 -3
  368. package/src/plugins/builtin/roadmap/pane.tsx +49 -2
  369. package/src/plugins/builtin/scanner/hilo-model.ts +18 -1
  370. package/src/plugins/builtin/scanner/hilo-pane.tsx +12 -2
  371. package/src/plugins/builtin/scanner/live-pane.tsx +7 -4
  372. package/src/plugins/builtin/scanner/lock-note.ts +26 -3
  373. package/src/plugins/builtin/sec/filing-content.ts +38 -6
  374. package/src/plugins/builtin/sec/headless.ts +5 -1
  375. package/src/plugins/builtin/sec/index.tsx +4 -2
  376. package/src/plugins/builtin/sec/model.ts +2 -0
  377. package/src/plugins/builtin/sectors/index.tsx +11 -5
  378. package/src/plugins/builtin/series-inventory.ts +2 -0
  379. package/src/plugins/builtin/shared/cloud-resource.ts +73 -8
  380. package/src/plugins/builtin/shared/cloud-upgrade.ts +24 -1
  381. package/src/plugins/builtin/shared/forming-bars.ts +29 -0
  382. package/src/plugins/builtin/shared/fred-points.ts +6 -1
  383. package/src/plugins/builtin/shared/fx-rate-freshness.ts +17 -0
  384. package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
  385. package/src/plugins/builtin/shared/series-detail-chart.tsx +4 -2
  386. package/src/plugins/builtin/shared/series-warning.ts +13 -4
  387. package/src/plugins/builtin/shared/use-quote-board.ts +5 -0
  388. package/src/plugins/builtin/short-interest/publications-pane.tsx +2 -1
  389. package/src/plugins/builtin/short-volume/pane.tsx +2 -1
  390. package/src/plugins/builtin/short-volume/series.ts +2 -3
  391. package/src/plugins/builtin/social-mentions/client.ts +15 -10
  392. package/src/plugins/builtin/social-mentions/index.tsx +1 -2
  393. package/src/plugins/builtin/social-mentions/pane.tsx +2 -1
  394. package/src/plugins/builtin/social-mentions/series.ts +2 -3
  395. package/src/plugins/builtin/supply-chain/client.ts +22 -1
  396. package/src/plugins/builtin/supply-chain/graph-client.ts +22 -1
  397. package/src/plugins/builtin/supply-chain/index.tsx +1 -2
  398. package/src/plugins/builtin/thirteenf/api.ts +127 -29
  399. package/src/plugins/builtin/thirteenf/data.ts +165 -35
  400. package/src/plugins/builtin/thirteenf/index.tsx +2 -4
  401. package/src/plugins/builtin/thirteenf/model.ts +2 -0
  402. package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
  403. package/src/plugins/builtin/thirteenf/pane.tsx +41 -16
  404. package/src/plugins/builtin/thirteenf/types.ts +14 -0
  405. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +22 -6
  406. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +81 -11
  407. package/src/plugins/builtin/ticker-detail/financials/model.ts +35 -2
  408. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +53 -18
  409. package/src/plugins/builtin/ticker-detail/headless.ts +70 -11
  410. package/src/plugins/builtin/ticker-detail/index.tsx +5 -10
  411. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +16 -6
  412. package/src/plugins/builtin/time-sales/index.tsx +1 -2
  413. package/src/plugins/builtin/time-sales/pane.tsx +3 -1
  414. package/src/plugins/builtin/trace-bonds/headless.ts +11 -2
  415. package/src/plugins/builtin/trace-bonds/model.ts +44 -10
  416. package/src/plugins/builtin/trace-bonds/pane.tsx +12 -7
  417. package/src/plugins/builtin/treasury-auctions/cache.ts +20 -8
  418. package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
  419. package/src/plugins/builtin/treasury-auctions/headless.ts +3 -1
  420. package/src/plugins/builtin/treasury-auctions/pane.tsx +10 -4
  421. package/src/plugins/builtin/vol-surface/client.ts +18 -3
  422. package/src/plugins/builtin/vol-surface/index.tsx +1 -2
  423. package/src/plugins/builtin/volatility/client.ts +43 -16
  424. package/src/plugins/builtin/volatility/headless.ts +5 -2
  425. package/src/plugins/builtin/volatility/index.tsx +1 -2
  426. package/src/plugins/builtin/volatility/model.ts +17 -6
  427. package/src/plugins/builtin/volatility/pane.tsx +6 -2
  428. package/src/plugins/builtin/volatility/series.ts +2 -1
  429. package/src/plugins/builtin/world-indices/footer.ts +6 -1
  430. package/src/plugins/builtin/world-venue-map/client.ts +10 -1
  431. package/src/plugins/builtin/world-venue-map/index.ts +6 -2
  432. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +48 -7
  433. package/src/plugins/builtin/world-venue-map/layers.ts +17 -0
  434. package/src/plugins/builtin/world-venue-map/local-layers.ts +141 -0
  435. package/src/plugins/builtin/world-venue-map/map-symbols.ts +18 -0
  436. package/src/plugins/builtin/world-venue-map/map.tsx +4 -2
  437. package/src/plugins/builtin/world-venue-map/scope.ts +63 -0
  438. package/src/plugins/builtin/yield-curve/client.ts +4 -6
  439. package/src/plugins/builtin/yield-curve/curves.ts +16 -2
  440. package/src/plugins/builtin/yield-curve/headless.ts +3 -3
  441. package/src/plugins/bundle.ts +52 -2
  442. package/src/plugins/catalog-browser.ts +2 -2
  443. package/src/plugins/catalog-cli.ts +20 -0
  444. package/src/plugins/host-link.ts +13 -1
  445. package/src/plugins/installer.ts +64 -42
  446. package/src/plugins/loader.ts +43 -11
  447. package/src/plugins/peer-imports.ts +201 -0
  448. package/src/plugins/peer-retry.ts +26 -29
  449. package/src/plugins/plugin-process.ts +128 -0
  450. package/src/plugins/redirection-guard.ts +45 -0
  451. package/src/remote/endpoint.ts +16 -3
  452. package/src/renderers/browser/main.tsx +3 -2
  453. package/src/renderers/cloudflare/http-proxy.ts +7 -123
  454. package/src/renderers/dom/desktop/icons.tsx +5 -2
  455. package/src/renderers/dom/host/image-surface.tsx +46 -21
  456. package/src/renderers/dom/styles.css +7 -0
  457. package/src/renderers/dom/toast-host.tsx +2 -0
  458. package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
  459. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
  460. package/src/renderers/electrobun/bun/desktop/initialization.ts +12 -3
  461. package/src/renderers/electrobun/bun/desktop/rpc-timing.ts +88 -0
  462. package/src/renderers/electrobun/bun/desktop/update.ts +20 -4
  463. package/src/renderers/electrobun/bun/desktop/view-ids.ts +8 -4
  464. package/src/renderers/electrobun/bun/external-plugins.ts +4 -0
  465. package/src/renderers/electrobun/bun/window/attention.ts +54 -0
  466. package/src/renderers/electrobun/bun/window/create.ts +6 -2
  467. package/src/renderers/electrobun/bun/window/native-timing.ts +48 -0
  468. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +82 -4
  469. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +13 -5
  470. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +3 -0
  471. package/src/renderers/electrobun/view/main.tsx +7 -2
  472. package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
  473. package/src/renderers/electrobun/view/ui-host.tsx +9 -2
  474. package/src/renderers/opentui/browser-open.ts +10 -4
  475. package/src/renderers/opentui/image/surface.tsx +1 -1
  476. package/src/renderers/opentui/start.tsx +12 -6
  477. package/src/renderers/opentui/toast-host.tsx +2 -1
  478. package/src/series/registry.ts +18 -0
  479. package/src/series/resolve.ts +2 -1
  480. package/src/series/standalone.ts +10 -1
  481. package/src/series/types.ts +12 -0
  482. package/src/series/validate.ts +11 -2
  483. package/src/sources/gloomberb-cloud/fred-series.ts +5 -3
  484. package/src/sources/gloomberb-cloud/index.ts +28 -5
  485. package/src/sources/gloomberb-cloud/news.ts +24 -3
  486. package/src/sources/gloomberb-cloud/normalizers.ts +17 -2
  487. package/src/sources/provider-errors.ts +26 -3
  488. package/src/sources/provider-router/history.ts +5 -0
  489. package/src/sources/provider-router/index.ts +13 -0
  490. package/src/sync/core-contributors.ts +32 -0
  491. package/src/telemetry/attention-counts.ts +64 -6
  492. package/src/telemetry/attention-setting.ts +33 -0
  493. package/src/telemetry/crash-reports-node.ts +3 -1
  494. package/src/telemetry/stall-clocks.ts +93 -0
  495. package/src/telemetry/stall-reports-node.ts +25 -9
  496. package/src/telemetry/stall-reports.ts +218 -13
  497. package/src/test-support/outage.ts +8 -0
  498. package/src/test-support/preload.ts +9 -0
  499. package/src/tickers/company-name-listing.ts +59 -29
  500. package/src/tickers/company-name.ts +84 -1
  501. package/src/tickers/listing-fate.ts +14 -3
  502. package/src/tickers/saved-instrument.ts +17 -0
  503. package/src/tickers/search/home-listing.ts +104 -8
  504. package/src/tickers/search/index.ts +5 -2
  505. package/src/tickers/search/types.ts +6 -1
  506. package/src/time-series/bar-aggregation.ts +7 -1
  507. package/src/time-series/base-series.ts +3 -3
  508. package/src/time-series/chart-bounds.ts +1 -1
  509. package/src/time-series/chart-data.ts +5 -2
  510. package/src/time-series/chart-seed.ts +1 -1
  511. package/src/time-series/coin-session-close.ts +211 -0
  512. package/src/time-series/live-bars.ts +13 -2
  513. package/src/time-series/live-quotes.ts +1 -1
  514. package/src/time-series/market.ts +5 -3
  515. package/src/time-series/official-close.ts +159 -0
  516. package/src/time-series/price-comparison.ts +75 -8
  517. package/src/time-series/resolve.ts +32 -4
  518. package/src/time-series/series-presentation.ts +1 -1
  519. package/src/time-series/studies/formula.ts +205 -0
  520. package/src/time-series/studies/indicators.ts +299 -0
  521. package/src/time-series/studies/leverage.ts +75 -0
  522. package/src/time-series/studies/pair.ts +138 -0
  523. package/src/time-series/studies/resolve.ts +333 -0
  524. package/src/time-series/studies/shared.ts +141 -0
  525. package/src/time-series/studies/units.ts +243 -0
  526. package/src/time-series/studies/vwap.ts +274 -0
  527. package/src/time-series/types.ts +7 -0
  528. package/src/types/config.ts +18 -2
  529. package/src/types/data-provider.ts +23 -0
  530. package/src/types/financials.ts +40 -3
  531. package/src/types/headless.ts +42 -3
  532. package/src/types/plugin.ts +32 -3
  533. package/src/ui/host.tsx +17 -1
  534. package/src/ui/toast.tsx +26 -1
  535. package/src/utils/ascii-text.ts +140 -0
  536. package/src/utils/cli-output.ts +91 -18
  537. package/src/utils/concurrency.ts +24 -0
  538. package/src/utils/exchanges.ts +6 -0
  539. package/src/utils/external-url.ts +20 -7
  540. package/src/utils/financial-statements.ts +73 -16
  541. package/src/utils/fundamentals.ts +17 -5
  542. package/src/utils/futures-generic.ts +4 -4
  543. package/src/utils/http-proxy-upstream.ts +124 -0
  544. package/src/utils/lease-liabilities.ts +54 -0
  545. package/src/utils/main-thread-stall.ts +122 -5
  546. package/src/utils/perf-marks.ts +58 -12
  547. package/src/utils/price-history-integrity.ts +47 -1
  548. package/src/utils/private-files.ts +141 -0
  549. package/src/utils/public-address.ts +99 -0
  550. package/src/account/upgrade-personalization.ts +0 -57
  551. package/src/account/upgrade-tickers.ts +0 -124
  552. package/src/api-client/upgrade-experiment.ts +0 -91
  553. package/src/plugins/builtin/cloud/browser.tsx +0 -3
  554. package/src/time-series/studies.ts +0 -1628
@@ -2,6 +2,7 @@ import type { HeadlessBundleResult, HeadlessPaneContext, HeadlessPaneDefinition,
2
2
  import { createVolatilityDependencies, loadVolatilityData, type VolatilityLoadResult } from "./client";
3
3
  import { termStateLabel, type VolatilityData } from "./model";
4
4
  import { newestReportTime, oldestReportTime } from "../../../utils/utc-time";
5
+ import { servedFromSavedCopy } from "../../../data/saved-copies";
5
6
 
6
7
  function formattedValue(value: unknown): string { return typeof value === "number" && Number.isFinite(value) ? value.toFixed(2) : "--"; }
7
8
  function formattedPercentile(value: unknown): string { return typeof value === "number" && Number.isFinite(value) ? value.toFixed(0) : "--"; }
@@ -60,6 +61,8 @@ export function createVolatilityHeadless(dependencies: VolatilityHeadlessDepende
60
61
  async load(args, context) {
61
62
  const result = await dependencies.load(args, context);
62
63
  context.signal.throwIfAborted();
64
+ // A saved copy answering a failed refresh ends the report with when it was saved.
65
+ const savedCopy = <T>(value: T) => result.savedAt != null ? servedFromSavedCopy(value, result.savedAt) : value;
63
66
  // Daily index closes, dated by the newest index on the board; a sparse index shows as the oldest.
64
67
  const boardDates = result.data.board.filter((row) => row.value != null).map((row) => row.date);
65
68
  const freshness = { source: "Cboe and FRED", status: "not-a-feed" as const, basis: "daily index closes", cadence: "daily" as const,
@@ -67,10 +70,10 @@ export function createVolatilityHeadless(dependencies: VolatilityHeadlessDepende
67
70
  return { ...projectVolatilityHeadless(result.data), freshness, errors: result.errors,
68
71
  complete: result.phase === "ready" && !result.stale && result.data.warnings.length === 0,
69
72
  unavailableSymbols: result.data.board.filter((row) => row.value == null).map((row) => row.symbol),
70
- metadata: { stale: result.stale, phase: result.phase, loaded: result.loaded, total: result.total,
73
+ metadata: savedCopy({ stale: result.stale, phase: result.phase, loaded: result.loaded, total: result.total,
71
74
  data: result.data, unit: "index points", observations: "daily history or isolated index observation",
72
75
  percentile: "Midrank within last calendar year; at least 200 observations spanning 300 days",
73
- vixFuturesAvailable: false, methodology: "docs/research-data.md#vix-curve-and-cross-asset-volatility-board" },
76
+ vixFuturesAvailable: false, methodology: "docs/research-data.md#vix-curve-and-cross-asset-volatility-board" }),
74
77
  };
75
78
  },
76
79
  };
@@ -1,13 +1,12 @@
1
1
  import type { PluginModule } from "../plugin-module";
2
2
  import { volatilityHeadless } from "./headless";
3
- import { VolatilityPane } from "./pane";
4
3
  import { VOLATILITY_SERIES } from "./series";
5
4
 
6
5
 
7
6
  export const volatilityModule: PluginModule = {
8
7
  series: VOLATILITY_SERIES,
9
8
  panes: [{
10
- id: "volatility-term-structure", name: "Volatility", icon: "V", component: VolatilityPane,
9
+ id: "volatility-term-structure", name: "Volatility", icon: "V", get component(): typeof import("./pane").VolatilityPane { const { VolatilityPane } = require("./pane"); return VolatilityPane; },
11
10
  defaultPosition: "right", defaultMode: "floating", defaultFloatingSize: { width: 110, height: 34 },
12
11
  tableExport: true, headless: volatilityHeadless,
13
12
  }],
@@ -30,6 +30,11 @@ export interface VolatilityHistoryInput {
30
30
  stale?: boolean;
31
31
  error?: string | null;
32
32
  fetchedAt?: number | null;
33
+ /**
34
+ * When the copy shown was saved, when a failed refresh left it answering:
35
+ * the pane's footer says "saved copy from …" instead of the failure.
36
+ */
37
+ savedAt?: number | null;
33
38
  }
34
39
  export interface VolatilitySeriesInput {
35
40
  observations: CloudFredObservationPayload[];
@@ -37,6 +42,8 @@ export interface VolatilitySeriesInput {
37
42
  stale?: boolean;
38
43
  error?: string | null;
39
44
  fetchedAt?: number | null;
45
+ /** As `VolatilityHistoryInput.savedAt`. */
46
+ savedAt?: number | null;
40
47
  }
41
48
  export interface VolatilityInputs {
42
49
  history?: Partial<Record<VolatilityIndexId, VolatilityHistoryInput>>;
@@ -131,6 +138,8 @@ export interface VolatilityBoardRow {
131
138
  status: "available" | "limited" | "unavailable";
132
139
  stale: boolean;
133
140
  error: string | null;
141
+ /** When the copy shown was saved, when a failed refresh left it answering. */
142
+ savedAt?: number;
134
143
  warnings: string[];
135
144
  }
136
145
  export interface VolatilityData {
@@ -233,8 +242,9 @@ function fredHistory(inputs: VolatilityInputs["fred"]): FredVolatilityHistory {
233
242
  const input = inputs?.[definition.seriesId];
234
243
  const normalized = normalizeHistory(input?.observations ?? [], true);
235
244
  warnings.push(...normalized.warnings.map((warning) => `${definition.seriesId}: ${warning}`));
236
- if (input?.error) warnings.push(`${definition.seriesId}: ${input.error}`);
237
- if (input?.stale) warnings.push(`${definition.seriesId}: cached source is stale`);
245
+ // A saved copy is dated in the footer; its failed refresh is not repeated here.
246
+ if (input?.error && input.savedAt == null) warnings.push(`${definition.seriesId}: ${input.error}`);
247
+ if (input?.stale && input.savedAt == null) warnings.push(`${definition.seriesId}: cached source is stale`);
238
248
  const latest = normalized.history.at(-1);
239
249
  return { ...definition, title: input?.info?.title ?? definition.label, value: latest?.value ?? null,
240
250
  date: latest?.date ?? null, history: normalized.history,
@@ -300,15 +310,16 @@ function boardRow(definition: typeof VOLATILITY_INDICES[number], input: Volatili
300
310
  if (latest && latest.date !== normalized.rows.at(-1)?.date) warnings.push("Latest supplied close unavailable; showing last valid observation");
301
311
  if (latest && !adjacent) warnings.push("1D change unavailable: previous daily close missing or too far apart");
302
312
  if (latest && !broadCoverage) warnings.push(`1Y percentile unavailable: ${history.length} observations across ${coverageDays} days`);
303
- if (input?.stale) warnings.push("Cached daily history is stale");
304
- if (input?.error) warnings.push(input.error);
313
+ // A saved copy is dated in the footer; its failed refresh is not repeated here.
314
+ if (input?.stale && input.savedAt == null) warnings.push("Cached daily history is stale");
315
+ if (input?.error && input.savedAt == null) warnings.push(input.error);
305
316
  return { ...definition, unit: "index points", value: latest?.value ?? null, date: latest?.date ?? null,
306
317
  source: input?.source ?? null, previousDate: previous?.date ?? null, change1d,
307
318
  change1dPercent: change1d != null ? change1d / previous!.value! * 100 : null,
308
319
  percentile1y, sampleSize: history.length, coverageDays, history,
309
320
  missingDates: normalized.rows.filter((row) => row.value == null && Date.parse(row.date) > cutoff).map((row) => row.date),
310
321
  status: !latest ? "unavailable" : warnings.length > 0 ? "limited" : "available",
311
- stale: input?.stale ?? false, error: input?.error ?? null, warnings };
322
+ stale: input?.stale ?? false, error: input?.error ?? null, ...(input?.savedAt != null ? { savedAt: input.savedAt } : {}), warnings };
312
323
  }
313
324
  function alignedCurve(board: readonly VolatilityBoardRow[], fred: FredVolatilityHistory, term: VolatilityTermHistory): VolatilityCurve {
314
325
  const rows = VOLATILITY_CURVE_INDICES.map((definition) => board.find((row) => row.id === definition.id)!);
@@ -337,7 +348,7 @@ function alignedCurve(board: readonly VolatilityBoardRow[], fred: FredVolatility
337
348
  const spot = points[1]!.value, threeMonth = points[2]!.value;
338
349
  const warnings = points.flatMap((point, index) => [
339
350
  ...(point.value == null ? [`${point.label}: no close aligned to ${date ?? "a common date"}`] : []),
340
- ...(point.value != null && rows[index]!.stale ? [`${point.label}: aligned close is from stale cached history`] : []),
351
+ ...(point.value != null && rows[index]!.stale && rows[index]!.savedAt == null ? [`${point.label}: aligned close is from stale cached history`] : []),
341
352
  ]);
342
353
  const ratio = spot != null && threeMonth != null ? threeMonth / spot : null;
343
354
  const context = ratioPercentile(term, ratio, date);
@@ -2,6 +2,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { ChartTableHeader, CurveSurface, curveGhostColors, DataTableView, formatPercentAxis, PaneStatusBody, useChartTableSelection,
3
3
  usePaneFooter, usePaneNoticeFooter, usePaneTabs, type DataTableColumn, type StatItem } from "../../../components";
4
4
  import { curveStrip, curveSurfaceMinRows } from "../../../components/chart/curve";
5
+ import { savedCopyFooterInfo } from "../../../components/layout/pane/footer";
6
+ import { STALE_SEGMENT } from "../../../components/layout/pane/status-footer";
5
7
  import { dropColumnsToFit } from "../../../components/ui/table-layout";
6
8
  import { useAsyncResource } from "../../../react/async-resource";
7
9
  import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
@@ -170,13 +172,15 @@ export function VolatilityPane({ focused, width, height }: PaneProps) {
170
172
  const observationBasis = intraday ? "intraday" : sparse ? "observation" : settled ? "close" : DAY_SO_FAR_TEXT;
171
173
  usePaneFooter("volatility", () => ({ info: [
172
174
  ...(resource.loading ? [{ id: "loading", parts: [{ text: "loading volatility", tone: "muted" as const }] }] : []),
173
- ...(result?.stale ? [{ id: "stale", parts: [{ text: "stale", tone: "warning" as const }] }] : []),
175
+ // A saved copy answering a failed refresh says when it was saved; other stale closes say only that.
176
+ ...savedCopyFooterInfo([result?.savedAt]),
177
+ ...(result?.stale && result.savedAt == null ? [STALE_SEGMENT] : []),
174
178
  // One fact, "close 2026-09-25": the basis and its date read as one segment.
175
179
  ...(data || asOf ? [{ id: "basis", parts: [
176
180
  ...(data ? [{ text: observationBasis, tone: "muted" as const }] : []),
177
181
  ...(asOf ? [{ text: observationTime ? `${observationTime.slice(0, 16).replace("T", " ")} UTC` : asOf, tone: "muted" as const }] : []),
178
182
  ] }] : []),
179
- ], hints: [{ id: "view", key: "v", label: "iew", onPress: cycleTab }] }), [resource.loading, result?.stale, data, asOf, observationTime, observationBasis, cycleTab]);
183
+ ], hints: [{ id: "view", key: "v", label: "iew", onPress: cycleTab }] }), [resource.loading, result?.stale, result?.savedAt, data, asOf, observationTime, observationBasis, cycleTab]);
180
184
  const { strip: tabStrip, rows: tabRows } = usePaneTabs({ tabs: TABS, activeValue: tab, onSelect: setTab, focused, dense: true });
181
185
  const contentHeight = Math.max(5, height - tabRows);
182
186
  // The board needs only its rows; the selected index history takes the rest.
@@ -105,11 +105,12 @@ export const VOLATILITY_SERIES: readonly SeriesDefinition[] = [
105
105
  tags: ["term structure", "contango", "backwardation", "VIX"],
106
106
  description: "VIX 3M over VIX on each date both closed, from Cboe's daily closes and FRED's before them; above 1 is contango.",
107
107
  load: async ({ start, signal }) => {
108
- const { term, warning } = await loadVolatilityTermHistory({ ...(start ? { start } : {}), ...(signal ? { signal } : {}) });
108
+ const { term, warning, savedAt } = await loadVolatilityTermHistory({ ...(start ? { start } : {}), ...(signal ? { signal } : {}) });
109
109
  const attribution = termAttribution(term);
110
110
  return {
111
111
  points: term.ratioHistory.map((point) => ({ date: point.date, value: point.value })),
112
112
  ...(warning ? { warning } : {}),
113
+ ...(savedAt != null ? { savedAt } : {}),
113
114
  ...(attribution ? { attribution } : {}),
114
115
  };
115
116
  },
@@ -1,7 +1,9 @@
1
1
  import { usePaneFooter, type PaneFooterSegment } from "../../../components";
2
2
  import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
3
+ import { useQuoteDelayFooter } from "../shared/cloud-upgrade";
3
4
  import {
4
5
  boardErrorMessage,
6
+ boardQuotes,
5
7
  quoteBoardFooterInfo,
6
8
  quoteBoardStatus,
7
9
  type BoardQuoteMap,
@@ -12,14 +14,17 @@ export function useWorldIndicesFooter(quotes: BoardQuoteMap, onRefresh: () => vo
12
14
  const errorMessage = boardErrorMessage(quotes);
13
15
 
14
16
  usePaneRefreshKey(onRefresh, { focused });
17
+ // Signed out or free, the indices' quotes are delayed ones.
18
+ const quoteDelay = useQuoteDelayFooter(boardQuotes(quotes), "indices-delay");
15
19
 
16
20
  usePaneFooter(
17
21
  "world-indices",
18
22
  () => {
19
23
  const info: PaneFooterSegment[] = quoteBoardFooterInfo(status);
20
24
  if (errorMessage) info.push({ id: "reason", parts: [{ text: errorMessage, tone: "warning" }] });
25
+ if (quoteDelay) info.push(quoteDelay);
21
26
  return { info };
22
27
  },
23
- [errorMessage, focused, onRefresh, status.latestTs, status.loading, status.savedAt, status.stale, status.unavailable],
28
+ [errorMessage, focused, onRefresh, quoteDelay, status.latestTs, status.loading, status.savedAt, status.stale, status.unavailable],
24
29
  );
25
30
  }
@@ -10,11 +10,20 @@ import {
10
10
  type GeoLayersPayload,
11
11
  type GeoRequest,
12
12
  } from "../../../api-client/geo";
13
+ import { withLocalGeoLayers } from "./local-layers";
13
14
 
14
15
  export const ENTITY_PAGE_SIZE = 100;
15
16
 
16
17
  /** Requests under /cloud/geo/ through the shared client, resolved late so tests can stub it. */
17
- export const cloudGeoRequest: GeoRequest = (path, init) => apiClient.geo(path, init);
18
+ const serverGeoRequest: GeoRequest = (path, init) => apiClient.geo(path, init);
19
+
20
+ /** The server's layers and the app's own (`local-layers.ts`). */
21
+ export const cloudGeoRequest: GeoRequest = withLocalGeoLayers(serverGeoRequest);
22
+
23
+ /** The same, with the app's own layers narrowed to what a linked pane selects. */
24
+ export function scopedGeoRequest(scope: string | null): GeoRequest {
25
+ return scope ? withLocalGeoLayers(serverGeoRequest, scope) : cloudGeoRequest;
26
+ }
18
27
 
19
28
  export async function loadWorldVenues(): Promise<CloudWorldVenueMapPayload> {
20
29
  const response = await apiClient.getCloudWorldVenues();
@@ -9,6 +9,7 @@ import { CHOKEPOINT_SERIES, createGeoChartSeriesCapability } from "./geo-series"
9
9
  import { mapHeadless } from "./headless";
10
10
  import { buildMapSettingsDef, groupTitle, MAP_PRESET_OPTIONS, parseMapPreset } from "./layers";
11
11
  import { WORLD_VENUE_MAP_PANE_ID, WorldVenueMapPane } from "./pane";
12
+ import { mapFollowsTicker } from "./scope";
12
13
 
13
14
  const chartComposerHeadless = chartHeadless("chart-composer-pane");
14
15
 
@@ -33,6 +34,7 @@ const chokepointTemplate: PaneTemplateDef = {
33
34
  keywords: ["chokepoint", "chokepoints", "transits", "suez", "panama", "hormuz", "malacca", "shipping", "strait", "canal"],
34
35
  shortcut: { prefix: "CHOKE", argPlaceholder: "chokepoint", argKind: "text", argOptional: true },
35
36
  canCreate: () => true,
37
+ catalogSettings: {},
36
38
  createInstance: (_context, options) => createChokepointInstance(cloudGeoRequest, options?.arg),
37
39
  };
38
40
 
@@ -46,6 +48,8 @@ export const worldVenueMapModule: PluginModule = {
46
48
  defaultPosition: "right",
47
49
  defaultMode: "floating",
48
50
  defaultFloatingSize: { width: 124, height: 36 },
51
+ // Linked to a list, a map with the app's own layers (facilities) shows the selected company's.
52
+ tickerFollower: mapFollowsTicker,
49
53
  // The layer picker comes from the server's catalog; without one the pane keeps its old settings.
50
54
  settings: (context) => buildMapSettingsDef(context.settings, peekGeoCatalog()?.layers) ?? { fields: [] },
51
55
  },
@@ -55,8 +59,8 @@ export const worldVenueMapModule: PluginModule = {
55
59
  id: "world-venue-map-pane",
56
60
  paneId: WORLD_VENUE_MAP_PANE_ID,
57
61
  label: "World Map",
58
- description: "A world map with trading venues and their hours; ships, ports, energy and airports as layers.",
59
- keywords: ["world", "map", "globe", "countries", "venue", "venues", "exchange", "mic", "market hours", "open markets", "ships", "ship", "vessels", "vessel", "shipping", "ais", "ports", "port", "chokepoints", "pipelines", "airports", "energy", "layers"],
62
+ description: "A world map with trading venues and their hours; ships, ports, energy, airports and AI buildout facilities as layers.",
63
+ keywords: ["world", "map", "globe", "countries", "venue", "venues", "exchange", "mic", "market hours", "open markets", "ships", "ship", "vessels", "vessel", "shipping", "ais", "ports", "port", "chokepoints", "pipelines", "airports", "energy", "facilities", "data centers", "fabs", "layers"],
60
64
  shortcut: {
61
65
  prefix: "MAP",
62
66
  argPlaceholder: "layers",
@@ -26,6 +26,8 @@ import {
26
26
  type DataTableCell,
27
27
  type PageRequest,
28
28
  type PaneHint,
29
+ type QueryBarFilter,
30
+ TickerBadgeList,
29
31
  } from "../../../components";
30
32
  import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
31
33
  import {
@@ -36,13 +38,17 @@ import {
36
38
  usePluginPaneState,
37
39
  } from "../../../public/react";
38
40
  import { relativeLuminance } from "../../../theme/color-utils";
41
+ import { displayWidth } from "../../../utils/format";
42
+ import { truncateWithEllipsis } from "../../../utils/text-wrap";
39
43
  import { useThemeColors } from "../../../theme/theme-context";
40
44
  import type { PaneProps } from "../../../types/plugin";
41
45
  import { Box, Text, TextAttributes, useUiCapabilities, type ScrollBoxRenderable } from "../../../ui";
42
46
  import { formatGeoSeriesExpression } from "../chart-composer/series-expression";
43
47
  import { CLOUD_PLAN_KEY, useCloudUpgradeAction } from "../shared/cloud-upgrade";
44
48
  import { useOpenDescription } from "../shared/open-description";
45
- import { ENTITY_PAGE_SIZE, loadEntityDetail, loadEntityPage, loadWorldVenues } from "./client";
49
+ import { ENTITY_PAGE_SIZE, loadEntityDetail, loadEntityPage, loadWorldVenues, scopedGeoRequest } from "./client";
50
+ import { isLocalGeoLayer } from "./local-layers";
51
+ import { useLinkedGeoScope } from "./scope";
46
52
  import { EntityDetail } from "./entity-detail";
47
53
  import { useWorldMapEvidence } from "./evidence";
48
54
  import { entityCellText, entityColumns, TICKER_COLUMN_ID, type EntityColumn } from "./entity-table";
@@ -52,6 +58,7 @@ import { WORLD_VENUE_MAP_PANE_ID } from "./ids";
52
58
  import {
53
59
  entityOpenZoom,
54
60
  entityTableBbox,
61
+ framePlaces,
55
62
  featureAnchor,
56
63
  formatGeoValue,
57
64
  formatTickerLinks,
@@ -162,7 +169,7 @@ const SelectedEntityHeader = memo(function SelectedEntityHeader({
162
169
  return (
163
170
  <Box flexDirection="column" height={2} width={width} paddingX={1}>
164
171
  <Box flexDirection="row" justifyContent="space-between" width="100%">
165
- <Text fg={colors.textBright} attributes={TextAttributes.BOLD}>{row.label}</Text>
172
+ <Text fg={colors.textBright} attributes={TextAttributes.BOLD}>{truncateWithEllipsis(row.label, Math.max(1, width - 3 - displayWidth(layer.name)))}</Text>
166
173
  <Text fg={color}>{layer.name}</Text>
167
174
  </Box>
168
175
  <Text fg={colors.textMuted}>{meta}</Text>
@@ -193,7 +200,11 @@ export function LayeredMapView({ focused, width, height, layers, tokens, plain =
193
200
  setView((current) => sameGeoView(current, next) ? current : next);
194
201
  }, []);
195
202
  const canAccess = useCallback((layer: GeoLayerInfo) => layer.access !== "pro" || hasProAccess, [hasProAccess]);
196
- const states = useGeoLayerFeed(layers, view, visible, { canAccess });
203
+ // A linked map narrows the app's own layers to what its source pane selects.
204
+ const linkedScope = useLinkedGeoScope(layers.some((layer) => isLocalGeoLayer(layer.id)));
205
+ const scope = linkedScope.scope;
206
+ const request = useMemo(() => scopedGeoRequest(scope?.key ?? null), [scope?.key]);
207
+ const states = useGeoLayerFeed(layers, view, visible, { canAccess, request });
197
208
 
198
209
  // A layer coloured by class (ships) takes the first colour, so its own never meets the class colours.
199
210
  const colorOrder = useMemo(
@@ -244,7 +255,7 @@ export function LayeredMapView({ focused, width, height, layers, tokens, plain =
244
255
  ...(sort.key ? { sort: sort.key, dir: sort.dir } : {}),
245
256
  ...(tableBbox ? { bbox: tableBbox } : {}),
246
257
  offset,
247
- }, signal);
258
+ }, signal, request);
248
259
  const rows = page.rows ?? [];
249
260
  const total = typeof page.total === "number" ? page.total : null;
250
261
  return {
@@ -252,18 +263,28 @@ export function LayeredMapView({ focused, width, height, layers, tokens, plain =
252
263
  rows,
253
264
  hasMore: rows.length >= ENTITY_PAGE_SIZE && (total === null || offset + rows.length < total),
254
265
  nextOffset: offset + rows.length,
266
+ scopeKey: scope?.key ?? null,
255
267
  };
256
- }, [query, sort.dir, sort.key, tableBboxKey, tableLayer.id]);
268
+ }, [query, request, scope?.key, sort.dir, sort.key, tableBboxKey, tableLayer.id]);
257
269
  const pages = usePagedRows(tableReadable ? loadPage : null, { getId: (row) => row.id });
258
270
  const rows = pages.rows;
271
+ // A linked map frames the places of each company or theme it moves to, once.
272
+ const framedScopeRef = useRef<string | null>(null);
273
+ const firstPage = pages.pages[0];
274
+ useEffect(() => {
275
+ if (!firstPage?.scopeKey || !isLocalGeoLayer(tableLayer.id) || framedScopeRef.current === firstPage.scopeKey) return;
276
+ framedScopeRef.current = firstPage.scopeKey;
277
+ const frame = framePlaces(firstPage.rows);
278
+ if (frame) setFocus({ key: `scope:${firstPage.scopeKey}`, ...frame, exact: true });
279
+ }, [firstPage, tableLayer.id]);
259
280
  useWorldMapEvidence(tokens, states, rows.length, pages.loading);
260
281
  const refresh = useCallback(() => pages.reload(), [pages.reload]);
261
282
  usePaneRefreshKey(refresh, { focused: focused && !searchFocused });
262
283
 
263
284
  const detailOpen = !!open && open.layer === tableLayer.id;
264
285
  const detailLoader = useMemo(
265
- () => (detailOpen && open ? () => loadEntityDetail(open.layer, open.id) : null),
266
- [detailOpen, open?.id, open?.layer],
286
+ () => (detailOpen && open ? () => loadEntityDetail(open.layer, open.id, undefined, request) : null),
287
+ [detailOpen, open?.id, open?.layer, request],
267
288
  );
268
289
  const detail = useAsyncResource(detailLoader);
269
290
  const openRow = detailOpen
@@ -412,9 +433,29 @@ export function LayeredMapView({ focused, width, height, layers, tokens, plain =
412
433
 
413
434
  const onScroll = useTableLoadMore(scrollRef, pages.hasMore && !detailOpen, pages.loadMore);
414
435
  // A layer that cannot serve has nothing to search and, alone, nothing to switch to: no bar at all.
436
+ // What a linked map shows, before the search: a switch between the company and its theme when the
437
+ // source has both, else the one it has.
438
+ const scoped = !!scope && isLocalGeoLayer(tableLayer.id);
439
+ const { company: scopeCompany, theme: scopeTheme } = linkedScope;
440
+ const scopeFilter: QueryBarFilter | null = scoped && scopeCompany && scopeTheme ? {
441
+ id: "scope",
442
+ label: "Show",
443
+ inline: true,
444
+ value: linkedScope.mode,
445
+ options: [
446
+ { value: "company", label: scopeCompany.label },
447
+ { value: "theme", label: "Theme", description: scopeTheme.label },
448
+ ],
449
+ onChange: linkedScope.setMode,
450
+ } : null;
451
+ const scopeLeading = !scoped || scopeFilter ? undefined
452
+ : scopeCompany ? <TickerBadgeList symbols={[scopeCompany.label]} width={displayWidth(scopeCompany.label) + 2} listed />
453
+ : <Text fg={colors.textDim}>{truncateWithEllipsis(scope.label, Math.max(8, Math.floor(listWidth / 3)))}</Text>;
415
454
  const queryBar = !tableReadable && layers.length < 2 ? undefined : (
416
455
  <QueryBar
417
456
  width={listWidth}
457
+ leading={scopeLeading}
458
+ filters={scopeFilter ? [scopeFilter] : undefined}
418
459
  search={tableReadable ? {
419
460
  value: query,
420
461
  onChange: setQuery,
@@ -142,6 +142,7 @@ export const MAP_PRESET_OPTIONS = [
142
142
  { value: "ports", label: "Ports" },
143
143
  { value: "energy", label: "Pipelines, fields and terminals" },
144
144
  { value: "air", label: "Airports and flights" },
145
+ { value: "facilities", label: "AI buildout facilities" },
145
146
  ] as const;
146
147
 
147
148
  function shortDate(iso: string): string | null {
@@ -329,6 +330,22 @@ export function entityOpenZoom(layer: Pick<GeoLayerInfo, "geometry" | "minZoom">
329
330
  return layer.minZoom === undefined ? base : Math.max(base, 2 ** layer.minZoom);
330
331
  }
331
332
 
333
+ /** Below this span a frame is a region, not a street: one facility shows its surroundings. */
334
+ const FRAME_MIN_SPAN_DEGREES = 6;
335
+ const FRAME_MAX_ZOOM = 8;
336
+
337
+ /** The centre and zoom that frame a set of places (a linked company's facilities); null without one. */
338
+ export function framePlaces(places: readonly { lon: number; lat: number }[]): { longitude: number; latitude: number; zoom: number } | null {
339
+ const located = places.filter((place) => Number.isFinite(place.lon) && Number.isFinite(place.lat));
340
+ if (!located.length) return null;
341
+ const lons = located.map((place) => place.lon);
342
+ const lats = located.map((place) => place.lat);
343
+ const west = Math.min(...lons), east = Math.max(...lons), south = Math.min(...lats), north = Math.max(...lats);
344
+ // The map is about twice as wide as tall, so latitude counts double; a margin keeps edge markers inside.
345
+ const span = Math.max(east - west, (north - south) * 2, FRAME_MIN_SPAN_DEGREES) * 1.5;
346
+ return { longitude: (west + east) / 2, latitude: (south + north) / 2, zoom: Math.min(FRAME_MAX_ZOOM, Math.max(1, 360 / span)) };
347
+ }
348
+
332
349
  /**
333
350
  * The area a layer's table reads. A layer drawn only from a zoom (live
334
351
  * flights) lists what the map shows once it is that close; further out, and
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Layers the app serves itself, beside the server's catalog. A plugin whose
3
+ * rows have a place (TheBuildout's facilities) registers one, and the map,
4
+ * its table and its detail read it through the same wire contract as a server
5
+ * layer: the catalog lists it, and its feature, entity and detail requests are
6
+ * answered in the app without reaching Gloom Cloud. A scope (the company or
7
+ * theme the pane follows) narrows what a local layer answers; server layers
8
+ * never see it.
9
+ */
10
+ import {
11
+ resetGeoCatalogCache,
12
+ type GeoBbox,
13
+ type GeoEntitiesPayload,
14
+ type GeoEntityPayload,
15
+ type GeoFeaturesPayload,
16
+ type GeoLayerInfo,
17
+ type GeoLayersPayload,
18
+ type GeoRequest,
19
+ } from "../../../api-client/geo";
20
+ import { logFailure } from "../../../utils/log-failure";
21
+
22
+ /** What a linked map narrows a local layer to: `company:CRWV`, `theme:<slug>`. */
23
+ export interface GeoScope {
24
+ key: string;
25
+ /** The words the pane shows for it: a ticker or a theme's name. */
26
+ label: string;
27
+ }
28
+
29
+ export interface LocalFeatureQuery {
30
+ bbox: GeoBbox | null;
31
+ zoom: number;
32
+ limit: number;
33
+ scope: string | null;
34
+ }
35
+
36
+ export interface LocalEntityQuery {
37
+ q: string;
38
+ sort: string | null;
39
+ dir: "asc" | "desc";
40
+ limit: number;
41
+ offset: number;
42
+ bbox: GeoBbox | null;
43
+ scope: string | null;
44
+ }
45
+
46
+ export interface LocalGeoLayer {
47
+ info: GeoLayerInfo;
48
+ features: (query: LocalFeatureQuery, signal?: AbortSignal) => Promise<GeoFeaturesPayload>;
49
+ entities: (query: LocalEntityQuery, signal?: AbortSignal) => Promise<GeoEntitiesPayload>;
50
+ entity: (id: string, scope: string | null, signal?: AbortSignal) => Promise<GeoEntityPayload>;
51
+ }
52
+
53
+ const localLayers = new Map<string, LocalGeoLayer>();
54
+
55
+ /** Adds a layer to every map's catalog until the returned function removes it. */
56
+ export function registerLocalGeoLayer(layer: LocalGeoLayer): () => void {
57
+ localLayers.set(layer.info.id, layer);
58
+ // A catalog read before the layer existed would hide it for minutes.
59
+ resetGeoCatalogCache();
60
+ return () => {
61
+ if (localLayers.get(layer.info.id) !== layer) return;
62
+ localLayers.delete(layer.info.id);
63
+ resetGeoCatalogCache();
64
+ };
65
+ }
66
+
67
+ export function isLocalGeoLayer(layerId: string): boolean {
68
+ return localLayers.has(layerId);
69
+ }
70
+
71
+ function bboxParam(value: string | null): GeoBbox | null {
72
+ if (!value) return null;
73
+ const parts = value.split(",").map(Number);
74
+ return parts.length === 4 && parts.every(Number.isFinite) ? parts as GeoBbox : null;
75
+ }
76
+
77
+ function numberParam(value: string | null, fallback: number): number {
78
+ const parsed = value === null ? Number.NaN : Number(value);
79
+ return Number.isFinite(parsed) ? parsed : fallback;
80
+ }
81
+
82
+ /** The server's catalog with the local layers after it; a server without one still lists them. */
83
+ async function mergedCatalog(request: GeoRequest, init?: RequestInit): Promise<GeoLayersPayload> {
84
+ const local = [...localLayers.values()].map((layer) => layer.info);
85
+ const server = await request<GeoLayersPayload>("layers", init).catch((error: unknown) => {
86
+ if (!local.length) throw error;
87
+ logFailure("cloud-geo", "Geo catalog request failed")(error);
88
+ return null;
89
+ });
90
+ const serverLayers = Array.isArray(server?.layers) ? server.layers : [];
91
+ const ids = new Set(serverLayers.map((layer) => layer?.id));
92
+ return { layers: [...serverLayers, ...local.filter((layer) => !ids.has(layer.id))], asOf: server?.asOf ?? null };
93
+ }
94
+
95
+ /** A path segment as written when it is not valid percent-encoding (a stray `%` in an entity id). */
96
+ function decodeSegment(segment: string): string {
97
+ try {
98
+ return decodeURIComponent(segment);
99
+ } catch {
100
+ // Intentional ignore: a segment that does not decode is matched as it was written.
101
+ return segment;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * A request that answers the local layers itself and sends everything else on.
107
+ * `scope` narrows the local layers' answers; a new scope is a new request
108
+ * function, so the map's feed and table ask again.
109
+ */
110
+ export function withLocalGeoLayers(request: GeoRequest, scope: string | null = null): GeoRequest {
111
+ return (<T>(path: string, init?: RequestInit): Promise<T> => {
112
+ const [route = "", search = ""] = path.split("?");
113
+ const params = new URLSearchParams(search);
114
+ const signal = init?.signal ?? undefined;
115
+ if (route === "layers") return (localLayers.size ? mergedCatalog(request, init) : request(path, init)) as Promise<T>;
116
+ const parts = route.split("/").map(decodeSegment);
117
+ const layer = parts.length >= 3 ? localLayers.get(parts[1]!) ?? null : null;
118
+ if (parts[0] === "layers" && layer && parts[2] === "features") {
119
+ return layer.features({
120
+ bbox: bboxParam(params.get("bbox")),
121
+ zoom: numberParam(params.get("zoom"), 0),
122
+ limit: numberParam(params.get("limit"), 1500),
123
+ scope,
124
+ }, signal) as Promise<T>;
125
+ }
126
+ if (parts[0] === "layers" && layer && parts[2] === "entities") {
127
+ return layer.entities({
128
+ q: params.get("q") ?? "",
129
+ sort: params.get("sort"),
130
+ dir: params.get("dir") === "asc" ? "asc" : "desc",
131
+ limit: numberParam(params.get("limit"), 100),
132
+ offset: numberParam(params.get("offset"), 0),
133
+ bbox: bboxParam(params.get("bbox")),
134
+ scope,
135
+ }, signal) as Promise<T>;
136
+ }
137
+ const entityLayer = parts[0] === "entities" ? localLayers.get(parts[1] ?? "") : undefined;
138
+ if (entityLayer && parts[2]) return entityLayer.entity(parts.slice(2).join("/"), scope, signal) as Promise<T>;
139
+ return request<T>(path, init);
140
+ }) as GeoRequest;
141
+ }
@@ -18,6 +18,9 @@ export type MapSymbolId =
18
18
  | "terminal"
19
19
  | "field"
20
20
  | "chokepoint"
21
+ | "rack"
22
+ | "bolt"
23
+ | "chip"
21
24
  | "dot";
22
25
 
23
26
  export interface MapSymbol {
@@ -85,6 +88,17 @@ export const MAP_SYMBOLS: Record<MapSymbolId, MapSymbol> = {
85
88
  landmark: true,
86
89
  cell: "◆",
87
90
  },
91
+ // A server rack: the cabinet and its shelves, for a data center.
92
+ rack: { d: "M-5 -7.2H5V7.2H-5ZM-5 -2.4H5M-5 2.4H5M-2.6 -4.8H-0.6M-2.6 0H-0.6M-2.6 4.8H-0.6", paint: "stroke", size: 13, cell: "▤" },
93
+ // A lightning bolt, for a power plant or substation.
94
+ bolt: { d: "M1.8 -8L-5.4 1.4H-0.6L-2 8L5.4 -1.8H0.6Z", paint: "fill", size: 13, cell: "↯" },
95
+ // A chip and its pins, for a fab or a packaging plant.
96
+ chip: {
97
+ d: "M-4.6 -4.6H4.6V4.6H-4.6ZM-2 -4.6V-7.6M2 -4.6V-7.6M-2 4.6V7.6M2 4.6V7.6M-4.6 -2H-7.6M-4.6 2H-7.6M4.6 -2H7.6M4.6 2H7.6",
98
+ paint: "stroke",
99
+ size: 13,
100
+ cell: "▣",
101
+ },
88
102
  dot: { d: "M0 -3.4A3.4 3.4 0 1 1 0 3.4A3.4 3.4 0 1 1 0 -3.4Z", paint: "fill", size: 11, cell: "•" },
89
103
  };
90
104
 
@@ -162,6 +176,10 @@ const RULES: Record<string, SymbolRule> = {
162
176
  },
163
177
  terminal: () => fixed("terminal"),
164
178
  field: () => fixed("field"),
179
+ // Facilities (TheBuildout): by what the site is; anything else is a dot.
180
+ "data-center": () => fixed("rack"),
181
+ "power-plant": () => fixed("bolt"),
182
+ fab: () => fixed("chip"),
165
183
  };
166
184
 
167
185
  /** The layer registry: layer ids the app knows, then the kinds their entities carry. */
@@ -43,6 +43,8 @@ export interface WorldMapFocus {
43
43
  longitude: number;
44
44
  latitude: number;
45
45
  zoom?: number;
46
+ /** Exactly at `zoom`, out as well as in: a frame around a set of places. */
47
+ exact?: boolean;
46
48
  }
47
49
 
48
50
  interface WorldVenueMapProps {
@@ -690,8 +692,8 @@ function DesktopWorldVenueMap(props: WorldVenueMapProps) {
690
692
  if (!focus || !sized || appliedFocusRef.current === focus.key) return;
691
693
  appliedFocusRef.current = focus.key;
692
694
  setViewport((current) => {
693
- const zoom = Math.max(current.zoom, focus.zoom ?? 1);
694
- if (zoom <= 1) return current;
695
+ const zoom = focus.exact ? Math.max(1, focus.zoom ?? 1) : Math.max(current.zoom, focus.zoom ?? 1);
696
+ if (zoom <= 1 && !focus.exact) return current;
695
697
  return clampWorldMapViewport({ zoom, centerLongitude: focus.longitude, centerLatitude: focus.latitude }, props.width, plotHeight, 1, maxZoom);
696
698
  });
697
699
  }, [focusKey, sized]);